diff --git a/.github/workflows/extended_checks.yml b/.github/workflows/extended_checks.yml index 319ea4ab50..d6e8d4cc4d 100644 --- a/.github/workflows/extended_checks.yml +++ b/.github/workflows/extended_checks.yml @@ -70,6 +70,9 @@ jobs: steps: - name: "SCM Checkout" uses: actions/checkout@v4 + with: + # Full history so the lint step can `git diff` against the PR base branch. + fetch-depth: 0 - name: "Install CMake and Ninja" uses: lukka/get-cmake@latest @@ -178,6 +181,7 @@ jobs: set -eux cmake --build ./build --config Release --target all_utils_exe $BIN/daslang -exe -output ./bin/das-fmt ./utils/das-fmt/dasfmt.das + $BIN/daslang -exe -output ./bin/das-lint ./utils/lint/main.das - name: "Sequence release smoke test" # Full daspkg install -> release -> launch cycle on the in-tree sequence @@ -218,6 +222,21 @@ jobs: $BIN/daslang ./utils/das-fmt/dasfmt.das -- --path ./ --verify $BIN/das-fmt.exe --path ./ --verify + - name: "Run lint on changed .das files" + if: matrix.target == 'linux' + run: | + set -eux + BASE_REF="${{ github.event.pull_request.base.ref }}" + BASE_REF="${BASE_REF:-master}" + mapfile -t CHANGED < <(git diff --name-only --diff-filter=AM "origin/${BASE_REF}...HEAD" -- '*.das') + if [ ${#CHANGED[@]} -eq 0 ]; then + echo "no .das files changed; skipping lint" + exit 0 + fi + echo "linting: ${CHANGED[*]}" + $BIN/daslang ./utils/lint/main.das -- "${CHANGED[@]}" --quiet + $BIN/das-lint.exe "${CHANGED[@]}" --quiet + - name: "Test daslang_static" run: | set -eux @@ -227,6 +246,12 @@ jobs: $BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests --ser serialized.bin $BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests --deser serialized.bin + - name: "Test MCP tools" + if: matrix.target == 'linux' + run: | + set -eux + $BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./utils/mcp/test_tools.das + - name: "Run self-binder (bind_clangbind.das)" if: matrix.target == 'linux' run: | diff --git a/.gitignore b/.gitignore index 3f9c8044b1..10b523165a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ build-asan/ build-ubsan/ build-linux-asan/ build-linux/ +build_eastl/ +# EASTL local-verify checkouts (build_eastl uses these via -isystem) +eastl/ +eabase/ # docs site/doc-latex/ # daspkg per-package cmake build dirs (utils/daspkg/commands.das:976 -> {pkg_dir}/_build) @@ -97,6 +101,11 @@ modules/.daspkg_cache/ modules/.daspkg.log examples/**/modules/ utils/**/modules/ +# Exception: MCP test fixture for project_root flag (utils/mcp/test_tools.das +# Tier 1 tests need a daspkg-style modules//.das_module layout to verify +# that -project_root flows through correctly). +!utils/mcp/tests/_pretend_root/modules/ +!utils/mcp/tests/_pretend_root/modules/** # Claude Code (local settings + runtime state) .claude/ diff --git a/CLAUDE.md b/CLAUDE.md index 083b83e714..086dc62d6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,6 +67,7 @@ Task-specific instructions are split into skill files under `skills/`. You MUST | `skills/writing_benchmarks.md` | Writing/running `benchmarks/` files | | `skills/daspkg.md` | Running daspkg commands, `.das_package` manifests | | `skills/dynamic_modules.md` | `.das_module` descriptors, adding modules under `modules/` | +| `skills/external_module_debugging.md` | Working on an external daslang module (dasImgui, dasPUGIXML, dasSQLITE, etc.) locally — need to run/lint/test from a standalone daslang.exe or via MCP before push-to-CI. Covers the `/modules/` junction pattern + `project_root` MCP arg | | `skills/install_instructions.md` | Updating `install/CLAUDE.md` or `install/skills/` for the shipped SDK | | `skills/aot_testing.md` | AOT test files, `test_aot` binary, `Module::aotRequire()`, AOT hash mismatches | | `skills/visitor_gen_bind.md` | Adding `Visitor` virtual methods / `canVisit*` gates / `gen_bind.das` regen | @@ -269,6 +270,9 @@ Full migration table (when reading older docs that say `var inscope` or `<-` for | `int(BfT.a) \| int(BfT.b)` (same bitfield, or enum with `operator \|`) | `int(BfT.a \| BfT.b)` | PERF019: collapse two int casts to one. Const-foldable forms only surface under lint policies | | `foo \|= BfT.m` / `foo &= ~BfT.m` (bitfield `foo`, single named bit) | `foo.m = true` / `foo.m = false` | STYLE022: bitfield-as-field assignment reads bit-name-first, drops the `~` for clears | | `uint(bf & BfT.m) != 0u` / `int(bf & BfT.m) == 0` (bitfield `bf`, single named bit) | `bf.m` / `!bf.m` | STYLE023: bitfield-as-field read; drop the int cast + `!= 0` / `== 0` compare | +| `unsafe(x + y)` / `unsafe { let d = x + y }` where nothing inside requires unsafe | drop the wrap | STYLE024: redundant `unsafe` — flagged when no descendant matches a known inherently-unsafe shape (reinterpret/upcast cast, `delete`, `addr`, table-index, variant-write, ExprCallFunc with `unsafeOperation`). Macro-generated subtrees (`genFlags.generated == true`) skipped per design | +| `unsafe { stmt1; stmt2; stmt_needing_unsafe }` (only ONE stmt actually needs unsafe) | `stmt1; stmt2; unsafe()` | STYLE025: narrow block-form unsafe to expression-form on the single unsafe-needing statement. Silent when ≥2 statements need unsafe (block is justified) | +| `unsafe { ...; unsafe { ... }; ... }` (nested `unsafe { }` block) | drop the inner wrap | STYLE026: outer `unsafe` already covers the whole inner scope, so the inner block is pure noise. Closure / lambda / generator bodies are NOT nested for this rule — they execute in a separate context where the outer wrap does not propagate | For path/filename ops use `fio` helpers (`base_name`/`dir_name`/`path_join`/etc.) — see `skills/filesystem.md`. Never hand-roll `rfind("/")` / slice — misses Windows separators. diff --git a/cmake/das_config_eastl/das_config.h b/cmake/das_config_eastl/das_config.h index 3bf3a09219..c00fcd6b41 100644 --- a/cmake/das_config_eastl/das_config.h +++ b/cmake/das_config_eastl/das_config.h @@ -55,11 +55,11 @@ using std::thread; using std::unique_lock; } // namespace das -#if (!defined(DAS_ENABLE_EXCEPTIONS)) || (!DAS_ENABLE_EXCEPTIONS) -#define FMT_THROW(x) das::das_throw(((x).what())) namespace das { void das_throw(const char * msg); } +#if (!defined(DAS_ENABLE_EXCEPTIONS)) || (!DAS_ENABLE_EXCEPTIONS) +#define FMT_THROW(x) das::das_throw(((x).what())) #endif #if DAS_CUSTOM_HASH diff --git a/daslib/archive.das b/daslib/archive.das index 7790a3fcca..2707f7d830 100644 --- a/daslib/archive.das +++ b/daslib/archive.das @@ -183,9 +183,7 @@ def public serialize(var arch : Archive; var value : auto(TT)&) { delete value var index : int arch |> read_raw(index) - unsafe { - value |> set_variant_index(index) - } + unsafe(value |> set_variant_index(index)) } else { var index = variant_index(value) arch |> write_raw(index) @@ -335,9 +333,7 @@ def public serialize(var arch : Archive; var value : array) { arch.stream->read(unsafe(addr(value[0])), len * typeinfo sizeof(type)) } } else { - unsafe { - value |> resize(len) - } + unsafe(value |> resize(len)) for (element in value) { arch |> _::serialize(element) } diff --git a/daslib/array_boost.das b/daslib/array_boost.das index 6d33d18f7a..0634bdf41b 100644 --- a/daslib/array_boost.das +++ b/daslib/array_boost.das @@ -84,9 +84,7 @@ def public temp_array(var data : auto? ==const; lenA : int; a : auto(TT)) : arra //! * data memory does not change within the lifetime of the returned array var res : array if (lenA >= 1) { - unsafe { - _builtin_make_temp_array(res, data, lenA) - } + unsafe(_builtin_make_temp_array(res, data, lenA)) } return <- res } @@ -101,9 +99,7 @@ def public temp_array(data : auto? ==const; lenA : int; a : auto(TT)) : array if (lenA >= 1) { - unsafe { - _builtin_make_temp_array(res, data, lenA) - } + unsafe(_builtin_make_temp_array(res, data, lenA)) } return <- res } diff --git a/daslib/jobque_boost.das b/daslib/jobque_boost.das index 72adcfbdff..348899cb3a 100644 --- a/daslib/jobque_boost.das +++ b/daslib/jobque_boost.das @@ -131,10 +131,8 @@ def for_each(channel : Channel?; blk : block<(res : auto(TT)#) : void>) { void_data = vd } if (void_data == null) break - unsafe { - let typed_data = reinterpret void_data - invoke(blk, *typed_data) - } + let typed_data = unsafe(reinterpret void_data) + invoke(blk, *typed_data) } } @@ -165,11 +163,9 @@ def pop_one(channel : Channel?; blk : block<(res : auto(TT)#) : void>) { void_data = vd } if (void_data == null) return false - unsafe { - let typed_data = reinterpret void_data - invoke(blk, *typed_data) - return true - } + let typed_data = unsafe(reinterpret void_data) + invoke(blk, *typed_data) + return true } def pop_and_clone_one(channel : Channel?; blk : block<(res : auto(TT)#) : void>) { diff --git a/daslib/json_boost.das b/daslib/json_boost.das index 80369732ba..baa928f4af 100644 --- a/daslib/json_boost.das +++ b/daslib/json_boost.das @@ -236,12 +236,7 @@ def from_JV(v : JsonValue const explicit?; ent : auto(EnumT); defV : EnumT = def var res : auto(EnumTT) = default let ti = typeinfo rtti_typeinfo(type) for (ef in *ti.enumType) { - if (name == ef.name) { - unsafe { - res = reinterpret(ef.value) - } - return res - } + return unsafe(reinterpret(ef.value)) if (name == ef.name) } panic("not a valid enumeration {name} in {typeinfo typename(type)}") } else { @@ -493,9 +488,7 @@ def from_JV(v : JsonValue const explicit?; anything : auto(TT)) { let vv : JsonValue? = (v.value as _object)?["$variant"] ?? default if (vv == null || !((vv.value is _number) || (vv.value is _longint))) return <- ret let index = (vv.value is _number) ? int(vv.value as _number) : int(vv.value as _longint) - unsafe { - set_variant_index(ret, index) - } + unsafe(set_variant_index(ret, index)) apply(ret) $(name : string; var field) { (v as _object) |> get(name) $(val) { move_to_ref(field, _::from_JV(val, decltype_noref(field))) diff --git a/daslib/rtti.das b/daslib/rtti.das index 1ac4abb645..5239a13e8a 100644 --- a/daslib/rtti.das +++ b/daslib/rtti.das @@ -188,9 +188,7 @@ def type_info(vinfo : VarInfo) : TypeInfo const? { [generic] def RttiValue_nothing { var t : RttiValue - unsafe { - set_variant_index(t, typeinfo variant_index(t)) - } + unsafe(set_variant_index(t, typeinfo variant_index(t))) return t } diff --git a/daslib/style_lint.das b/daslib/style_lint.das index 0021784b26..dbf8df4fd7 100644 --- a/daslib/style_lint.das +++ b/daslib/style_lint.das @@ -31,6 +31,9 @@ module style_lint shared private //! STYLE020 — from_JV(v, type, defV) on a scalar — use 'v ?? defV' (operator ?? from json_boost) //! STYLE021 — table built by repeated insert — use 'var v = JV((k1=..., k2=...))' //! STYLE022 — bitfield compound-bit-set/clear ('foo |= BfT.m' / 'foo &= ~BfT.m') — use field-assign 'foo.m = true' / 'foo.m = false' +//! STYLE024 — redundant 'unsafe' wrap; inner expression / block body has no operation requiring unsafe — drop the wrap +//! STYLE025 — 'unsafe { ... }' block where only one statement needs unsafe — narrow to 'unsafe()' +//! STYLE026 — nested 'unsafe { ... }' block; outer wrap already covers the scope — drop the inner require daslib/ast_boost require strings @@ -53,6 +56,11 @@ class StyleLintVisitor : AstVisitor { @do_not_delete current_function : Function? // STYLE011: track uninitialized variables from most recent ExprLet @do_not_delete pending_uninit_vars : array + // STYLE024/025 — tight unsafe checks. + @do_not_delete skip_userSaidItsSafe : array + @do_not_delete unsafeExprs : table + unsafe_stack : array + unsafe_block_stack : array def StyleLintVisitor() { pass @@ -112,6 +120,7 @@ class StyleLintVisitor : AstVisitor { def override preVisitFunction(var fn : FunctionPtr) : void { current_function = fn + unsafe_block_stack |> push(0) self->check_defer_pipe(fn) } @@ -156,6 +165,7 @@ class StyleLintVisitor : AstVisitor { def override visitFunction(var fn : FunctionPtr) : FunctionPtr { current_function = null + unsafe_block_stack |> pop return <- fn } @@ -210,6 +220,17 @@ class StyleLintVisitor : AstVisitor { self->check_block_pipe(expr.at, expr.arguments) self->check_style019_clamp(expr) self->check_style020_from_jv(expr) + if (expr.genFlags.generated) return + if (self->call_func_needs_unsafe(expr.func, expr.flags.isForLoopSource)) { + self->mark_unsafe_in_stack() + return + } + if (expr.func != null && expr.func.flags.isClassMethod + && expr.func.result != null && expr.func.result.isClass + && expr.func.result.structType != null + && expr.func.name == expr.func.result.structType.name) { + self->mark_unsafe_in_stack() + } } // --- STYLE020: from_JV scalar form → v ?? defV --- @@ -309,6 +330,10 @@ class StyleLintVisitor : AstVisitor { // --- STYLE001/002 for desugared generators --- def override preVisitExprMakeStruct(var expr : ExprMakeStruct?) : void { + if (!expr.genFlags.generated && !expr.makeStructFlags.isNewClass + && expr.makeType != null && expr.makeType.isClass) { + self->mark_unsafe_in_stack() + } // generator() <| $ { ... } is lowered before lint runs — ExprMakeGenerator // is replaced by ExprMakeStruct. Detect via source line inspection. if (expr.at.fileInfo == null || expr.at.line == 0u) return @@ -367,6 +392,10 @@ class StyleLintVisitor : AstVisitor { } def override preVisitExprOp2(expr : ExprOp2?) : void { + if (!expr.genFlags.generated + && self->call_func_needs_unsafe(expr.func, expr.flags.isForLoopSource)) { + self->mark_unsafe_in_stack() + } if (expr.op == "==") { if (self->is_string_rtti_comparison(expr.left) || self->is_string_rtti_comparison(expr.right)) { self->style_warning("STYLE006: string(__rtti) comparison should use `is` operator; e.g. expr is ExprFoo", expr.at) @@ -659,6 +688,15 @@ class StyleLintVisitor : AstVisitor { if (v.init == null && !v.flags.generated && !v.flags.inScope && (current_function == null || current_function.fromGeneric == null)) { pending_uninit_vars |> push(v) } + // STYLE024/025: `let v & = unsafe(expr)` — local reference binding to + // a non-local expression requires unsafe per the compiler. Record the + // init-expression's `at` so preVisitExpression won't flag the wrap as + // redundant (the wrap is required by the let-binding, not by the inner + // expression's shape). + if (v.init != null && v._type != null && v._type.flags.ref + && v.init.genFlags.userSaidItsSafe) { + skip_userSaidItsSafe |> push(v.init) + } } def find_assign_var(expr : Expression?) : Variable? { @@ -674,6 +712,9 @@ class StyleLintVisitor : AstVisitor { // --- STYLE017(b): adjacent `if (cond) { return b1 }; return b2` (b1 != b2) --- def override preVisitExprBlock(blk : ExprBlock?) : void { + if (blk.blockFlags.isClosure) { + unsafe_block_stack |> push(0) + } let n = length(blk.list) for (i in range(n - 1)) { let cur = blk.list[i] @@ -704,6 +745,13 @@ class StyleLintVisitor : AstVisitor { } } + def override visitExprBlock(var blk : ExprBlock?) : ExpressionPtr { + if (blk.blockFlags.isClosure) { + unsafe_block_stack |> pop + } + return blk + } + def override preVisitExprBlockExpression(blk : ExprBlock?; expr : ExpressionPtr) : void { if (!empty(pending_uninit_vars)) { if (expr is ExprCopy) { @@ -1187,6 +1235,191 @@ class StyleLintVisitor : AstVisitor { } return false } + + // --- STYLE024/STYLE025: redundant or over-broad `unsafe` --- + + def unsafe_count_for(expr : Expression?) : int { + return 0 if (expr == null) + return unsafeExprs |> key_exists(expr) ? unsafeExprs[expr] : 0 + } + + def mark_unsafe_in_stack() : void { + //! Add 1 to the current top of `unsafe_stack`. + let n = length(unsafe_stack) + if (n > 0) { + unsafe_stack[n - 1] = unsafe_stack[n - 1] + 1 + } + } + + def override preVisitExpression(expr : ExpressionPtr) : void { + //! Push a fresh 0 slot for this node. + unsafe_stack |> push(0) + } + + def override visitExpression(var expr : ExpressionPtr) : ExpressionPtr { + //! Pop the slot for this node. The popped value is the subtree's + //! count of inherently-unsafe leaves. Use it to: + //! - store `unsafeExprs[expr.at] = count` for later queries + //! (used by `visitExprUnsafe` to decide STYLE024 vs STYLE025), + //! - propagate count to the parent's slot (now top after our pop), + //! - fire STYLE024 if `expr` is an `unsafe(...)` wrap target + //! (`userSaidItsSafe`) and its subtree had count 0. + let n = length(unsafe_stack) + let count = unsafe_stack[n - 1] + unsafe_stack |> pop + if (count > 0) { + unsafeExprs |> insert(expr, count) + let m = length(unsafe_stack) + if (m > 0) { + unsafe_stack[m - 1] = unsafe_stack[m - 1] + count + } + } + // STYLE024 expression form: parser sets `userSaidItsSafe` on the + // inner expr of `unsafe(expr)`. Suppress when the wrap is required + // by an enclosing `let x & = ...` ref-binding. + if (expr.genFlags.userSaidItsSafe && !expr.genFlags.generated + && (current_function == null || current_function.fromGeneric == null) + && !(skip_userSaidItsSafe |> has_value(expr)) + && count == 0) { + self->style_warning("STYLE024: redundant 'unsafe(...)' wrap; the inner expression has no operation that requires unsafe — drop the wrap", expr.at) + } + return expr + } + + def call_func_needs_unsafe(func : Function const?; is_for_loop_src : bool) : bool { + return (func == null + || func.flags.unsafeOperation + || (func.moreFlags.unsafeOutsideOfFor && !is_for_loop_src)) + } + + // Inherently-unsafe leaves — mark unconditionally (when not generated). + + def override preVisitExprDelete(expr : ExprDelete?) : void { + if (!expr.genFlags.generated) self->mark_unsafe_in_stack() + } + def override preVisitExprAddr(expr : ExprAddr?) : void { + if (!expr.genFlags.generated) self->mark_unsafe_in_stack() + } + def override preVisitExprRef2Ptr(expr : ExprRef2Ptr?) : void { + // `addr(x)` — address-of-reference. Always unsafe. + if (!expr.genFlags.generated) self->mark_unsafe_in_stack() + } + def override preVisitExprAsVariant(expr : ExprAsVariant?) : void { + if (!expr.genFlags.generated) self->mark_unsafe_in_stack() + } + + // Conditional own-contribution. + + def override preVisitExprCast(expr : ExprCast?) : void { + if (!expr.genFlags.generated + && (expr.castFlags.upcastCast || expr.castFlags.reinterpretCast)) { + self->mark_unsafe_in_stack() + } + } + + def override preVisitExprAt(expr : ExprAt?) : void { + // Table / raw-pointer index needs unsafe; array-index is safe. Lint + // pass sometimes leaves `_type` unset on the subexpr — bias to + // "needs unsafe" when unknown so the wrap stays. + if (expr.genFlags.generated) return + let unknown_subexpr_type = (expr.subexpr == null || expr.subexpr._type == null) + let is_unsafe_index = (!unknown_subexpr_type + && (expr.subexpr._type.baseType == Type.tTable + || expr.subexpr._type.baseType == Type.tPointer)) + if (unknown_subexpr_type || is_unsafe_index) { + self->mark_unsafe_in_stack() + } + } + + def override preVisitExprField(expr : ExprField?) : void { + // variant.field requires unsafe (write context; over-mark on read + // is safer than under-mark — biases toward keeping wraps). + if (!expr.genFlags.generated && expr.value != null + && expr.value._type != null + && expr.value._type.baseType == Type.tVariant) { + self->mark_unsafe_in_stack() + } + } + + def override preVisitExprReturn(expr : ExprReturn?) : void { + // Returning a reference or a temporary requires unsafe + if (expr.genFlags.generated) return + var has = expr.returnFlags.returnReference + if (!has && expr.subexpr != null && expr.subexpr._type != null) { + let t = expr.subexpr._type + has = t.flags.ref || t.flags.temporary + } + if (!has && current_function != null && current_function.result != null) { + let rt = current_function.result + has = rt.flags.ref || rt.flags.temporary + } + if (!has && expr.returnFlags.moveSemantics && expr.subexpr != null) { + has = (expr.subexpr._type == null + || expr.subexpr._type.baseType == Type.autoinfer + || expr.subexpr._type.baseType == Type.alias + || expr.subexpr._type.flags.ref || expr.subexpr._type.flags.temporary) + } + if (has) self->mark_unsafe_in_stack() + } + + def override preVisitExprOp1(expr : ExprOp1?) : void { + if (!expr.genFlags.generated + && self->call_func_needs_unsafe(expr.func, expr.flags.isForLoopSource)) { + self->mark_unsafe_in_stack() + } + } + def override preVisitExprOp3(expr : ExprOp3?) : void { + if (!expr.genFlags.generated + && self->call_func_needs_unsafe(expr.func, expr.flags.isForLoopSource)) { + self->mark_unsafe_in_stack() + } + } + def override preVisitExprNew(expr : ExprNew?) : void { + if (!expr.genFlags.generated + && (expr.func == null || expr.func.flags.unsafeOperation)) { + self->mark_unsafe_in_stack() + } + } + + def override preVisitExprUnsafe(expr : ExprUnsafe?) : void { + let n = length(unsafe_block_stack) + if (expr != null && !expr.genFlags.generated + && (current_function == null || current_function.fromGeneric == null) + && n > 0 && unsafe_block_stack[n - 1] > 0) { + self->style_warning("STYLE026: nested 'unsafe \{ ... }' block; the outer 'unsafe' already covers this scope — drop the inner wrap", expr.at) + } + if (n > 0) { + unsafe_block_stack[n - 1]++ + } + } + + def override visitExprUnsafe(var expr : ExprUnsafe?) : ExpressionPtr { + let n = length(unsafe_block_stack) + if (n > 0) { + unsafe_block_stack[n - 1]-- + } + // Post-order: `unsafe { ... }` block form. The body's own + // `visitExpression` already aggregated its statements' counts into + // `unsafeExprs[body]` — just look it up. + // 0 unsafe leaves → wrap redundant (STYLE024) + // 1 unsafe leaf → narrow to `unsafe()` (STYLE025) + // ≥ 2 leaves → block is justified, silent + if (expr == null || expr.body == null || expr.genFlags.generated + || (current_function != null && current_function.fromGeneric != null) + || !(expr.body is ExprBlock)) return expr + let blk = expr.body as ExprBlock + let count = self->unsafe_count_for(blk) + if (count == 0) { + self->style_warning("STYLE024: redundant 'unsafe \{ ... }' block; no statement requires unsafe — drop the wrap", expr.at) + } elif (count == 1 && + !(blk.list[0] is ExprYield || + blk.list[0] is ExprDelete || + blk.list[0] is ExprReturn || + blk.list[0] is ExprNew)) { + self->style_warning("STYLE025: 'unsafe \{ ... }' block scope is too broad; only one operation requires unsafe — narrow to 'unsafe()' wrapping that operation", expr.at) + } + return expr + } } // --------------------------------------------------------------------------- @@ -1204,6 +1437,10 @@ def public style_lint(prog : ProgramPtr; compile_time_errors : bool; comment_hyg if (comment_hygiene) { astVisitor->scan_long_comment_blocks(prog) } + // STYLE024/025 stack-balance sanity check. + if (!empty(astVisitor.unsafe_stack)) { + panic("style_lint internal: unsafe_stack not balanced after walk (size={length(astVisitor.unsafe_stack)})") + } let count = astVisitor.warning_count unsafe { delete astVisitor; } return count @@ -1220,6 +1457,10 @@ def public style_lint_collect(prog : ProgramPtr; var warnings : array; c if (comment_hygiene) { astVisitor->scan_long_comment_blocks(prog) } + // STYLE024/025 stack-balance sanity check. + if (!empty(astVisitor.unsafe_stack)) { + panic("style_lint internal: unsafe_stack not balanced after walk (size={length(astVisitor.unsafe_stack)})") + } let count = astVisitor.warning_count warnings |> reserve(length(astVisitor.warnings)) for (w in astVisitor.warnings) { diff --git a/include/daScript/das_config.h b/include/daScript/das_config.h index 3eaf7a45aa..112ccf0ee4 100644 --- a/include/daScript/das_config.h +++ b/include/daScript/das_config.h @@ -34,11 +34,11 @@ namespace das {using namespace std;} -#if (!defined(DAS_ENABLE_EXCEPTIONS)) || (!DAS_ENABLE_EXCEPTIONS) -#define FMT_THROW(x) das::das_throw(((x).what())) namespace das { void das_throw(const char * msg); } +#if (!defined(DAS_ENABLE_EXCEPTIONS)) || (!DAS_ENABLE_EXCEPTIONS) +#define FMT_THROW(x) das::das_throw(((x).what())) #endif #if DAS_CUSTOM_HASH diff --git a/include/das_hash_map/das_hash_map.h b/include/das_hash_map/das_hash_map.h index d7c8302a27..ceab0c3543 100644 --- a/include/das_hash_map/das_hash_map.h +++ b/include/das_hash_map/das_hash_map.h @@ -10,42 +10,26 @@ // Probe hash sentinels match anyhash.h: 0 = EMPTY, 1 = KILLED. User-supplied // hashes that collide with sentinels are remapped to the FNV-64 prime. // -// K and V must be default-constructible: rehash allocates `vector(new_cap)` / -// `vector(new_cap)` and erase assigns `K{}` / `V{}` over killed slots. This +// K and V must be default-constructible: rehash allocates `das::vector(new_cap)` / +// `das::vector(new_cap)` and erase assigns `K{}` / `V{}` over killed slots. This // matches runtime_table.h's design and daslang's actual call sites (all AST / // pointer / primitive types). Supporting non-default-constructible K/V would need // uninitialized slot storage with placement-new-on-demand — out of scope here. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// This header is pulled in by das_config.h, which is itself pulled in through -// platform.h before platform.h finishes declaring __forceinline / NO_ASAN_INLINE / -// DAS_SUPPRESS_UB. So we stay dependency-free and provide a local __forceinline -// fallback for non-MSVC and inline the FNV-64 hash directly. +// This header is pulled in by das_config.h, which already includes , , +// , , , etc. before reaching us. Platform.h's +// __forceinline / NO_ASAN_INLINE / DAS_SUPPRESS_UB are NOT yet defined at this point, +// so we provide a local __forceinline fallback below and inline the FNV-64 hash +// directly (cannot pull in anyhash.h for the same reason). The same constraint +// is why at() routes its missing-key path through das::das_throw (declared by +// das_config.h itself, just above us) rather than raw `throw`: games embedding +// daslang frequently disable C++ exceptions entirely. #if !defined(_MSC_VER) && !defined(__forceinline) #define __forceinline inline __attribute__((always_inline)) #endif namespace das { -// harmless if caller already did `using namespace std` inside das:: -using std::vector; -using std::pair; -using std::hash; -using std::equal_to; -using std::move; -using std::forward; -using std::forward_iterator_tag; - namespace daslang_hash_map_detail { static constexpr uint64_t HASH_EMPTY = 0; @@ -72,7 +56,7 @@ namespace daslang_hash_map_detail { // block[1]; fast path does a 2-byte read and tolerates the 1-byte overread. // // Under ASAN, default DAS_SAFE_HASH to 1 — the fast path's overread crosses the - // std::string allocation boundary (heap allocs are sized to chars+null exactly), + // das::string allocation boundary (heap allocs are sized to chars+null exactly), // which ASAN flags. Cannot use NO_ASAN_INLINE here because this header is pulled // in before platform.h sets it up. // @@ -126,24 +110,24 @@ namespace daslang_hash_map_detail { // AST hash keys are already hash-quality (wyhash outputs used as keys), // so no further mixing is needed. to_hash_key still guards collisions // with 0/1 just in case. -// * null-terminated C strings and std::string: hash_blockz64 (FNV-64 from anyhash.h). -// * anything else: std::hash fallback. +// * null-terminated C strings and das::string: hash_blockz64 (FNV-64 from anyhash.h). +// * anything else: das::hash fallback. // template struct daslang_hash { - size_t operator () ( const T & x ) const { return std::hash{}(x); } + size_t operator () ( const T & x ) const { return das::hash{}(x); } }; template -struct daslang_hash::value>> { +struct daslang_hash::value>> { __forceinline size_t operator () ( T x ) const noexcept { return size_t(x) + 2; } }; // Pointer keys: raw `ptr >> 4` clusters catastrophically on tight-stride -// allocations (1M short heap strings: 36× regression). std::hash (raw ptr) +// allocations (1M short heap strings: 36× regression). das::hash (raw ptr) // is only mediocre. Fibonacci hashing — shift-out-alignment + golden-ratio // multiply — is the winner in our benchmarks: // N=100: 0.48× ska N=10k: 0.77× ska N=1M: 0.87× ska @@ -159,8 +143,8 @@ struct daslang_hash { }; template <> -struct daslang_hash { - __forceinline size_t operator () ( const std::string & s ) const noexcept { +struct daslang_hash { + __forceinline size_t operator () ( const das::string & s ) const noexcept { return size_t(daslang_hash_map_detail::hash_blockz64(reinterpret_cast(s.c_str()))); } }; @@ -183,9 +167,9 @@ struct daslang_hash { // daslang_hash_map // -template , class KeyEqual = equal_to> +template , class KeyEqual = das::equal_to> class daslang_hash_map { - // std::vector is specialized and returns proxy references — incompatible + // das::vector is specialized and returns proxy references — incompatible // with our iterator's V& contract. Use uint8_t storage for V=bool and cast at // the access points. bool and uint8_t share size and representation on every // platform daslang ships on. @@ -195,14 +179,14 @@ class daslang_hash_map { // uint8_t, and bool values outside {0,1} are trap representations). In practice // MSVC/gcc/clang all compile it correctly — we only ever store 0 or 1 through // the bool side, and we never take addresses across the boundary. This is a - // conscious trade vs (a) storing real bool + dealing with vector's proxy + // conscious trade vs (a) storing real bool + dealing with das::vector's proxy // iterators, or (b) memcpy'ing every read/write. Revisit if a sanitizer fires. - static constexpr bool is_bool_value = std::is_same::value; - using value_storage_t = typename std::conditional::type; + static constexpr bool is_bool_value = das::is_same::value; + using value_storage_t = typename das::conditional::type; - vector hashes_; // HASH_EMPTY / HASH_KILLED / remapped-hash - vector keys_; - vector values_; + das::vector hashes_; // HASH_EMPTY / HASH_KILLED / remapped-hash + das::vector keys_; + das::vector values_; size_t size_ = 0; size_t tombstones_ = 0; Hash hash_; @@ -220,21 +204,21 @@ class daslang_hash_map { // returned by iterator::operator* — aggregate with two reference members // so C++17 structured bindings (`auto [k, v] = *it`) bind directly to the - // underlying container slots without going through std::tuple_size. - // `first` is a NON-const K& — matches ska::flat_hash_map's `value_type = pair` - // rather than std::unordered_map's `pair`. Mutating the key through + // underlying container slots without going through das::tuple_size. + // `first` is a NON-const K& — matches ska::flat_hash_map's `value_type = das::pair` + // rather than das::unordered_map's `das::pair`. Mutating the key through // this reference would corrupt the hash; callers must not do that (matches the - // ska contract used throughout daslang). Converts implicitly to pair so + // ska contract used throughout daslang). Converts implicitly to das::pair so // code like `auto kv = *it; m2.insert(kv);` still compiles. struct reference { K & first; V & second; - operator pair () const { return { first, second }; } + operator das::pair () const { return { first, second }; } }; struct const_reference { const K & first; const V & second; - operator pair () const { return { first, second }; } + operator das::pair () const { return { first, second }; } }; class const_iterator; @@ -252,17 +236,17 @@ class daslang_hash_map { // GCC's [basic.scope.class] diagnostic (-fpermissive) fires otherwise, // because the name's meaning would differ between enclosing-scope lookup // (outer struct) and complete-class-scope lookup (the alias). - using iterator_category = forward_iterator_tag; - // pair (not pair): matches ska's value_type contract and + using iterator_category = das::forward_iterator_tag; + // das::pair (not das::pair): matches ska's value_type contract and // matches what operator* actually yields (non-const K& inside reference). - using value_type = pair; + using value_type = das::pair; using difference_type = ptrdiff_t; using pointer = daslang_hash_map::reference *; using reference = daslang_hash_map::reference; private: daslang_hash_map * owner_ = nullptr; size_t index_ = 0; - mutable typename std::aligned_storage::type ref_storage_; + mutable typename das::aligned_storage::type ref_storage_; friend class daslang_hash_map; friend class const_iterator; public: @@ -290,15 +274,15 @@ class daslang_hash_map { class const_iterator { public: - using iterator_category = forward_iterator_tag; - using value_type = pair; + using iterator_category = das::forward_iterator_tag; + using value_type = das::pair; using difference_type = ptrdiff_t; using pointer = const daslang_hash_map::const_reference *; using reference = daslang_hash_map::const_reference; private: const daslang_hash_map * owner_ = nullptr; size_t index_ = 0; - mutable typename std::aligned_storage::type ref_storage_; + mutable typename das::aligned_storage::type ref_storage_; friend class daslang_hash_map; public: const_iterator () noexcept = default; @@ -331,7 +315,7 @@ class daslang_hash_map { daslang_hash_map & operator = ( daslang_hash_map && ) noexcept = default; ~ daslang_hash_map () = default; - daslang_hash_map ( std::initializer_list> il ) { + daslang_hash_map ( das::initializer_list> il ) { reserve(il.size()); for ( const auto & kv : il ) insert(kv); } @@ -393,12 +377,12 @@ class daslang_hash_map { V & at ( const K & key ) { const size_t idx = find_index(key, hash_(key)); - if ( idx == SIZE_MAX ) throw std::out_of_range("daslang_hash_map::at"); + if ( idx == SIZE_MAX ) das::das_throw("daslang_hash_map::at"); return val_at(idx); } const V & at ( const K & key ) const { const size_t idx = find_index(key, hash_(key)); - if ( idx == SIZE_MAX ) throw std::out_of_range("daslang_hash_map::at"); + if ( idx == SIZE_MAX ) das::das_throw("daslang_hash_map::at"); return val_at(idx); } @@ -414,12 +398,12 @@ class daslang_hash_map { // modification // - pair insert ( const pair & kv ) { + das::pair insert ( const das::pair & kv ) { auto r = reserve_slot(kv.first, hash_(kv.first)); if ( r.second ) val_at(r.first) = kv.second; return { iterator{this, r.first}, r.second }; } - pair insert ( pair && kv ) { + das::pair insert ( das::pair && kv ) { auto h = hash_(kv.first); auto r = reserve_slot(das::move(kv.first), h); if ( r.second ) val_at(r.first) = das::move(kv.second); @@ -427,19 +411,19 @@ class daslang_hash_map { } template - pair emplace ( Args &&... args ) { - pair kv(das::forward(args)...); + das::pair emplace ( Args &&... args ) { + das::pair kv(das::forward(args)...); return insert(das::move(kv)); } template - pair try_emplace ( const K & key, Args &&... args ) { + das::pair try_emplace ( const K & key, Args &&... args ) { auto r = reserve_slot(key, hash_(key)); if ( r.second ) val_at(r.first) = V(das::forward(args)...); return { iterator{this, r.first}, r.second }; } template - pair try_emplace ( K && key, Args &&... args ) { + das::pair try_emplace ( K && key, Args &&... args ) { auto h = hash_(key); auto r = reserve_slot(das::move(key), h); if ( r.second ) val_at(r.first) = V(das::forward(args)...); @@ -497,7 +481,7 @@ class daslang_hash_map { // If inserted, slot is initialized with hash + key; caller is responsible for // writing the value. NOTE: probe loop mirrors runtime_table.h::reserve. template - __forceinline pair reserve_slot ( KFwd && key, uint64_t raw_hash ) { + __forceinline das::pair reserve_slot ( KFwd && key, uint64_t raw_hash ) { if ( size_ >= hashes_.size() / 2 ) grow(); else if ( (hashes_.size() - size_) / 2 < tombstones_ ) rehash_same_capacity(); uint64_t mask = hashes_.size() - 1; @@ -547,9 +531,9 @@ class daslang_hash_map { void rehash_into ( size_t new_cap ) { // new_cap must be a power of 2. grow()/rehash_same_capacity() uphold that. - vector new_hashes(new_cap, daslang_hash_map_detail::HASH_EMPTY); - vector new_keys(new_cap); - vector new_values(new_cap); + das::vector new_hashes(new_cap, daslang_hash_map_detail::HASH_EMPTY); + das::vector new_keys(new_cap); + das::vector new_values(new_cap); const uint64_t mask = new_cap - 1; for ( size_t i = 0, n = hashes_.size(); i < n; ++i ) { const uint64_t kh = hashes_[i]; @@ -573,10 +557,10 @@ class daslang_hash_map { // daslang_hash_set // -template , class KeyEqual = equal_to> +template , class KeyEqual = das::equal_to> class daslang_hash_set { - vector hashes_; - vector keys_; + das::vector hashes_; + das::vector keys_; size_t size_ = 0; size_t tombstones_ = 0; Hash hash_; @@ -593,7 +577,7 @@ class daslang_hash_set { size_t index_ = 0; friend class daslang_hash_set; public: - using iterator_category = forward_iterator_tag; + using iterator_category = das::forward_iterator_tag; using value_type = K; using difference_type = ptrdiff_t; using pointer = const K *; @@ -628,7 +612,7 @@ class daslang_hash_set { daslang_hash_set & operator = ( daslang_hash_set && ) noexcept = default; ~ daslang_hash_set () = default; - daslang_hash_set ( std::initializer_list il ) { + daslang_hash_set ( das::initializer_list il ) { reserve(il.size()); for ( const auto & k : il ) insert(k); } @@ -668,17 +652,17 @@ class daslang_hash_set { size_type count ( const K & key ) const { return find_index(key, hash_(key)) == SIZE_MAX ? 0 : 1; } bool contains ( const K & key ) const { return find_index(key, hash_(key)) != SIZE_MAX; } - pair insert ( const K & key ) { + das::pair insert ( const K & key ) { auto r = reserve_slot(key, hash_(key)); return { const_iterator{this, r.first}, r.second }; } - pair insert ( K && key ) { + das::pair insert ( K && key ) { auto h = hash_(key); auto r = reserve_slot(das::move(key), h); return { const_iterator{this, r.first}, r.second }; } template - pair emplace ( Args &&... args ) { + das::pair emplace ( Args &&... args ) { K key(das::forward(args)...); return insert(das::move(key)); } @@ -724,7 +708,7 @@ class daslang_hash_set { } template - __forceinline pair reserve_slot ( KFwd && key, uint64_t raw_hash ) { + __forceinline das::pair reserve_slot ( KFwd && key, uint64_t raw_hash ) { if ( size_ >= hashes_.size() / 2 ) grow(); else if ( (hashes_.size() - size_) / 2 < tombstones_ ) rehash_same_capacity(); uint64_t mask = hashes_.size() - 1; @@ -772,8 +756,8 @@ class daslang_hash_set { void rehash_same_capacity () { rehash_into(hashes_.size()); } void rehash_into ( size_t new_cap ) { - vector new_hashes(new_cap, daslang_hash_map_detail::HASH_EMPTY); - vector new_keys(new_cap); + das::vector new_hashes(new_cap, daslang_hash_map_detail::HASH_EMPTY); + das::vector new_keys(new_cap); const uint64_t mask = new_cap - 1; for ( size_t i = 0, n = hashes_.size(); i < n; ++i ) { const uint64_t kh = hashes_[i]; diff --git a/include/misc/include_fmt.h b/include/misc/include_fmt.h index 267aadb12e..ac9b004c1a 100644 --- a/include/misc/include_fmt.h +++ b/include/misc/include_fmt.h @@ -1,10 +1,10 @@ #pragma once -#if (!defined(DAS_ENABLE_EXCEPTIONS)) || (!DAS_ENABLE_EXCEPTIONS) -#define FMT_THROW(x) das::das_throw(((x).what())) namespace das { void das_throw(const char * msg); } +#if (!defined(DAS_ENABLE_EXCEPTIONS)) || (!DAS_ENABLE_EXCEPTIONS) +#define FMT_THROW(x) das::das_throw(((x).what())) #endif #include diff --git a/mouse-data/docs/das-namespace-abstraction-std-vs-eastl-swap.md b/mouse-data/docs/das-namespace-abstraction-std-vs-eastl-swap.md new file mode 100644 index 0000000000..a75e8f8d77 --- /dev/null +++ b/mouse-data/docs/das-namespace-abstraction-std-vs-eastl-swap.md @@ -0,0 +1,50 @@ +--- +slug: das-namespace-abstraction-std-vs-eastl-swap +title: how does daslang das:: namespace abstraction work — what does das::vector resolve to and how does the eastl swap happen +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +`das::vector`, `das::pair`, `das::move`, `das::hash`, `das::equal_to`, etc. are NOT defined as `using std::X` declarations — they're injected wholesale by a single `using namespace std;` or `using namespace eastl;` directive inside `namespace das { ... }`. + +**Default config** (`include/daScript/das_config.h:34`): +```cpp +namespace das {using namespace std;} +``` + +**EASTL config** (`cmake/das_config_eastl/das_config.h:45-56`): +```cpp +namespace das { +using namespace eastl; +using std::atomic; +using std::condition_variable; +using std::lock_guard; +using std::mutex; +using std::nullptr_t; +using std::recursive_mutex; +using std::stringstream; +using std::thread; +using std::unique_lock; +} // namespace das +``` + +EASTL config does `using namespace eastl;` THEN explicit `using std::X` for the handful of things EASTL doesn't have (thread primitives, atomic, stringstream). + +**Activated by** `-DDAS_CONFIG_INCLUDE_DIR=cmake/das_config_eastl` at CMake time. CI workflow: `.github/workflows/build_eastl.yml`. + +**Implications when writing code that ends up in `namespace das`:** + +1. Always write `das::vector` not `std::vector`. The EASTL build will route to `eastl::vector` automatically. Writing `std::vector` actively bypasses the abstraction. + +2. Don't add `using std::vector;` blocks inside `namespace das { ... }` — they'll shadow `eastl::vector` in the EASTL build, defeating the abstraction. The das_hash_map.h cleanup in PR #2716 was exactly this pattern. + +3. Things EASTL doesn't have (`out_of_range`, `exception`, `runtime_error`, `` in general) must use `std::` directly — there's no `eastl::out_of_range`. EASTL's own headers throw `std::out_of_range` (see `eastl/include/EASTL/bitset.h`, `map.h`, `array.h`). + +4. **Type traits, iterators, containers, allocators**: all dual-namespace, use `das::`. + **Exception types, fmt library, threading from std**: use `std::` directly. + +To check what's available where: `grep -rn "das::X\|eastl::X" /Users/borisbatkin/Work/daScript/include/ 2>/dev/null | head` shows the convention in practice. + +## Questions +- how does daslang das:: namespace abstraction work — what does das::vector resolve to and how does the eastl swap happen diff --git a/mouse-data/docs/das-throw-universal-panic-primitive-low-level-headers.md b/mouse-data/docs/das-throw-universal-panic-primitive-low-level-headers.md new file mode 100644 index 0000000000..8b8481e59c --- /dev/null +++ b/mouse-data/docs/das-throw-universal-panic-primitive-low-level-headers.md @@ -0,0 +1,27 @@ +--- +slug: das-throw-universal-panic-primitive-low-level-headers +title: what is the right primitive to fail/panic from a low-level daslang C++ header — don't use raw throw because embedded games disable exceptions +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**Use `das::das_throw(const char* msg)`** — daslang's universal panic primitive. Declared in `include/daScript/das_config.h`, defined in `src/simulate/runtime_string.cpp`. + +**Why NOT raw `throw`** in low-level headers: +- Games embedding daslang frequently disable C++ exceptions entirely (`-fno-exceptions` / EASTL no-exceptions mode). Raw `throw` won't compile. +- `daslang`'s OWN default is `DAS_ENABLE_EXCEPTIONS=0` (see `include/daScript/simulate/simulate.h:27-28`). Only Windows-no-LLVM and WASM set it to 1. +- EASTL has no `eastl::exception` or `eastl::out_of_range`, so even with exceptions enabled, `throw das::exception()` doesn't resolve in the EASTL config. + +**What das_throw does:** +- `DAS_ENABLE_EXCEPTIONS=0` (default): setjmp/longjmp via thread-local `g_throwBuf`. If no try-catch is set up, calls `DAS_FATAL_ERROR("unhanded das_throw, %s\n", msg)`. +- `DAS_ENABLE_EXCEPTIONS=1`: `throw dasException(msg, LineInfo())` so existing `catch(dasException&)` handlers in `src/simulate/simulate_exceptions.cpp` pick it up. + +**Constraint to know**: as of PR #2716, `das_throw` is declared unconditionally in `das_config.h` / `include_fmt.h` / `das_config_eastl/das_config.h`. Before that PR, the declaration was gated on `DAS_ENABLE_EXCEPTIONS=0`, so calling it from headers in WASM/Windows-no-LLVM builds would have failed at link time. + +**FMT_THROW vs das_throw**: `FMT_THROW(x)` (from fmt library or daslang's override) takes an exception OBJECT and is conditionally `throw x` or `das::das_throw(x.what())`. das_throw takes a `const char*` directly — use this in low-level code where you don't have an exception object to construct. + +**Pre-PR pattern that broke**: das_hash_map.h had `throw std::out_of_range("daslang_hash_map::at")` in `at()`. This compiled on macOS libc++ (transitive ``) but failed on Linux libstdc++ (no transitive) and EASTL (no `eastl::out_of_range`). The fix in PR #2716: replace with `das::das_throw("daslang_hash_map::at")`. + +## Questions +- what is the right primitive to fail/panic from a low-level daslang C++ header — don't use raw throw because embedded games disable exceptions diff --git a/mouse-data/docs/dasimgui-drawlist-prim-macro-path-key-only-no-state-global-render-only-rail.md b/mouse-data/docs/dasimgui-drawlist-prim-macro-path-key-only-no-state-global-render-only-rail.md new file mode 100644 index 0000000000..2e51058dd5 --- /dev/null +++ b/mouse-data/docs/dasimgui-drawlist-prim-macro-path-key-only-no-state-global-render-only-rail.md @@ -0,0 +1,82 @@ +--- +slug: dasimgui-drawlist-prim-macro-path-key-only-no-state-global-render-only-rail +title: How do I design a dasImgui macro annotation that emits path-key telemetry per call site but no state-global, for render-only operations? +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +For render-only call sites (drawlist primitives, frame markers, etc.) where every invocation is purely transient — no hover, no active, no edit state worth preserving across `@live` reloads — emit a stable `::` path-key per call site for mouse-cards / playwright targeting BUT skip the `__widget__` state-global emission that `[widget]` does. + +The pattern is a sibling to `[widget]` / `[container]`, sharing the same surface area in `widgets/imgui_boost.das`. Mirror this template (`[drawlist_prim]` is the reference implementation from dasImgui PR #48): + +```das +// widgets/imgui_boost.das — same module as [widget] / [container] annotations. +// Annotation MUST live in a different module from the call sites — defining +// the annotation class and immediately using [drawlist_prim] in the same file +// errors out because the parser hasn't completed annotation registration +// when the sibling annotation gets resolved. + +class DrawlistPrimCallMacro : AstCallMacro { + kind_name : string + + def override visit(prog : Program?; mod : Module?; var expr : smart_ptr) : ExpressionPtr { + let modName = mod == null ? "anon" : string(mod.name) + let path_key = "{modName}:{expr.at.line:d}:{expr.at.column:d}" + var rewritten = new ExprCall(at = expr.at, name := kind_name) + var pathArg = new ExprConstString(at = expr.at, value := path_key) + rewritten.arguments |> push(pathArg) + for (a in expr.arguments) { + rewritten.arguments |> push(clone_expression(a)) + } + return <- rewritten + } +} + +[function_macro(name = "drawlist_prim")] +class DrawlistPrimFunctionMacro : AstFunctionAnnotation { + def override transform(var func : FunctionPtr; var args : AnnotationArgumentList) : ExpressionPtr { + let kind = string(find_arg(args, "kind")) + // Inject a leading `widget_ident : string` parameter so the rewritten + // call carries the path key into the runtime register helper. + var widthIdentVar = new Variable(at = func.at, + name := "widget_ident", + _type <- new TypeDecl(baseType = Type.tString, at = func.at)) + widthIdentVar.flags.generated = true + func.arguments |> emplace(widthIdentVar, 0) + // Register the call-site rewriter under the function's own name — + // call sites write `add_rect(dl, ...)` and the macro slots in + // the path-key argument transparently. + var inst = new DrawlistPrimCallMacro(kind_name = kind) + compiling_module() |> add_call_macro(make_call_macro(kind, inst)) + return [[ExpressionPtr]] + } +} +``` + +Then at the impl side (`widgets/imgui_drawlist_builtin.das` or similar): + +```das +[drawlist_prim(kind = "add_rect")] +def add_rect(widget_ident : string; var dl : ImDrawList?; p_min, p_max : float2; col : uint; rounding : float; flags : ImDrawFlags; thickness : float) { + drawlist_register(widget_ident, "add_rect", make_bbox(p_min, p_max)) + *dl |> AddRect(p_min, p_max, col, rounding, flags, thickness) +} +``` + +Where `drawlist_register(widget_ident, kind, bbox)` writes a lightweight `WidgetEntry` (kind + bbox, no hover/active/focus fields) into `g_registry`. No `__widget__` global declared — multiple primitives at the same source coordinates never collide because nothing's stored. + +**Key correctness points**: + +1. **Annotation lives in a different file from the call sites**. Same-file definition + use fails — parser order issue. Put `[function_macro(name="...")] class` in `imgui_boost.das` next to `[widget]`/`[container]`; primitives `require imgui/imgui_boost_v2 public` then tag themselves with `[drawlist_prim(kind="add_rect")]`. + +2. **Path-key shape is `::`** — leaf format that `widget_path_key(widget_ident)` recognises and slots under the current container (if any). Tests should assert leaves match a `:\d+:\d+$` regex. + +3. **No state global**. `[widget]` declares `__widget__` to persist hover/click state across `@live`. Drawlist primitives skip that step — they're stateless, so collisions are impossible-by-design, NOT impossible-by-accident. + +4. **Container nesting is lexical**, NOT visual. `with_window_drawlist` painted INSIDE `window(W, ...)` produces nested path keys (`W/mod:line:col`); same call OUTSIDE the window block produces unnested keys. Visual drawlist scope (`with_foreground_drawlist` paints to the viewport drawlist regardless of where called) is independent of telemetry nesting. + +Verified by `tests/integration/test_drawlist_path_key.das` and `test_with_window_drawlist.das` in dasImgui PR #48. + +## Questions +- How do I design a dasImgui macro annotation that emits path-key telemetry per call site but no state-global, for render-only operations? diff --git a/mouse-data/docs/dasimgui-hover-target-widget-variant-pattern-color-button-hover-selectable-hover.md b/mouse-data/docs/dasimgui-hover-target-widget-variant-pattern-color-button-hover-selectable-hover.md new file mode 100644 index 0000000000..413e882a55 --- /dev/null +++ b/mouse-data/docs/dasimgui-hover-target-widget-variant-pattern-color-button-hover-selectable-hover.md @@ -0,0 +1,37 @@ +--- +slug: dasimgui-hover-target-widget-variant-pattern-color-button-hover-selectable-hover +title: dasImgui hover-target widget variant pattern (color_button_hover / selectable_hover) +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**Hover-target [widget] variants** mirror their click/toggle cousins but use `EmptyMarkerState` and return a per-frame `bool hovered` from `IsItemHovered()`. Use when the widget is a *surface* for a hover-driven side-effect (cursor swap, capture override, status preview), not a *trigger*. + +Pattern shape (PR-A2 introduced two: `color_button_hover` next to `color_button`, `selectable_hover` next to `selectable`): + +```das +[widget] +def color_button_hover(var state : EmptyMarkerState; desc_id : string; + col : float4; + size : float2 = float2(0.0f, 0.0f); + flags : ImGuiColorEditFlags = ImGuiColorEditFlags.None) : bool { + ColorButton(desc_id, col, flags, size) + let hovered = IsItemHovered() + marker_finalize(widget_ident, "color_button_hover", state) + return hovered +} +``` + +Key choices: +- **`EmptyMarkerState`** (the same shape backing `bullet` / `same_line`) — there's no per-frame state worth persisting (hover is transient; ImGui re-queries each frame). Caller branches on the returned bool synchronously. +- **`marker_finalize`** — same finalizer the empty-marker widgets use. No L2/L3 dispatcher actions; the path-key is what playwright targets. +- **Distinct name** (`_hover` suffix) rather than overloading the existing widget — kind: in the snapshot is the disambiguator, and tests can `find_widget(snap, path)?["kind"] == "color_button_hover"`. Overloading would conflate them in telemetry. +- **Caller pattern**: `let hovered = widget_hover(IDENT, (...))`; if (hovered) { side-effect }. The regular widget's state holds click/toggle bookkeeping; the hover variant has neither. + +When a site needs BOTH click and hover, use the click variant + `IsItemHovered()` after the call — the post-item hover query is ALLOWED in `imgui_lint.das`. No need for a third variant. + +See dasImgui PR #47 (PR-A2 of imgui_demo port) for the introduction. + +## Questions +- dasImgui hover-target widget variant pattern (color_button_hover / selectable_hover) diff --git a/mouse-data/docs/dasimgui-imguisizeconstraints-lambda-needs-module-scope-lifetime-not-stack-local.md b/mouse-data/docs/dasimgui-imguisizeconstraints-lambda-needs-module-scope-lifetime-not-stack-local.md new file mode 100644 index 0000000000..8710216e70 --- /dev/null +++ b/mouse-data/docs/dasimgui-imguisizeconstraints-lambda-needs-module-scope-lifetime-not-stack-local.md @@ -0,0 +1,46 @@ +--- +slug: dasimgui-imguisizeconstraints-lambda-needs-module-scope-lifetime-not-stack-local +title: Why does `SetNextWindowSizeConstraints(min, max, ImGuiSizeConstraints(@(var d) { ... }))` crash with an access violation inside `CalcWindowNextAutoFitSize` on the next frame? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +**Because ImGui stores the callback pointer + user_data on `g.NextWindowData.SizeCallback` / `.SizeCallbackUserData` and consumes them DEFERRED — at the next `Begin()` AND on later `CalcWindowNextAutoFitSize` passes — but your daslang `ImGuiSizeConstraints` struct (which holds the `lambda<...>` the C++ trampoline invokes) was a stack local in the function that called `SetNextWindowSizeConstraints`.** That local dies when the function returns. The lambda's captured `Context*` + closure body is GC'd before ImGui dereferences `SizeCallbackUserData`, so `_builtin_SetNextWindowSizeConstraints`'s trampoline reads freed memory → AV in the auto-fit pass. + +**The fix:** make the `ImGuiSizeConstraints` holder live at least as long as the window. Module-scope `var private` + `[init]` seeding is the canonical pattern: + +```das +// app_small.das — ShowExampleAppConstrainedResize +var private SMALL_CONSTR_SQUARE_CN : ImGuiSizeConstraints +var private SMALL_CONSTR_ASPECT_CN : ImGuiSizeConstraints +var private SMALL_CONSTR_STEP_CN : ImGuiSizeConstraints + +[init] +def small_constr_init() { + SMALL_CONSTR_SQUARE_CN <- ImGuiSizeConstraints(@(var d : ImGuiSizeCallbackData) { + let m = max(d.DesiredSize.x, d.DesiredSize.y); d.DesiredSize = float2(m, m) + }) + SMALL_CONSTR_ASPECT_CN <- ImGuiSizeConstraints(@(var d : ImGuiSizeCallbackData) { + d.DesiredSize.y = d.DesiredSize.x / (16.0f / 9.0f) + }) + SMALL_CONSTR_STEP_CN <- ImGuiSizeConstraints(@(var d : ImGuiSizeCallbackData) { + let s = 100.0f + d.DesiredSize = floor(d.DesiredSize / float2(s, s)) * float2(s, s) + }) +} + +// In ShowExampleAppConstrainedResize, per case: +SetNextWindowSizeConstraints(float2(0,0), float2(FLT_MAX, FLT_MAX), SMALL_CONSTR_SQUARE_CN) +``` + +**Why this is non-obvious from the daslang API surface:** `ImGuiSizeConstraints(...)` looks like a value constructor that returns by value, and `SetNextWindowSizeConstraints(min, max, var cn : ImGuiSizeConstraints)` takes `var cn` so it looks borrow-style. But the C++ `_builtin_SetNextWindowSizeConstraints` at `src/dasIMGUI.main.cpp:361-384` reinterprets the daslang struct's bytes (Context*, Lambda, LineInfo*) and stashes them in ImGui's `SizeCallback`/`SizeCallbackUserData` slots — pointer escape ImGui consumes later in the frame and on subsequent auto-fit passes. No daslang side-channel reflects this lifetime extension. + +The 2-arg no-callback form `SetNextWindowSizeConstraints(min, max)` (in `ALLOWED_IMGUI`) doesn't have this trap — no callback to keep alive. + +**Tutorial parity:** the `examples/tutorial/window_size_constraints.das` example uses the same module-scope `SQUARE_CN` / `ASPECT_CN` / `STEP_CN` pattern. If you copy a `SetNextWindowSizeConstraints` lambda call from your own code into a function body, you've reintroduced the bug. + +## Questions +- Why does `SetNextWindowSizeConstraints(min, max, ImGuiSizeConstraints(@(var d) { ... }))` crash with an access violation inside `CalcWindowNextAutoFitSize` on the next frame? +- How do I correctly extend the lifetime of an `ImGuiSizeConstraints` callback lambda in dasImgui so ImGui's deferred SizeCallback consumer doesn't dereference freed memory? +- Can `ImGuiSizeConstraints` be a function-local in dasImgui, or does it need module-scope lifetime? diff --git a/mouse-data/docs/dasimgui-integration-tests-use-examples-features-recordings-use-examples-tutorial.md b/mouse-data/docs/dasimgui-integration-tests-use-examples-features-recordings-use-examples-tutorial.md new file mode 100644 index 0000000000..8f4766f974 --- /dev/null +++ b/mouse-data/docs/dasimgui-integration-tests-use-examples-features-recordings-use-examples-tutorial.md @@ -0,0 +1,27 @@ +--- +slug: dasimgui-integration-tests-use-examples-features-recordings-use-examples-tutorial +title: dasImgui integration tests use examples/features/, recordings use examples/tutorial/ +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +For a new dasImgui component, the **full triad** needs FOUR files, not three, because tests and recordings need different lifecycles: + +| File | Lifecycle | Driven by | +|---|---|---| +| `examples/features/.das` | `harness_init` / `harness_begin_frame` / `harness_new_frame` (require `imgui/imgui_harness`) | Integration tests via `with_imgui_app(feature_path)` | +| `examples/tutorial/.das` | `live_create_window` / `live_imgui_init` / `live_begin_frame` (require `imgui_live` + GLFW + GL stack) | Recording drivers via `with_recording_app(tutorial_path, apng, secs)` | +| `tests/integration/test_.das` | n/a | dastest; points at the `features/` file | +| `tests/integration/record_.das` | n/a | record entry-point; points at the `tutorial/` file | + +Plus the RST tutorial page (`doc/source/tutorials/.rst`) doing `literalinclude` of the `tutorial/` file, and the APNG dropped at `doc/source/_static/tutorials/.apng`. + +**Don't** point an integration test at a `tutorial/` file. `with_imgui_app` spawns plain `daslang-live` which doesn't dispatch the `live_create_window` lifecycle — the host window never opens, `wait_for_widget("ROOT_WIN", 15s)` times out, and you get a CI failure that looks like "the widget never registers" but is actually "the app's `update()` never runs." + +CI failure shape for the wrong-lifecycle case: `wait_for_widget` returns null after 15s with the failure message " registers (demo renders)" — no compile error, no panic, just silent timeout. + +Bit me in dasImgui PR #47 (PR-A2 inputs.das sweep): I skipped `features/` and pointed tests at `tutorial/` directly. macOS CI surfaced 3 timeouts; fixup commit `2665617` added the feature files. Pattern confirmed against existing `test_with_indent.das` ← `features/with_indent.das` and similar. + +## Questions +- dasImgui integration tests use examples/features/, recordings use examples/tutorial/ diff --git a/mouse-data/docs/dasimgui-sphinx-w-cross-ref-add-labels-to-external-types-rst-when-wrappers-reference-new-imgui-types.md b/mouse-data/docs/dasimgui-sphinx-w-cross-ref-add-labels-to-external-types-rst-when-wrappers-reference-new-imgui-types.md new file mode 100644 index 0000000000..8f2d533c92 --- /dev/null +++ b/mouse-data/docs/dasimgui-sphinx-w-cross-ref-add-labels-to-external-types-rst-when-wrappers-reference-new-imgui-types.md @@ -0,0 +1,46 @@ +--- +slug: dasimgui-sphinx-w-cross-ref-add-labels-to-external-types-rst-when-wrappers-reference-new-imgui-types +title: When I add a new dasImgui boost-surface wrapper that references an ImGui type (e.g. `ImGuiTableFlags`, `ImGuiCond`, `ImGuiViewport`), why does CI sphinx-W fail with "undefined label" warnings even though the wrapper compiles fine? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +**Because `imgui2rst.das` (the dasImgui doc generator) emits `:ref:\`enum-imgui-\`` / `:ref:\`handle-imgui-\`` cross-refs in every generated `stdlib/generated/.rst` whenever a wrapper's signature mentions an imgui-owned type — but those labels are NOT in any generated file (they live in `doc/source/stdlib/external_types.rst`, which is hand-maintained).** New wrapper signature → new cross-ref → if the matching `.. _enum-imgui-:` / `.. _handle-imgui-:` anchor doesn't exist in `external_types.rst`, sphinx-W explodes the CI build with `undefined label: enum-imgui-imguitableflags` etc. + +**Surface:** local `daspkg build` won't catch it — only sphinx-W (`sphinx-build -W -b html doc/source doc/build/html`) does, which runs in the docs CI job. AND local sphinx-W might pass anyway if you don't first invoke `imgui2rst.das` to regenerate `stdlib/generated/`; CI does this in-pipeline. + +**The discipline:** any time you add a wrapper module / signature in `widgets/` that mentions a previously-unreferenced ImGui type, add the cross-ref anchor to `doc/source/stdlib/external_types.rst` in the SAME PR. The label format is lowercased-by-sphinx but you write the proper case: + +```rst +.. _enum-imgui-ImGuiTableFlags: +.. _enum-imgui-ImGuiTableColumnFlags: +.. _enum-imgui-ImGuiTableRowFlags: + +``imgui::ImGui*Flags`` +====================== +Bitfield enums exposed by the ``imgui`` builtin module: ... list ... + +.. _enum-imgui-ImGuiCond: + +``imgui::ImGuiCond`` +==================== +[description] + +.. _handle-imgui-ImGuiViewport: + +``imgui::ImGuiViewport`` +======================== +[description] +``` + +Multiple anchor lines stacked above ONE heading is the working pattern (sphinx points all of them at the same section). Used for `ImGui*Flags` (one heading, ~9 stacked labels) and `ImGuiViewport` / `ImGuiSizeCallbackData` (one heading each). + +**Concrete instance (2026-05-17, PR #45):** `widgets/imgui_table_builtin.das` introduced `data_table(..., flags : ImGuiTableFlags, ...)` + 6 pass-throughs mentioning `ImGuiTableColumnFlags` / `ImGuiTableRowFlags`; `widgets/imgui_window_constraints_builtin.das` introduced `ImGuiSizeCallbackData` in the lambda signature; `widgets/imgui_containers_builtin.das` added `set_window_size(cond : ImGuiCond)` + `viewport_center(self : ImGuiViewport)`. Build went green locally, sphinx-W in CI rejected the PR until 6 new labels (3 table flags + ImGuiCond + ImGuiViewport + ImGuiSizeCallbackData) landed in `external_types.rst`. + +**Companion:** `feedback_dasimgui_sphinx_w_apng_must_exist.md` is the cousin for static assets (`.. image:: _static/tutorials/.apng` must exist at CI time). Both fail in the same job; check both when sphinx-W errors land on a wrapper-addition PR. + +## Questions +- When I add a new dasImgui boost-surface wrapper that references an ImGui type (e.g. `ImGuiTableFlags`, `ImGuiCond`, `ImGuiViewport`), why does CI sphinx-W fail with "undefined label" warnings even though the wrapper compiles fine? +- Where do I add the `enum-imgui-` / `handle-imgui-` Sphinx anchors that `imgui2rst.das` emits cross-refs to? +- Why does my local sphinx-W pass on a dasImgui doc build but CI fails on the same commit? diff --git a/mouse-data/docs/daslang-deduce-project-root-auto-fallback-from-script-dir-affects-test-design.md b/mouse-data/docs/daslang-deduce-project-root-auto-fallback-from-script-dir-affects-test-design.md new file mode 100644 index 0000000000..5b3288d6ad --- /dev/null +++ b/mouse-data/docs/daslang-deduce-project-root-auto-fallback-from-script-dir-affects-test-design.md @@ -0,0 +1,51 @@ +--- +slug: daslang-deduce-project-root-auto-fallback-from-script-dir-affects-test-design +title: Why does daslang.exe auto-resolve daspkg modules without an explicit `-project_root` when the script lives inside the project root? +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**`daslang.exe` auto-deduces `-project_root` from `files.front()`'s directory** when the flag isn't passed explicitly. See `utils/daScript/main.cpp:379` `deduce_project_root()`: + +```cpp +static string deduce_project_root(string maybe_project_root, string compile_file) { + if (!maybe_project_root.empty()) return maybe_project_root; // explicit wins + if (!projectFile.empty()) { /* try .das_project's getDynModulesFolder() */ } + if (!compile_file.empty()) { // fallback: script's dir + auto filename_start = compile_file.find_last_of("\\/"); + return filename_start != string::npos + ? compile_file.substr(0, filename_start) + : "./"; + } + return ""; +} +``` + +Used at `main.cpp:738`: `project_root = deduce_project_root(project_root, files.front())` → fed into `require_dynamic_modules(access, getDasRoot(), project_root, tout)` → which scans `/modules/*/.das_module`. + +**Implication for MCP project_root testing**: which file is `files.front()`? + +| Spawn path | files.front() | Auto-deduces project_root from | +|---|---|---| +| `daslang.exe consumer.das` (direct) | `consumer.das` | consumer's own dir | +| `daslang.exe subtool.das -- consumer.das` (MCP subprocess) | `subtool.das` | `utils/mcp/subtools/` (no `modules/` there) | +| `do_run_script` (spawns `daslang.exe consumer.das` directly) | `consumer.das` | consumer's own dir | + +So for an MCP test fixture at `utils/mcp/tests/_pretend_root/consumer.das`: +- MCP subtool tests (`compile_check`, `lint`, etc.) → spawn `daslang.exe subtool.das -- consumer.das` → auto-deduce uses `utils/mcp/subtools/` → no `modules/` → `pretend_mod` unregistered → compile fails without explicit `-project_root`. ✓ TESTABLE. +- `do_run_script(consumer.das, ...)` → spawns `daslang.exe ... consumer.das` directly → auto-deduce uses `_pretend_root/` → finds `modules/pretend_mod/` → resolves WITHOUT explicit project_root. ✗ FALSE POSITIVE. + +**Workaround for testing `do_run_script`**: use inline code mode (`do_run_script("", code_string, ...)`) — the temp stub is written to `get_das_root()` whose `modules/` tree is daslang's own (no pretend_mod). Then the negative case fails as expected. + +Same gotcha for `do_eval_expression` — its temp stub also lands in `get_das_root()`, so the auto-deduce doesn't help and explicit project_root is required. + +Surfaced 2026-05-18 during Tier 1 MCP project_root test design. Affects ANY test fixture that places the consumer file inside the project_root subtree. + +## Questions +- Why does daslang.exe auto-resolve daspkg modules without an explicit `-project_root` when the script lives inside the project root? +- How does `deduce_project_root` interact with MCP test fixtures? +- Why does `do_run_script` succeed against my pretend-daspkg fixture without project_root, but `do_compile_check` fails? + +## Questions +- Why does daslang.exe auto-resolve daspkg modules without an explicit `-project_root` when the script lives inside the project root? diff --git a/mouse-data/docs/daslang-eastl-config-local-build-setup.md b/mouse-data/docs/daslang-eastl-config-local-build-setup.md new file mode 100644 index 0000000000..795ea941df --- /dev/null +++ b/mouse-data/docs/daslang-eastl-config-local-build-setup.md @@ -0,0 +1,40 @@ +--- +slug: daslang-eastl-config-local-build-setup +title: how do I locally build and verify the daslang EASTL CI configuration — clone setup, cmake invocation, what to test +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +CI workflow: `.github/workflows/build_eastl.yml`. Mirrors it for local repro: + +```bash +# Clone EASTL and EABase separately — EASTL repo no longer has EABase as a submodule +# (the workflow's `--recurse-submodules` is now a no-op; EABase is a separate repo) +git clone --depth 1 https://github.com/electronicarts/EASTL.git eastl +git clone --depth 1 https://github.com/electronicarts/EABase.git eabase + +cmake -B build_eastl -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DDAS_LLVM_DISABLED=ON -DDAS_GLFW_DISABLED=ON \ + -DDAS_TESTS_DISABLED=ON -DDAS_TUTORIAL_DISABLED=ON \ + -DDAS_AOT_EXAMPLES_DISABLED=ON \ + -DDAS_CONFIG_INCLUDE_DIR=cmake/das_config_eastl \ + -DCMAKE_CXX_FLAGS="-isystem $PWD/eastl/include -isystem $PWD/eabase/include/Common" + +cmake --build build_eastl --target daslang --parallel +./bin/daslang --version # smoke test — should print version like "0.6.2" +``` + +**The CI build only verifies the `daslang` target compiles** (`-DDAS_TESTS_DISABLED=ON`). Full test execution is not done; the goal is just compile-coverage to catch type-conversion bugs (e.g. eastl::string → std::filesystem::path). + +**Gitignore**: add `eastl/`, `eabase/`, `build_eastl/` to `.gitignore` so the local checkouts don't show up as untracked. PR #2716 did this. + +**Common errors and meanings:** +- `error: no member named 'X' in namespace 'das'` — your code uses `das::X` but X isn't visible via `using namespace eastl;` AND isn't explicitly `using std::X` in the EASTL config. Fix: either ensure it's in eastl, add an explicit `using std::X` to the EASTL config, or use a different primitive (e.g. `das_throw` instead of `throw das::exception`). +- Linker error on `das_throw` — declaration is gated on `DAS_ENABLE_EXCEPTIONS=0`; if the build sets `=1`, declaration disappears. As of PR #2716 the declaration is unconditional. Pre-PR was a known foot-gun. + +**Note**: das_config_eastl includes ``, `vector.h`, `string.h`, `type_traits.h`, etc. — but NOT `` (which doesn't exist anyway — EASTL throws `std::out_of_range` directly from `` pulled transitively). + +## Questions +- how do I locally build and verify the daslang EASTL CI configuration — clone setup, cmake invocation, what to test diff --git a/mouse-data/docs/daslang-live-cwd-flag-chdir-before-require-dynamic-modules-relative-paths-break.md b/mouse-data/docs/daslang-live-cwd-flag-chdir-before-require-dynamic-modules-relative-paths-break.md new file mode 100644 index 0000000000..796598c2be --- /dev/null +++ b/mouse-data/docs/daslang-live-cwd-flag-chdir-before-require-dynamic-modules-relative-paths-break.md @@ -0,0 +1,37 @@ +--- +slug: daslang-live-cwd-flag-chdir-before-require-dynamic-modules-relative-paths-break +title: Why does daslang-live with relative `-project_root` fail to resolve daspkg modules? +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**daslang-live's `-cwd` flag changes the working directory to the script's folder BEFORE `require_dynamic_modules` runs**, so any relative `-project_root

` passed alongside it gets re-resolved against the *new* cwd and fails to find `

/modules/`. + +Code path (utils/daslang-live/main.cpp): +- Line 696-707: `-cwd` handler calls `SetCurrentDirectoryA(dir)` / `chdir(dir)` where `dir = dirname(scriptFile)`. +- Line 725-730: AFTER chdir, `require_dynamic_modules(access, getDasRoot(), project_root, tout)` runs. If `project_root` was relative, it now points at `/` — which is almost never what the caller meant. + +Concrete failure mode: spawning daslang-live with `-cwd -project_root utils/mcp/tests/_pretend_root /consumer.das` chdir's to `utils/mcp/tests/_pretend_root/`, then tries to scan `utils/mcp/tests/_pretend_root/utils/mcp/tests/_pretend_root/modules/` (the relative path doubled). Result: `error[20605]: missing prerequisite 'pretend_mod/greet'; file not found` even though the registration would succeed with the absolute path. + +**Fix on the caller side**: resolve all paths to absolute BEFORE composing daslang-live's argv. In `utils/mcp/tools/live.das` `do_live_launch`: + +```das +let abs_file = resolve_path(file) +let abs_project = empty(project) ? "" : resolve_path(project) +let abs_project_root = empty(project_root) ? "" : resolve_path(project_root) +let script_args = build_live_script_args(abs_file, abs_project, abs_project_root, is_windows) +``` + +`resolve_path()` lives in `utils/mcp/tools/common.das` and joins relative paths to `get_das_root()`. + +Surfaced 2026-05-18 during MCP test_live_tools.das implementation; the bug was latent until anyone actually fed `do_live_launch` a relative project_root (real-world callers had been passing absolute paths). Fix shipped in PR for the same project_root work. + +**Same gotcha applies to plain `daslang.exe`?** No — daslang.exe doesn't have a `-cwd` flag, so the cwd stays wherever the caller spawned from and relative `-project_root` resolves naturally. + +## Questions +- Why does daslang-live with relative `-project_root` fail to resolve daspkg modules? +- What happens when daslang-live `-cwd` and `-project_root` are both set with relative paths? + +## Questions +- Why does daslang-live with relative `-project_root` fail to resolve daspkg modules? diff --git a/mouse-data/docs/daslang-live-no-port-flag-single-instance-lock-e2e-spawn-tests-infeasible.md b/mouse-data/docs/daslang-live-no-port-flag-single-instance-lock-e2e-spawn-tests-infeasible.md new file mode 100644 index 0000000000..973652fcc1 --- /dev/null +++ b/mouse-data/docs/daslang-live-no-port-flag-single-instance-lock-e2e-spawn-tests-infeasible.md @@ -0,0 +1,32 @@ +--- +slug: daslang-live-no-port-flag-single-instance-lock-e2e-spawn-tests-infeasible +title: Why is reliable in-suite e2e testing of daslang-live spawn infeasible? +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**daslang-live has no `-port` CLI flag** — the HTTP server port is hardcoded to 9090 in the binary. The MCP `port` argument (in `do_live_launch`, `do_live_status`, etc.) only changes which port the MCP *polls*, not what daslang-live *binds*. So a test using an isolated high port like 19090: + +1. Calls `do_live_launch(file, ..., port="19090")`. The launcher spawns daslang-live which binds 9090. +2. `do_live_launch`'s post-spawn poll loop hits `live_is_running("19090")` → returns false (nothing on 19090). +3. After 10s the loop returns `"daslang-live launched but did not respond on port {p} within 10 seconds"` — the test sees an error, but daslang-live is alive on 9090. +4. Test's cleanup `do_live_shutdown("19090")` no-ops. The orphan process needs manual `taskkill`. + +Plus **`acquire_single_instance()` lock** (`utils/daslang-live/main.cpp:709`) — only ONE daslang-live can run per host. A stale instance from a previous failed test blocks the next spawn entirely. The lock is process-wide, not port-scoped. + +Combined: parallel test isolation requires either (a) a `-port ` flag on daslang-live or (b) a `--dry-run-launch` mode in `do_live_launch` that returns the would-be argv without spawning. Without either, the practical path is: + +- **Pin args-builder logic at unit level** — extract `build_live_script_args(file, project, project_root, is_windows)` and test the string output directly. No spawn, no port. See `utils/mcp/test_tools.das` Tier 4a tests. +- **Pin the dispatcher side at MCP level** — Tier 1 tests against the daspkg-style fixture exercise `do_compile_check / do_lint / do_list_requires / ...` end-to-end through the subprocess argv path, proving `-project_root` flows correctly. Doesn't touch daslang-live but covers the same code. +- **Defer real spawn tests** until an upstream daslang-live change adds port-iso or dry-run mode. + +Surfaced 2026-05-18 during MCP `test_live_tools.das` implementation. Tier 4b deferred to follow-up; documented in PR plan + `skills/external_module_debugging.md`. + +## Questions +- Why is reliable in-suite e2e testing of daslang-live spawn infeasible? +- Why does daslang-live ignore the `port` argument I passed to `do_live_launch`? +- Can I run two daslang-live instances in parallel for test isolation? + +## Questions +- Why is reliable in-suite e2e testing of daslang-live spawn infeasible? diff --git a/mouse-data/docs/daspkg-standalone-marker-skips-in-tree-daslang-cmake-include-for-installed-modules.md b/mouse-data/docs/daspkg-standalone-marker-skips-in-tree-daslang-cmake-include-for-installed-modules.md new file mode 100644 index 0000000000..b3695af292 --- /dev/null +++ b/mouse-data/docs/daspkg-standalone-marker-skips-in-tree-daslang-cmake-include-for-installed-modules.md @@ -0,0 +1,31 @@ +--- +slug: daspkg-standalone-marker-skips-in-tree-daslang-cmake-include-for-installed-modules +title: daslang in-tree build fails because a daspkg-installed module's CMakeLists.txt clobbers PROJECT_VERSION — what's the opt-out? +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +When a `daspkg install --global` lands a module under `modules//` and the daslang in-tree build then errors out (typically `No VERSION specified for WRITE_BASIC_CONFIG_VERSION_FILE()` from `CMakeLists.txt:1476` writing `${PROJECT_VERSION}`), the cause is daslang's module sweep INCLUDEing the installed module's CMakeLists.txt and that CMakeLists.txt calling its own `project()` without a VERSION argument — clobbering the parent's `project(DAS VERSION 0.6.2)` PROJECT_VERSION to empty. + +Look at daslang `CMakeLists.txt` lines 476-481: + +```cmake +FOREACH(_module ${_modules}) + # Skip daspkg-installed modules that build themselves (standalone CMake) + IF(NOT EXISTS "${PROJECT_SOURCE_DIR}/modules/${_module}/.daspkg_standalone") + INCLUDE(modules/${_module}/CMakeLists.txt OPTIONAL) + ENDIF() +ENDFOREACH() +``` + +**Fix**: drop a `.daspkg_standalone` marker file at `modules//.daspkg_standalone` (any content; presence is the gate). The in-tree build then skips the include. The package builds standalone via its own CMake invocation (`cmake -B _build -S . -DDASLANG_DIR=`). + +This is meant for packages like dasImgui that have their own full CMakeLists with `project( CXX C)`, ImGui sources, etc. — they're not designed to coexist in daslang's project scope. + +**Pitfall**: if you'd installed the package into `daslang/modules//` as a real copy (NOT a junction/symlink) the marker file lives inside that copy. dasImgui's intended dev model has NO `modules/dasImgui` under daslang at all (see `dasimgui-dev-location-d-daspkg-not-modules` if it exists, or memory `feedback_dasimgui_dev_location`). + +Verified 2026-05-18 against daslang ced9f5175 + dasImgui 14d85b0. + +## Questions +- daslang in-tree build fails because a daspkg-installed module's CMakeLists.txt clobbers PROJECT_VERSION — what's the opt-out? diff --git a/mouse-data/docs/github-actions-pr-merge-ref-sphinx-duplicate-target-local-passes-ci-fails.md b/mouse-data/docs/github-actions-pr-merge-ref-sphinx-duplicate-target-local-passes-ci-fails.md new file mode 100644 index 0000000000..076863d07d --- /dev/null +++ b/mouse-data/docs/github-actions-pr-merge-ref-sphinx-duplicate-target-local-passes-ci-fails.md @@ -0,0 +1,32 @@ +--- +slug: github-actions-pr-merge-ref-sphinx-duplicate-target-local-passes-ci-fails +title: CI sphinx-build fails with "Duplicate explicit target name" but local + WSL repro builds clean — what's going on? +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +When local `sphinx-build -W` AND a WSL-Ubuntu-24.04 mirror of the CI pipeline both succeed, but GitHub Actions CI fails with `Duplicate explicit target name: "alias-X"` at lines you didn't write — the cause is almost always **GitHub Actions checking out the PR merge ref, not the branch HEAD**. + +`actions/checkout@v4` on pull-request events checks out a SYNTHESIZED MERGE COMMIT (PR-HEAD merged into base). Real `git merge origin/master` runs server-side. If master and the PR independently added the SAME RST explicit target (`.. _alias-foo:`) in DIFFERENT line ranges of the same file, git's recursive auto-merge cleanly combines both without conflict markers (because the edit hunks don't overlap). Sphinx then sees two definitions for the same label and bails with "Duplicate explicit target name". + +**Diagnostic** — add a debug step to the docs workflow that dumps the file right before sphinx-build: + +```yaml +- name: "DEBUG: dump file + check duplicate sources" + run: | + nl -ba doc/source/stdlib/external_types.rst | sed -n '50,130p' + grep -rn '_alias-imvec\|_alias-ImVec' doc/source/ + sha256sum doc/source/stdlib/external_types.rst +``` + +The grep on CI will show duplicate anchor lines (e.g. lines 61+62 AND 125+126). Locally the same grep returns only one set. + +**Fix**: `git fetch origin master && git merge origin/master` on the PR branch, then reconcile the duplicated content manually. Push. CI then sees the resolved version. + +**Catch this earlier**: a clean WSL repro that mirrors CI's `actions/checkout` would also need to merge master before building. Most WSL repros clone the branch and skip the merge step, which is why they silently miss this class of bug. If you want a faithful CI repro, do `git checkout pr-branch && git merge origin/master` in WSL too. + +Encountered 2026-05-18 on dasImgui PR #48 (drawlist-rail). PR #49 had landed on master 4h earlier adding `_alias-imvec2`/`_alias-imvec4` anchors to the same file my PR was anchoring `_alias-imcolor`. CI saw both blocks; local saw only mine. Resolved by merging master + folding `_alias-imcolor` into the existing block. + +## Questions +- CI sphinx-build fails with "Duplicate explicit target name" but local + WSL repro builds clean — what's going on? diff --git a/mouse-data/docs/github-pr-body-angle-brackets-stripped-in-backticks.md b/mouse-data/docs/github-pr-body-angle-brackets-stripped-in-backticks.md new file mode 100644 index 0000000000..aad50a33e4 --- /dev/null +++ b/mouse-data/docs/github-pr-body-angle-brackets-stripped-in-backticks.md @@ -0,0 +1,28 @@ +--- +slug: github-pr-body-angle-brackets-stripped-in-backticks +title: GitHub PR body strips angle brackets inside backticks — markdown body containing shows as empty code spans +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**Symptom**: PR body containing `` `` `` (header name in backticks) renders as empty backticks on GitHub. The angle-bracket content is gone from the stored body, not just visually hidden. + +**Cause**: GitHub's markdown pre-processor treats ``, ``, `` etc. as malformed HTML tags and silently strips them BEFORE the markdown's backtick-code-span wrapping fires. Affects both `gh pr create` and `mcp__github__update_pull_request`. + +**Fix**: escape with HTML entities — `<vector>`, `<utility>` — even though they're inside backticks. Markdown will literalize the entities once you escape `<` and `>`. + +Example of broken vs fixed: +``` +broken: includes ``, ``, `` etc. +fixed: includes `<vector>`, `<utility>`, `<string>` etc. +``` + +The same gotcha doesn't apply to commit messages — those render fine. + +**Discovered**: PR #2716, 2026-05-18. Initial body had 6 stdlib headers stripped from the rendered body until we caught it by re-reading via `mcp__github__pull_request_read`. + +**Verification habit**: after `update_pull_request` or `create_pull_request` of a body with `<` or `>` characters, immediately call `pull_request_read method=get` and grep for empty backtick-pairs (``). + +## Questions +- GitHub PR body strips angle brackets inside backticks — markdown body containing shows as empty code spans diff --git a/mouse-data/docs/how-do-i-dedupe-a-select-projection-that-the-where-predicate-inlines-and-the-terminator-valueexpr-clones-inside-a-splice-macro-s.md b/mouse-data/docs/how-do-i-dedupe-a-select-projection-that-the-where-predicate-inlines-and-the-terminator-valueexpr-clones-inside-a-splice-macro-s.md new file mode 100644 index 0000000000..1931f9131a --- /dev/null +++ b/mouse-data/docs/how-do-i-dedupe-a-select-projection-that-the-where-predicate-inlines-and-the-terminator-valueexpr-clones-inside-a-splice-macro-s.md @@ -0,0 +1,47 @@ +--- +slug: how-do-i-dedupe-a-select-projection-that-the-where-predicate-inlines-and-the-terminator-valueexpr-clones-inside-a-splice-macro-s +title: how do I dedupe a select projection that the where predicate inlines AND the terminator valueExpr clones inside a splice macro so the projection evaluates once per element instead of twice +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**The pattern:** `_select(proj) |> _where(p) |> terminator` chains splice via the where-after-select arm in `plan_loop_or_count`. Phase 3d (PR #2712) substituted `proj` INTO the predicate via peel-aware `fold_linq_cond_peel`, and the lane emitters ALSO cloned `proj` into the terminator's `valueExpr` (via `clone_expression(projection)`). Net: projection evaluated twice per element on ARRAY / ACCUMULATOR / EARLY_EXIT lanes. + +**Why intermediateBinds doesn't fix it:** `intermediateBinds` (used for chained-select bind chains) gets PREPENDED inside `perMatchStmts` BEFORE `wrap_with_condition`, so it ends up INSIDE the `if (whereCond) { ... }` wrap. A bind there can't dedupe with the predicate — the predicate is the wrap's `cond`, evaluated first; if it referenced the bind var, the var wouldn't exist yet at that point. + +**Fix (PR #2714):** new `preConditionStmts : array` planner state, spliced into the loop body OUTSIDE the if-wrap via a `prepend_precond` helper. In the `_where` arm when `seenSelect=true`: +```das +if (has_sideeffects(projection)) return null // semantic gate — see below +if (lane != LinqLane.COUNTER) { // COUNTER opt-out — see below + let wbName = "`vw`{at.line}`{at.column}`{length(preCondStmts)}" + var projType = clone_type(elementType) + preCondStmts |> push <| qmacro_expr() { + var $i(wbName) : $t(projType) := $e(projection) + } + var pvar = new ExprVar(at = at, name := wbName) + pvar._type = clone_type(elementType) + pvar._type.flags.ref = true + projection = pvar +} +predicate = fold_linq_cond_peel(cll._0.arguments[1], projection) +``` + +Both the predicate (peel-substituted to reference `wbName`) AND `valueExpr` (cloned from the now-rewritten `projection`) reference the same single bind. Projection evaluates exactly once per element. + +**Two non-obvious constraints:** + +1. **The ExprVar replacement must be typed manually.** Setting `pvar._type` from `clone_type(elementType)` + `flags.ref = true` matters: an untyped `ExprVar` would propagate `auto` into `push_clone` / accumulator call sites and surface as `30165: cannot infer push_clone return type` at the typer (downstream of the splice, hard to diagnose). Don't rely on the typer to re-walk the loop's local decls to resolve the ExprVar's type from the `var wbName : $t(projType) := ...` binding — that re-resolution happens AFTER the type errors fire. + +2. **`has_sideeffects` is a semantic gate, not a perf gate.** Moving an impure projection outside the if-wrap would visibly fire side effects on filter-rejected elements — bail to tier 2 cascade in that case. (NOT "double the work" — both the old and new forms run the projection twice per element on a pure projection; the dedup just turns that into one bind read + one bind read of the same var.) + +3. **COUNTER lane opt-out:** count terminators never reference `valueExpr`, so the dedup brings zero benefit, and the extra per-element bind decl regresses the single-stmt fast path. Gate the bind on `lane != LinqLane.COUNTER`; benchmark proof: `select_where_count` m3f stayed at 5 ns/op pre- and post-PR with the opt-out, regressed to 7 ns/op without it. + +**Files:** `daslib/linq_fold.das` `plan_loop_or_count` (preCondStmts state + where-arm bind logic), `prepend_precond` helper, signatures of `emit_accumulator_lane` / `emit_early_exit_lane` extended with `preCondStmts` param. + +**Bench impact** (100K rows INTERP): `select_where_sum` m3 59 → m3f 7 ns/op (8.4×; also beats SQLite at 37 ns/op by 5.3×). + +Linked: [[parser-bug-30701-tagged-block-arg-dup-check]] for the qmacro fix that surfaced during the PR, [[pr-2714-linq-splice-single-eval]] for the landed PR. + +## Questions +- how do I dedupe a select projection that the where predicate inlines AND the terminator valueExpr clones inside a splice macro so the projection evaluates once per element instead of twice diff --git a/mouse-data/docs/how-do-i-disable-hdpi-framebuffer-scaling-in-daslang-live-for-recording.md b/mouse-data/docs/how-do-i-disable-hdpi-framebuffer-scaling-in-daslang-live-for-recording.md new file mode 100644 index 0000000000..6bccf681a1 --- /dev/null +++ b/mouse-data/docs/how-do-i-disable-hdpi-framebuffer-scaling-in-daslang-live-for-recording.md @@ -0,0 +1,60 @@ +--- +slug: how-do-i-disable-hdpi-framebuffer-scaling-in-daslang-live-for-recording +title: How do I disable HDPI framebuffer scaling in daslang-live (for APNG recording at logical pixel count)? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +Verified 2026-05-17 (daslang PR #2704, dasImgui PR #44). + +Pass `--no-hdpi-framebuffer` to `daslang-live` after the `--` +separator. It tells `glfw_live` (in `modules/dasGlfw/dasglfw/glfw_live.das`) +to set two GLFW window hints before `glfwCreateWindow`: + + - `GLFW_COCOA_RETINA_FRAMEBUFFER = 0` (macOS: disable retina backing) + - `GLFW_SCALE_TO_MONITOR = 0` (Windows: don't auto-scale by monitor DPI) + +Result: framebuffer matches the requested logical size 1:1. A +640×320-logical window stays at 640×320 in the framebuffer, instead +of 1280×640 on retina mac or 960×480 on a 150%-scaled Windows +monitor. + +**Use case:** APNG recording tools (dasImgui's `with_recording_app` +spawn path) pass both: + +``` +daslang-live -- --imgui-content-scale=1.0 --no-hdpi-framebuffer +``` + +`--imgui-content-scale=1.0` is the ImGui-style-side companion +(`live_imgui_init` reads it and styles ImGui at 1x instead of querying +GLFW's content-scale). Without these flags, `glReadPixels`-based +capture quadruples on retina and the APNG balloons in size. + +**Linux is a no-op:** X11 doesn't apply scaling at the GLFW level; +Wayland reports framebuffer == window for normal windows. The flag is +harmless to pass everywhere. + +**Tutorial users running `daslang-live` directly pass neither flag and +keep native HDPI** — this is recording-only. + +**Verified on Windows (PR #44):** `record_boost_basics` produces a +640×320 / ~7.5 MB APNG matching the retina-mac reference within +encoder noise. `GLFW_SCALE_TO_MONITOR = 0` correctly engages. + +**Inverse plumbing — keep HDPI by default:** see +`how-do-i-make-dasimgui-hdpi-aware-what-s-the-canonical-scale-plumbing` +(PR #42). dasImgui's `live_imgui_init` reads +`glfwGetWindowContentScale` and scales fonts + style automatically. +`--no-hdpi-framebuffer` short-circuits this by reporting scale = 1.0 +even on retina. + +**Source:** +- daslang PR #2704: `modules/dasGlfw/dasglfw/glfw_live.das` +- dasImgui PR #44 helper invocation: + `widgets/imgui_playwright.das` `with_recording_app` argv build +- ImGui-side `--imgui-content-scale=1.0` parser: `widgets/imgui_live.das` + +## Questions +- How do I disable HDPI framebuffer scaling in daslang-live (for APNG recording at logical pixel count)? diff --git a/mouse-data/docs/how-do-i-draw-on-the-imgui-foreground-or-window-drawlist-from-dasimgui-code.md b/mouse-data/docs/how-do-i-draw-on-the-imgui-foreground-or-window-drawlist-from-dasimgui-code.md index fa423e086d..61f88e94c9 100644 --- a/mouse-data/docs/how-do-i-draw-on-the-imgui-foreground-or-window-drawlist-from-dasimgui-code.md +++ b/mouse-data/docs/how-do-i-draw-on-the-imgui-foreground-or-window-drawlist-from-dasimgui-code.md @@ -2,27 +2,50 @@ slug: how-do-i-draw-on-the-imgui-foreground-or-window-drawlist-from-dasimgui-code title: How do I draw on the imgui foreground or window drawlist from dasImgui code? created: 2026-05-11 -last_verified: 2026-05-11 +last_verified: 2026-05-18 links: [] --- -Use the pipe pattern, **without** wrapping the call in `unsafe { ... }`: +Use the **drawlist rail** in `imgui/imgui_drawlist_builtin` (auto-required via `imgui/imgui_harness`). Raw `imgui::GetWindowDrawList` / `imgui::AddRect` / `AddRectFilled` / `AddLine` / `GetForegroundDrawList` are no longer on `ALLOWED_IMGUI` as of dasImgui PR #48 — `IMGUI002` flags them. Scaffolding-only escape: `options _allow_imgui_legacy = true`. ```das -*GetForegroundDrawList() |> AddRect(p_min, p_max, color, rounding, ImDrawFlags.None, thickness) -*GetWindowDrawList() |> AddRectFilled(p_min, p_max, color) -*GetWindowDrawList() |> AddText(pos, color, text_str) -*GetWindowDrawList() |> AddCircleFilled(center, radius, color, num_segments) -*GetWindowDrawList() |> AddLine(p1, p2, color, thickness) +require imgui/imgui_drawlist_builtin + +let green = rgba(60u, 220u, 100u, 255u) +let yellow = rgba(240u, 220u, 60u, 255u) + +with_window_drawlist() $(var dl) { + dl |> add_rect_filled(p_min, p_max, color, rounding, ImDrawFlags.None) + dl |> add_rect(p_min, p_max, color, rounding, ImDrawFlags.None, thickness) + dl |> add_line(p1, p2, color, thickness) + dl |> add_circle_filled(center, radius, color, num_segments) + dl |> add_circle(center, radius, color, num_segments, thickness) + dl |> add_triangle_filled(a, b, c, color) + dl |> add_triangle(a, b, c, color, thickness) + dl |> add_text(pos, color, "label") +} + +// Viewport-level variants — paint over EVERY window / under EVERY window +// respectively. Same primitives apply. +with_foreground_drawlist() $(var fdl) { + fdl |> add_rect_filled(p_min, p_max, color) +} +with_background_drawlist() $(var bdl) { + bdl |> add_rect_filled(p_min, p_max, tint) +} ``` -`GetForegroundDrawList()` and `GetWindowDrawList()` return `ImDrawList?`; dereffing with `*` and piping into the bound `Add*` methods works directly. Many examples in `modules/dasImgui/example/imgui_demo.das` (e.g. lines 753, 953, 1674, 2689-2695). +**Three scope wrappers**: `with_window_drawlist` (current window's drawlist — clipped to the window rect), `with_foreground_drawlist` (viewport-level overlay on top), `with_background_drawlist` (viewport-level underlay beneath all windows). + +**Eight primitives** so far: `add_text`, `add_line`, `add_rect`, `add_rect_filled`, `add_circle`, `add_circle_filled`, `add_triangle`, `add_triangle_filled`. Each is tagged `[drawlist_prim(kind="add_X")]` in `widgets/imgui_drawlist_builtin.das`. The macro emits a `::` path-key per call site (mouse-cards / playwright targeting) WITHOUT registering a `__widget__` state-global — drawlist is render-only. Multiple `add_line` per frame don't collide because nothing's stored. See `dasimgui-drawlist-prim-macro-path-key-only-no-state-global-render-only-rail`. + +**Container nesting is lexical, not visual.** Calling `with_window_drawlist` INSIDE `window(DRAW_WIN, ...) { … }` nests its primitives under `DRAW_WIN/`. Calling `with_foreground_drawlist` OUTSIDE that block produces top-level paths even though the foreground drawlist is viewport-scoped. The split is by lexical container scope at the call site, not by which drawlist gets painted. -No `unsafe { ... }` wrap needed. The bound `Add*` shims accept the deref'd lvalue directly — wrapping is redundant. (An earlier project note claimed the wrap silently no-ops; that was a misattribution, see `project_dasimgui_unsafe_drawlist_noop` — the unsafe form is functionally equivalent. `UnsafeFolding` replaces `ExprUnsafe` with its body before simulate runs, so both forms generate the same SimNodes.) +Coordinate space: ImGui drawlist coords are screen-space pixels. For widget-relative drawing inside `with_window_drawlist`, use `GetCursorScreenPos()` at the band start as the origin, or pull rects from dasImgui's per-frame registry (`widget_rect(target_path)` in `imgui/imgui_boost_runtime`). -Coordinate space: ImGui drawlist coords are screen-space pixels. For widget-relative drawing you typically use `GetItemRectMin/Max` or values out of dasImgui's per-frame registry (`widget_rect(target_path)` in `imgui/imgui_boost_runtime`). +Reference feature + tests: `examples/features/drawlist.das`, `tests/integration/test_drawlist_primitives.das`, `test_with_window_drawlist.das`, `test_drawlist_path_key.das`. Tutorial: `doc/source/tutorials/drawlist.rst` + recording `examples/tutorial/drawlist.das`. -Verified: `widgets/imgui_visual_aids.das` paints all three primitives (highlight rect, mouse trail dots, narrate box + connector line) via this pattern without any `unsafe` wrapping. +**Historical note** (pre-PR-#48): the rail-less idiom was `*GetWindowDrawList() |> AddRect(...)` — dereffing the `ImDrawList?` return then piping into the bound method. No `unsafe { ... }` wrap was needed. That form still compiles where the legacy escape is on (`options _allow_imgui_legacy = true`), but the rail is the documented surface and gets the path-key telemetry "for free". ## Questions - How do I draw on the imgui foreground or window drawlist from dasImgui code? diff --git a/mouse-data/docs/how-do-i-make-with-recording-app-attach-to-an-already-running-daslang-live.md b/mouse-data/docs/how-do-i-make-with-recording-app-attach-to-an-already-running-daslang-live.md new file mode 100644 index 0000000000..e762954dd2 --- /dev/null +++ b/mouse-data/docs/how-do-i-make-with-recording-app-attach-to-an-already-running-daslang-live.md @@ -0,0 +1,88 @@ +--- +slug: how-do-i-make-with-recording-app-attach-to-an-already-running-daslang-live +title: How do I make a recording driver attach to an already-running daslang-live host instead of spawning a new one? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +Verified 2026-05-17 (dasImgui PR #44, master post-merge). + +`with_recording_app` (in `widgets/imgui_playwright.das`) probes +`GET /status` at the live-API port before spawning. If a host is +already serving, it skips both the `popen_argv` spawn AND the +`/shutdown` signal at the end — runs `record_start` / body / +`record_stop` directly against the running host. + +**Use case:** dev keeps `daslang-live` open while iterating on a +tutorial (live-reload edits visible immediately), then triggers the +recorder without restarting the host. Before PR #44, spawning would +collide on port 9090, recorder panicked, and you had to kill the +host first. + +**The probe pattern (lines ~656-672 of imgui_playwright.das after PR #44):** + +```daslang +let ATTACH_PROBE_SEC : float = 0.3f // module-level tunable + +// ... feature_path / output_apng_path / app setup ... + +if (wait_until_ready(app, ATTACH_PROBE_SEC)) { + print("[record] attaching to existing daslang-live at {base_url}\n") + post_command(app, "imgui_mouse_trail", JV((enabled = true))) + post_command(app, "imgui_cursor_sprite", JV((enabled = true))) + post_command(app, "record_start", JV((file = output_apng_path, + fps = fps, + max_seconds = max_seconds))) + invoke(body, app) + let stopped = post_command(app, "record_stop", null) + // ... cleanup mouse_trail + cursor_sprite ... + return // DO NOT post /shutdown +} + +// ... fall through to existing spawn flow ... +``` + +`wait_until_ready(app, 0.3f)` = three `/status` polls at the existing +100 ms cadence. Instant when something's listening; fast TCP-refused +when not. 0.3 s feels imperceptible to the user but is enough to +detect any healthy host. + +**Contract differences spawn-vs-attach:** + +| Aspect | Spawn | Attach | +|---|---|---| +| Process lifecycle | helper owns | caller owns | +| `/shutdown` at end | yes | NO — host kept alive | +| Test-timeout watchdog | `popen_argv(test_timeout)` | none (body must self-terminate) | +| HDPI/style | logical 1x (`--imgui-content-scale=1.0` + `--no-hdpi-framebuffer`) | host's current setting | +| Feature loaded | helper passes `feature_path` to spawned host | caller owns whatever's loaded | + +**Caller responsibility in attach mode:** the helper does NOT post a +switch-feature command. If the running host has a different feature +loaded than what the driver expects, the body's +`wait_for_render(app, T_WIDGET, 10.0f)` will time out and panic with +the standard `"{T_WIDGET} never rendered — wrong app running?"` +message. That's the existing fail-loud path. + +**Reproducing for the dev workflow:** + +``` +# Shell 1 — start the host +daslang-live -project_root /examples/tutorial/boost_basics.das + +# Shell 2 — record (the driver scans 127.0.0.1:9090, sees the host, attaches) +daslang -project_root /tests/integration/record_boost_basics.das +# [record] attaching to existing daslang-live at http://127.0.0.1:9090 +# ... APNG written, host stays alive ... +``` + +**Gotcha:** attached recordings stay at native HDPI on retina/scaled +Windows. The logical-1x mode is spawn-only because that's where +`--imgui-content-scale=1.0` + `--no-hdpi-framebuffer` are passed. For +canonical APNG regen (consistent dimensions across machines), use the +no-host-running invocation so the helper spawns at logical 1x. See +`how-do-i-disable-hdpi-framebuffer-scaling-in-daslang-live-for-recording`. + +## Questions +- How do I make a recording driver attach to an already-running daslang-live host instead of spawning a new one? diff --git a/mouse-data/docs/how-do-i-read-a-pre-dash-dash-daslang-exe-flag-from-inside-a-script.md b/mouse-data/docs/how-do-i-read-a-pre-dash-dash-daslang-exe-flag-from-inside-a-script.md new file mode 100644 index 0000000000..19fc0008a8 --- /dev/null +++ b/mouse-data/docs/how-do-i-read-a-pre-dash-dash-daslang-exe-flag-from-inside-a-script.md @@ -0,0 +1,67 @@ +--- +slug: how-do-i-read-a-pre-dash-dash-daslang-exe-flag-from-inside-a-script +title: How do I read a pre-`--` daslang.exe flag (like `-project_root`) from inside a running script? +created: 2026-05-17 +last_verified: 2026-05-17 +links: [] +--- + +Verified 2026-05-17 (dasImgui PR #44). + +`get_command_line_arguments()` (daslang builtin) returns the **full +original argv**. `get_user_args()` (in `daslib/clargs`) is mode-aware +and in interpreted mode returns ONLY the slice after `--`. So if you +want to read a `daslang.exe` flag like `-project_root` from inside a +script, **use `get_command_line_arguments()`, not `get_user_args()`**. + +**Why:** `setCommandLineArguments(argc, argv)` is called at +`utils/daScript/main.cpp:140` (and `:509` for the alternate code path) +**before** any flag parsing. It freezes the full argv into a daslang- +visible array. So `daslang.exe -project_root D:/foo myscript.das` +produces a `get_command_line_arguments()` of +`["daslang.exe", "-project_root", "D:/foo", "myscript.das"]` — +`-project_root` is visible. + +`get_user_args()` (`daslib/clargs.das:74`) wraps this with mode logic: +- Standalone exe: `argv[1..]` (skips program name) +- **Interpreter mode: post-`--` slice only** — `-project_root` is invisible + +**Scan with `find_flag_raw_value`:** + +```daslang +require daslib/clargs + +let argv_all <- get_command_line_arguments() +let pr = find_flag_raw_value(argv_all, "-project_root") |> unwrap_or("") +if (!empty(pr)) { + // … use the project root, e.g. forward to a spawned daslang-live +} +``` + +`find_flag_raw_value` handles both `-flag value` and `-flag=value` +forms. Same import (`daslib/clargs`) as `get_user_args()`. + +**Bug pattern this fixes:** dasImgui's `with_recording_app` initially +scanned `get_user_args()` for `-project_root`. The documented +single-flag invocation + +``` +daslang.exe -project_root tests/integration/record_X.das +``` + +silently dropped the flag at forward time, and the spawned +daslang-live died on `require imgui` with +`error[20605]: missing prerequisite 'imgui'`. Workaround was passing +`-project_root` **twice** (pre- AND post-`--`). The fix swaps the +scanner to `get_command_line_arguments()`; the flag is visible +regardless of position. + +**Companion card:** `daslang-script-flags-need-dash-dash-separator` +covers the inverse — when YOU want to pass a flag to the script and +it doesn't show in `get_user_args()`, the fix is to use `--` before +your flag. The two together: pre-`--` flags belong to daslang.exe and +need `get_command_line_arguments()`; post-`--` flags belong to your +script and show up in `get_user_args()`. + +## Questions +- How do I read a pre-`--` daslang.exe flag (like `-project_root`) from inside a running script? diff --git a/mouse-data/docs/inlining-a-sort-comparator-key-body-does-it-double-the-work-for-expensive-keys-compared-to-keeping-the-key-lambda-call.md b/mouse-data/docs/inlining-a-sort-comparator-key-body-does-it-double-the-work-for-expensive-keys-compared-to-keeping-the-key-lambda-call.md new file mode 100644 index 0000000000..ebfc1a93cc --- /dev/null +++ b/mouse-data/docs/inlining-a-sort-comparator-key-body-does-it-double-the-work-for-expensive-keys-compared-to-keeping-the-key-lambda-call.md @@ -0,0 +1,33 @@ +--- +slug: inlining-a-sort-comparator-key-body-does-it-double-the-work-for-expensive-keys-compared-to-keeping-the-key-lambda-call +title: inlining a sort comparator key body — does it double the work for expensive keys compared to keeping the key lambda call +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**No. Both forms evaluate the key body twice per comparison.** The intuition "if I inline the body twice, expensive keys do double work" is wrong — the original `$(v1, v2) => _::less(key(v1), key(v2))` already calls `key` twice per comparison (once for each side). + +**What inlining actually saves:** indirect lambda-dispatch overhead. Concretely, for each comparison `cmp(v1, v2)`: + +| Step | Before inline (key-taking) | After inline (body spliced) | +|---|---|---| +| comparator call | 1 indirect call (block dispatch) | 1 indirect call (block dispatch) | +| `key(v1)` | 1 indirect call (lambda dispatch) | 0 — body[v1] inlined | +| `key(v2)` | 1 indirect call (lambda dispatch) | 0 — body[v2] inlined | +| body evaluation | 2× (once per `key(v)`) | 2× (inlined into both sides) | +| `_::less` | 1 direct call | 1 direct call | +| **Net dispatches** | **3** | **1** | + +So the win is **2 fewer indirect dispatches per comparison**. For trivial keys (`$(_) => _.price`) where the body IS a single load, the dispatch IS the dominant cost — and the savings are big (PR #2714: sort_take m3f 56 → 27 ns/op, ~2.1×). For expensive keys (`$(_) => some_real_work(_)`) the body still runs twice either way, so the relative win shrinks to the dispatch overhead alone — small win compared to the work. + +**`has_sideeffects` is a SEMANTIC gate, not a perf gate.** It blocks substitution when the body has observable side effects, because `replaceVariable` doesn't preserve the typer-inserted ExprRef2Value wrappers and ordering guarantees that side-effecty expressions rely on — moving them around via macro substitution can change observable behavior. Has nothing to do with "would double the expensive work." + +**Schwartzian transform is the orthogonal optimization** that DOES help expensive keys: precompute `key(v)` once per element into a paired array, then sort the paired array by the precomputed scalar. Neither path here does that — both the original keyed top_n_by and the inlined `top_n_by_with_cmp` re-evaluate the key per comparison. Schwartzian would be a separate library entry / macro layer; not in scope for PR #2714. + +**Canonical example:** `try_make_inline_cmp` in `daslib/linq_fold.das` (PR #2714). The `try_make_inline_cmp` helper's docstring spells this out in detail with the dispatch-count table. + +Linked: [[pr-2714-linq-splice-single-eval]], [[qmacro-multi-arg-block-declaration-with-i-name-splices-fails-with-error-30701]]. + +## Questions +- inlining a sort comparator key body — does it double the work for expensive keys compared to keeping the key lambda call diff --git a/mouse-data/docs/libstdcpp-vs-libcpp-transitive-stdexcept-asymmetry.md b/mouse-data/docs/libstdcpp-vs-libcpp-transitive-stdexcept-asymmetry.md new file mode 100644 index 0000000000..4760295e47 --- /dev/null +++ b/mouse-data/docs/libstdcpp-vs-libcpp-transitive-stdexcept-asymmetry.md @@ -0,0 +1,35 @@ +--- +slug: libstdcpp-vs-libcpp-transitive-stdexcept-asymmetry +title: why does my low-level daslang header compile on macOS but fail on Linux with "no member named X in namespace das" — transitive include differences +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**Symptom**: A header that compiles fine on macOS (clang + libc++) fails on Linux (clang/gcc + libstdc++) with errors like: +``` +error: no member named 'out_of_range' in namespace 'das' +``` + +**Cause**: Standard library implementations differ in what's pulled in transitively. +- **libc++** (macOS, Emscripten/WASM): `` / `` / `` typically pull `` transitively, making `std::out_of_range`, `std::runtime_error` visible. +- **libstdc++** (Linux GCC default): does NOT pull `` from those headers. You must `#include ` explicitly. + +Same asymmetry applies to ``, `` (placement new declarations), some `` features depending on version. + +**In daslang specifically**: `include/das_hash_map/das_hash_map.h` was pulled via `das_config.h`'s ``, ``, etc. — but on Linux, those don't bring `` along. PR #2716's first iteration removed the local `` thinking it was redundant; CI immediately failed on the Linux jobs. macOS local build had said "all clean". + +**Diagnosis tip**: when CI fails Linux-only on what looks like a namespace-resolution issue, suspect transitive-include differences. Check: +```bash +grep -rn "X" /path/to/libcxx-include/ # is X declared transitively in libc++? +grep -rn "X" /path/to/libstdcpp-include/vector # vs libstdc++? +``` + +**Fix options**: +1. Add the explicit `#include ` (or whatever) in the header that needs it. +2. Avoid the symbol entirely (PR #2716 chose this — switched from `std::out_of_range` to `das::das_throw`, which has no `` dependency). + +**Don't rely on "macOS local says it works"** for include hygiene questions. The CI matrix exists because the toolchains diverge in ways the lone-dev macOS build won't catch. + +## Questions +- why does my low-level daslang header compile on macOS but fail on Linux with "no member named X in namespace das" — transitive include differences diff --git a/mouse-data/docs/my-daspkg-package-s-register-native-path-module-isn-t-found-what-s-the-require-path-supposed-to-look-like.md b/mouse-data/docs/my-daspkg-package-s-register-native-path-module-isn-t-found-what-s-the-require-path-supposed-to-look-like.md index 15c217c155..189e9390f4 100644 --- a/mouse-data/docs/my-daspkg-package-s-register-native-path-module-isn-t-found-what-s-the-require-path-supposed-to-look-like.md +++ b/mouse-data/docs/my-daspkg-package-s-register-native-path-module-isn-t-found-what-s-the-require-path-supposed-to-look-like.md @@ -2,7 +2,7 @@ slug: my-daspkg-package-s-register-native-path-module-isn-t-found-what-s-the-require-path-supposed-to-look-like title: My daspkg package's register_native_path module isn't found — what's the require path supposed to look like? created: 2026-05-09 -last_verified: 2026-05-09 +last_verified: 2026-05-18 links: [] --- @@ -52,11 +52,9 @@ require imgui_boost_v2 // ✗ - `daslang -dasroot ` similarly walks via `require_dynamic_modules` - daspkg install during build -**The MCP daslang server** does NOT pre-load `.das_module` files, so `mcp__daslang__compile_check` on a file that requires dynamic modules will fail with "missing prerequisite" — that's expected. End-to-end testing uses daslang-live, not MCP compile_check. +**The MCP daslang server** supports daspkg-style modules via the `project_root` argument (added 2026-05-18; commit 26e1407c1). Every MCP tool that already takes `project` also accepts `project_root`, equivalent to daslang's `-project_root

` CLI flag. For external dev work without `daspkg install`, point `project_root` at a dummy root with a `modules/` junction — see [external_module_debugging.md](../../skills/external_module_debugging.md) for the workflow. Before that change, MCP couldn't resolve dynamic modules and end-to-end testing fell back to daslang-live or shell. -**Surfaced 2026-05-09** during dasImgui Phase 0a; spent ~30 minutes wondering why `require imgui_boost_v2` couldn't find the just-registered module. Adding the `imgui/` prefix fixed it instantly. - -last_verified: 2026-05-09 +**Surfaced 2026-05-09** during dasImgui Phase 0a; spent ~30 minutes wondering why `require imgui_boost_v2` couldn't find the just-registered module. Adding the `imgui/` prefix fixed it instantly. Updated 2026-05-18 with MCP `project_root` support. ## Questions - My daspkg package's register_native_path module isn't found — what's the require path supposed to look like? diff --git a/mouse-data/docs/qmacro-multi-arg-block-declaration-with-i-name-splices-fails-with-error-30701-block-argument-is-already-declared-macro-tag-how-d.md b/mouse-data/docs/qmacro-multi-arg-block-declaration-with-i-name-splices-fails-with-error-30701-block-argument-is-already-declared-macro-tag-how-d.md new file mode 100644 index 0000000000..00d52919eb --- /dev/null +++ b/mouse-data/docs/qmacro-multi-arg-block-declaration-with-i-name-splices-fails-with-error-30701-block-argument-is-already-declared-macro-tag-how-d.md @@ -0,0 +1,44 @@ +--- +slug: qmacro-multi-arg-block-declaration-with-i-name-splices-fails-with-error-30701-block-argument-is-already-declared-macro-tag-how-d +title: qmacro multi-arg block declaration with $i name splices fails with error 30701 block argument is already declared MACRO TAG how do I emit a typed block with multiple distinct argument names +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +**Root cause:** `src/parser/parser_impl.cpp:ast_makeBlock` does a parse-time dup check on block-arg names via `closure->findArgument(name_at.name)`. The parser stamps every `$i(expr)` in block-arg-decl position with the literal placeholder name `"``MACRO``TAG``"` (the actual name is resolved later, post-parse, by the macro tag visitor). So `qmacro($($i(a) : $t(T), $i(b) : $t(T)) { ... })` parses BOTH args with that same placeholder string and the dup check fires before macro substitution gets a chance to assign distinct names. + +**Diagnostic:** error `30701: block argument is already declared ``MACRO``TAG``` while compiling a `[macro_function]` that emits a typed multi-arg block via `qmacro($($i(a) : ..., $i(b) : ...) { ... })`. + +**Two workarounds + one upstream fix:** + +1. **Upstream fix** (PR #2714 commit `bd76c588f`): in `parser_impl.cpp:885`, skip the dup check when `name_at.tag != nullptr` — tagged names are deferred-resolved by the macro processor, and genuine post-resolution dups surface as ordinary local-lookup conflicts during type inference. Three lines + a comment. General-purpose. + +2. **Hybrid emission** (works pre-fix): emit the block via qmacro with ONE typed `$i`-named arg, then append the second as a fresh `Variable` manually: + ```das + var cmpMake = qmacro($(v1 : $t(elemType) -&) { + return $e(cmpExpr) + }) as ExprMakeBlock + var cmpBody = cmpMake._block as ExprBlock + var v2Var = new Variable(at = at, name := "v2", _type = clone_type(elemType)) + v2Var._type.flags.removeConstant = true + v2Var._type.flags.ref = true + cmpBody.arguments |> emplace_new(v2Var) + return cmpMake + ``` + Fragile because the flag bookkeeping for the second var must match what qmacro's `$t(T) -&` produces for the first — different modifier ordering produces different rendered shapes (`Car -&` vs `Car&` vs `Car const -&`). + +3. **Untyped block args** (the cleanest for most use cases — what PR #2714 ships): drop type annotations on both args and let the typer infer from the call-site signature: + ```das + return qmacro($(v1, v2) { + return $e(cmpExpr) + }) + ``` + The typer resolves `v1`/`v2` types from the function the block gets passed to (e.g. `block<(v1 : TT -&, v2 : TT -&) : bool>` in `top_n_by_with_cmp(arr, n, cmp)`). Works because the block-arg-decl path here is a SOURCE-LEVEL block expression in qmacro, NOT an `$i`-tagged form, so the parser sees distinct `v1` and `v2` identifiers and the dup check passes naturally. + +**When to pick which:** untyped (#3) sidesteps both the dup-check bug AND the const-ref flag-bookkeeping fragility — recommended for any case where the typer can infer from context. Typed multi-arg `$i` (#1, post-fix) is for cases where you genuinely need the spliced name from a string variable (gensym from `at.line`/`at.column`) AND need explicit typing. + +See PR #2714 and the `try_make_inline_cmp` helper in `daslib/linq_fold.das` for the canonical example. + +## Questions +- qmacro multi-arg block declaration with $i name splices fails with error 30701 block argument is already declared MACRO TAG how do I emit a typed block with multiple distinct argument names diff --git a/mouse-data/docs/wsl-ubuntu-repro-dasimgui-docs-ci-build-job-step-by-step.md b/mouse-data/docs/wsl-ubuntu-repro-dasimgui-docs-ci-build-job-step-by-step.md new file mode 100644 index 0000000000..03a82f2c6d --- /dev/null +++ b/mouse-data/docs/wsl-ubuntu-repro-dasimgui-docs-ci-build-job-step-by-step.md @@ -0,0 +1,52 @@ +--- +slug: wsl-ubuntu-repro-dasimgui-docs-ci-build-job-step-by-step +title: How do I reproduce the dasImgui docs CI build job locally in WSL Ubuntu without sudo? +created: 2026-05-18 +last_verified: 2026-05-18 +links: [] +--- + +Mirror the GitHub Actions `ubuntu-latest` docs job step-for-step inside WSL Ubuntu 24.04. Useful when sphinx-build fails in CI but local Windows builds pass — and you suspect Linux-specific behavior. + +```bash +# 1. Setup (one-time). Bypass sudo password — WSL `-u root` has no password. +wsl.exe -d Ubuntu-24.04 -u root -- apt-get install -y python3-pip python3-venv build-essential libcurl4-openssl-dev libssl-dev clang flex bison libglu1-mesa-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev mesa-common-dev libwayland-dev libxkbcommon-dev + +# 2. Sphinx in a venv (system Python is PEP-668 externally-managed). +wsl.exe -d Ubuntu-24.04 -- bash -c "python3 -m venv ~/sphinx-venv && ~/sphinx-venv/bin/pip install --quiet sphinx==7.2.6 sphinx-rtd-theme==2.0.0" + +# 3. Clone daslang master + the dasImgui PR branch as siblings. +wsl.exe -d Ubuntu-24.04 -- bash -c " + mkdir -p ~/ci-repro && cd ~/ci-repro + git clone --depth=1 -b master https://github.com/GaijinEntertainment/daScript daslang-src + git clone --depth=1 -b https://github.com/borisbat/dasImgui dasImgui +" + +# 4. Build daslang. +wsl.exe -d Ubuntu-24.04 -- bash -c " + cd ~/ci-repro/daslang-src + cmake --no-warn-unused-cli -B./build -DCMAKE_BUILD_TYPE:STRING=Release -DDAS_HV_DISABLED=OFF -DDAS_PUGIXML_DISABLED=OFF -DDAS_LLVM_DISABLED=ON -DDAS_GLFW_DISABLED=OFF -G Ninja + cmake --build ./build --parallel --target daslang +" + +# 5. daspkg install dasImgui globally (CI step: 'daspkg install --global'). +wsl.exe -d Ubuntu-24.04 -- bash -c " + ~/ci-repro/daslang-src/bin/daslang ~/ci-repro/daslang-src/utils/daspkg/main.das -- install ~/ci-repro/dasImgui --global +" + +# 6. Run imgui2rst then sphinx-build, as CI does. +wsl.exe -d Ubuntu-24.04 -- bash -c " + cd ~/ci-repro/daslang-src/modules/dasImgui + ~/ci-repro/daslang-src/bin/daslang utils/imgui2rst.das -- --detail_output doc/source/stdlib/generated + ~/sphinx-venv/bin/sphinx-build -W --keep-going -b html -d build/sphinx-cache doc/source build/site +" +``` + +**Caveat — PR merge ref**: this repro builds the BRANCH HEAD, not the merge ref. If CI's failure is caused by collisions with master changes that landed since you forked (e.g., duplicate RST anchors auto-merged into one file), this repro will silently miss it. Add a `cd ~/ci-repro/dasImgui && git fetch origin master && git merge origin/master` step before running imgui2rst when you suspect a merge-ref bug. See `github-actions-pr-merge-ref-sphinx-duplicate-target-local-passes-ci-fails`. + +**Cost**: first run ~15 min (daslang Release build). Subsequent runs just iterate steps 5-6 (~30s for imgui2rst + sphinx). + +Verified 2026-05-18 against daslang ced9f5175 + dasImgui PR #48; WSL reproduced CI's pipeline cleanly (modulo the merge-ref caveat above). + +## Questions +- How do I reproduce the dasImgui docs CI build job locally in WSL Ubuntu without sudo? diff --git a/skills/das_macros.md b/skills/das_macros.md index 6ac4d53631..18d3d9e30e 100644 --- a/skills/das_macros.md +++ b/skills/das_macros.md @@ -40,6 +40,25 @@ If you find yourself reading older guidance about `var inscope`, `<-`, `move_new`, `add_ptr_ref` for AST types, the source is pre-migration. The post-migration rules above are correct as of daslang 0.6.x. +### Pre-set `_type` on emitted `ExprVar` (and similar nodes) that flow into typed positions + +`Expression::clone` deep-copies `_type` faithfully ([ast.cpp:1094](src/ast/ast.cpp#L1094): `expr->type = type ? new TypeDecl(*type) : nullptr`). So whatever you put on the source propagates to every consumer. The trap is the **source**: `new ExprVar(at = at, name := wbName)` leaves `_type` null, every `clone_expression` of it inherits the null, and if any of those clones flows into a generic call (`push_clone`, `sum`, etc.) the typer fails with `30165: cannot infer ... return type with 'auto'`. + +Don't rely on the typer's later local-variable-resolution pass to fix this — its generic-instantiation pass runs **first** and commits to `auto`, cascading errors up through every downstream consumer. + +Fix at emission time: + +```das +var pvar = new ExprVar(at = at, name := boundName) +pvar._type = clone_type(boundElementType) +pvar._type.flags.ref = true +// now any clone_expression(pvar) downstream carries the type into push_clone et al. +``` + +Same family of trap as the [ExprRef2Value blocker](#peel-exprref2value-before-qmatch) — the typer doesn't repair what macro substitution introduces, when the substitution lands in an already-typed AST fragment. + +Canonical example: `try_make_inline_cmp` and the `_where`-arm projection-bind rewrite in `daslib/linq_fold.das` (PR #2714). + ## The few residual smart_ptr types — `Program`, `Context`, `FileAccess` A small set of types are still `smart_ptr` (refcounted with manual diff --git a/skills/external_module_debugging.md b/skills/external_module_debugging.md new file mode 100644 index 0000000000..10e2fd3e11 --- /dev/null +++ b/skills/external_module_debugging.md @@ -0,0 +1,107 @@ +# Debugging an external daslang module locally + +When you're iterating on a daslang module that lives OUTSIDE the main daslang repo — dasImgui, dasPUGIXML, dasSQLITE, dasCards, dasTelegram, your own daspkg package, etc. — you need a way to run/lint/test from a standalone `daslang.exe` (or via the MCP server) without a full `daspkg install`. The trick is the **dummy-project-root junction pattern**. + +## The problem + +Each external module ships a `.das_module` descriptor (e.g. `D:/DASPKG/dasImgui/.das_module`) whose `initialize(project_path)` callback registers native paths like `register_native_path("imgui", "imgui_widgets_builtin", "

/widgets/imgui_widgets_builtin.das")`. daslang only fires those callbacks when it can locate the module under a `/modules//` directory and you pass `-project_root ` on the CLI (or `project_root` via the MCP tool). + +So running `daslang.exe path/to/your-module/main.das` directly fails with `missing prerequisite 'imgui'` — daslang has no `modules/` tree to scan, and the `.das_module` `initialize()` never fires. + +Running via `daspkg install` works but cuts off the live edit loop: every `.das` or C++ change needs a reinstall. + +## The fix: dummy-project-root junction + +Create a throwaway directory whose only contents are a `modules/` junction (Windows) or symlink (POSIX) pointing at your dev checkout. Then use it as `-project_root`. + +### Windows + +```cmd +mkdir D:\IMGUI_DEMO\modules +mklink /J D:\IMGUI_DEMO\modules\dasImgui D:\DASPKG\dasImgui +``` + +`mklink /J` creates a directory junction — same effect as a symlink for daslang's purposes and doesn't need admin. + +### POSIX + +```sh +mkdir -p /tmp/IMGUI_DEMO/modules +ln -s /path/to/dasImgui /tmp/IMGUI_DEMO/modules/dasImgui +``` + +## Using it + +Once the junction exists, the dummy root works with every tool that takes `-project_root` / `project_root`. + +### daslang.exe CLI + +```sh +daslang.exe -project_root D:/IMGUI_DEMO modules/dasImgui/examples/imgui_demo/harness_layout.das +``` + +### daslang-live + +```sh +daslang-live.exe -project_root D:/IMGUI_DEMO modules/dasImgui/examples/imgui_demo/harness_layout.das +``` + +### MCP tools (preferred — keeps everything inside the editor) + +Every MCP tool that already accepts `project` also accepts `project_root`. Pass it the dummy-root path: + +```jsonc +mcp__daslang__compile_check { + "file": "D:/DASPKG/dasImgui/examples/imgui_demo/layout.das", + "project_root": "D:/IMGUI_DEMO" +} +``` + +Same shape for `lint`, `find_symbol`, `goto_definition`, `find_references`, `type_of`, `ast_dump`, `list_requires`, `program_log`, `list_module_api`, `describe_type`, `run_test`, `run_script`, `eval_expression`, `aot`, `live_launch`. + +## Why a junction, not a copy + +The junction is the dev checkout. Edits to the module's source (`.das` or `.cpp`) are picked up live — no sync step. A copy would need rebuilding on every change. + +## When to use this + +- Iterating locally on an external module's daslang code before pushing to CI. +- Iterating on the module's C++ binding — rebuild `dasModule.shared_module`, the next MCP/daslang invocation picks it up (the MCP server itself does not load user-side bindings, so DLL stays unlocked between calls). +- Running the module's harnesses / integration tests outside the `daspkg install` flow. +- Cross-module lint/compile sanity checks before a PR. + +## When NOT to use this + +- Production daspkg installs — those use `daspkg install ` which sets up the same `/modules//` layout natively. +- Anything that depends on a built dasModule being copied to a specific install path — junctions are paths, not file copies, and the C++ binary still lives in the dev checkout. + +## Combining with `.das_project` + +Both `project` (a `.das_project` file path) and `project_root` (a directory) can be set on the same MCP call or CLI invocation. daslang loads them independently — `.das_project` for custom `module_get` callbacks, `-project_root` for daspkg-style `modules/*/` discovery. Use both when the module has its own `.das_project` AND you want daspkg dispatch. + +## Worked example: dasImgui + +```cmd +:: One-time setup +mkdir D:\IMGUI_DEMO\modules +mklink /J D:\IMGUI_DEMO\modules\dasImgui D:\DASPKG\dasImgui + +:: Direct CLI +daslang.exe -project_root D:/IMGUI_DEMO ^ + modules/dasImgui/examples/imgui_demo/harness_layout.das + +:: Via MCP (inside Claude Code / editor) +mcp__daslang__compile_check { + "file": "D:/DASPKG/dasImgui/examples/imgui_demo/layout.das", + "project_root": "D:/IMGUI_DEMO" +} +``` + +Same pattern for any other daspkg-style module — adjust the `mklink` target and the `-project_root` path. + +## Gotchas + +- **NEVER put a junction inside `D:\Work\daScript\modules\`** (daslang's own modules/ tree). That's reserved for daslang's stdlib; user-side modules belong in the dummy root. +- **The module's `.das_module` initializer is invoked with `project_path = /modules/`** (the junction path). Inside the initializer, `{project_path}/widgets/foo.das` resolves through the junction to the real dev-checkout path. This is by design — the `.das_module` doesn't need to know it's being used via a junction. +- **On Windows, `mklink /J` is a junction (NTFS), not a symbolic link.** Junctions don't need admin; symlinks (`mklink /D` without `/J`) do. Junctions work for daslang's path resolution. +- **`-project_root` is independent of `-dasroot`.** `-dasroot` points at the daslang stdlib (`daslib/`); `-project_root` points at your user modules. They don't conflict. diff --git a/skills/style_lint.md b/skills/style_lint.md index 6ee010732e..ca0db8f493 100644 --- a/skills/style_lint.md +++ b/skills/style_lint.md @@ -36,6 +36,9 @@ The `style_lint` module detects non-idiomatic patterns in daslang code at compil | STYLE021 | `var v : table` followed by ≥ 2 contiguous `v \|> insert(, ...)` | Use the named-tuple JV form: `var v = JV((k1=val1, k2=val2, ...))` (`daslib/json_boost.das:638`). Computed keys disqualify the whole chain. | | STYLE022 | `foo \|= BfT.m` / `foo &= ~BfT.m` where `foo._type.baseType == tBitfield` and the RHS resolves to exactly one named bit | Use `foo.m = true` / `foo.m = false` (bitfield-as-field assignment). RHS is matched in two shapes: `ExprField(value=ExprVar(BfT), name="m")` under lint policies, and `ExprConstBitfield()` under normal compile policies (single-bit mask mapped back to bit name via `TypeDecl.argNames`). The `&=` form requires explicit `~`; bare `foo &= BfT.m` stays silent (different semantics). Multi-bit RHS (`Mode.read \| Mode.write`) and dynamic RHS skipped. **Note**: only safe when the AOT C++ side has `__bit_set` overloads matching the underlying integer type — `include/daScript/simulate/aot.h` provides `Bitfield&`, `Bitfield8/16/64&`, and raw `uint8/16/32/64_t&` overloads (the raw-integer set covers handle-bound bitfield fields like `Function::flags`, which is `uint32_t` on the C++ side). | | STYLE023 | `int_cast(bf & BfT.m) !=/== 0` where `bf._type.baseType == tBitfield`, the cast is one of `int`/`uint`/`int64`/`uint64`, and the RHS of `&` resolves to one named bit | Use `bf.m` (for `!= 0`) or `!bf.m` (for `== 0`). Matches both operand orders (`cast != 0` and `0 != cast`). Single-bit detection mirrors STYLE022 (both `ExprField` and `ExprConstBitfield` shapes); multi-bit masks and dynamic RHS skipped. Triggers on any of the four `ExprConst{Int,UInt,Int64,UInt64}` zero literals so signed/unsigned and 32/64-bit cast forms all fire. | +| STYLE024 | Redundant `unsafe` wrap — `unsafe(expr)` (parser flag `userSaidItsSafe` on the inner expression) **or** `unsafe { ... }` block whose body contains no statement matching a known inherently-unsafe AST shape | Drop the wrap. The "inherently-unsafe" recognizer (`expr_needs_unsafe`) is a recursive walk that flags `ExprCast` with `upcastCast`/`reinterpretCast`, `ExprDelete`, `ExprAddr` (`@@`), `ExprRef2Ptr` (`addr(x)`), `ExprAsVariant`, `ExprAt` on a `table<>` value, `ExprField` whose value is variant-typed, and `ExprCallFunc` whose `func.flags.unsafeOperation` (or `moreFlags.unsafeOutsideOfFor` outside a for-loop source) is set. Subtrees with `genFlags.generated == true` are skipped entirely — macro-synthesized AST is excluded from the rule. Note: the parser-flag form (`unsafe(expr)`) only survives folding under `no_optimizations + no_infer_time_folding`, so const-foldable inner expressions (e.g. `unsafe(1 + 2)`) only fire under the lint runner (`utils/lint/main.das`). Block-form fires under regular compile too. | +| STYLE025 | `unsafe { stmt1; stmt2; ...; stmtN }` block where exactly **one** statement matches `expr_needs_unsafe`; the rest are mundane | Narrow to `unsafe()` on the one statement that needs it. Silent when ≥ 2 statements need unsafe (block scope is justified). 0-needing-statements falls through to STYLE024 (drop the block). | +| STYLE026 | Nested `unsafe { ... }` — an `unsafe` block appears inside another open `unsafe` block, with no closure/lambda/generator boundary between them | Drop the inner wrap. Tracked by `unsafe_block_stack` — one slot per closure level, pushed on function entry and `blockFlags.isClosure` entry, popped on exit. `preVisitExprUnsafe` flags any entry where `stack[top] > 0`. Closure bodies push a fresh 0-slot because they execute in a separate context where the outer caller's `unsafe { }` does not propagate. | Note: `get_ptr()` related patterns (null comparison, field access) are in `perf_lint` as PERF010/PERF011 since they have performance implications. diff --git a/src/simulate/runtime_string.cpp b/src/simulate/runtime_string.cpp index 54439cb286..9bba9a8f22 100644 --- a/src/simulate/runtime_string.cpp +++ b/src/simulate/runtime_string.cpp @@ -39,6 +39,14 @@ namespace das } *g_throwBuf = nullptr; } + #else + + // DAS_ENABLE_EXCEPTIONS=1: route das_throw through the project's exception class + // so catch (dasException&) handlers picked up by simulate_exceptions.cpp catch + // C++-side throws from low-level headers (daslang_hash_map::at, etc.) too. + void das_throw(const char * msg) { + throw dasException(msg ? msg : "", LineInfo()); + } #endif template diff --git a/utils/lint/tests/style024_redundant_unsafe.das b/utils/lint/tests/style024_redundant_unsafe.das new file mode 100644 index 0000000000..c0f7dceba9 --- /dev/null +++ b/utils/lint/tests/style024_redundant_unsafe.das @@ -0,0 +1,113 @@ +options gen2 +// STYLE024: redundant `unsafe` wrap; nothing inside requires unsafe +// +// Problem: writing `unsafe(expr)` or `unsafe { ... }` around code that +// has no inherently-unsafe operation adds noise and masks the *real* +// unsafe sites elsewhere in the file. The compiler still has to walk +// the wrap; the reader has to read it. Drop it. +// +// Bad pattern: +// let c = unsafe(a + b) +// unsafe { +// let d = a + b +// print("{d}\n") +// } +// +// Good pattern: +// let c = a + b +// { +// let d = a + b +// print("{d}\n") +// } +// +// Detection: the parser sets `genFlags.userSaidItsSafe = true` on the +// inner expression of `unsafe(expr)`, and produces an `ExprUnsafe` node +// for `unsafe { ... }`. The visitor's `expr_needs_unsafe` recognizer +// walks the body (skipping `genFlags.generated` subtrees per design) and +// looks for known inherently-unsafe AST shapes: +// ExprCast(upcast|reinterpret), ExprDelete, ExprAddr, ExprRef2Ptr, +// ExprAsVariant, ExprAt on table, ExprField on variant, and +// ExprCallFunc whose `func.flags.unsafeOperation` (or unsafeOutsideOfFor +// outside a for-loop source) is set. If nothing matches, the wrap is +// redundant. +// +// Note: the parser-flag form (`unsafe(expr)`) only survives compile-time +// folding when `no_optimizations + no_infer_time_folding` is set; the +// lint runner enables both. Constant-foldable inner expressions therefore +// only trip STYLE024 under `utils/lint/main.das`. The block form fires +// under regular compile too. + +expect 31209:5 + +require daslib/style_lint + +// --- Bad patterns --- + +def bad_unsafe_addition(a, b : int) : int { + return unsafe(a + b) // STYLE024 — wrap is decorative +} + +def bad_unsafe_block_print() : void { + let a = 1 + let b = 2 + unsafe { // STYLE024 — block has no unsafe-needing stmt + let d = a + b + print("{d}\n") + } +} + +def bad_unsafe_string_add(s : string) : string { + return unsafe(s + "tail") // STYLE024 — string concat is safe +} + +def bad_unsafe_table_read(var t : table) : bool { + return unsafe(key_exists(t, "k")) // STYLE024 — key_exists is safe +} + +def bad_unsafe_block_just_assignment() : int { + var x = 0 + unsafe { // STYLE024 — assignment / print are safe + x = 5 + print("set\n") + } + return x +} + +// --- Good patterns (no warning) --- + +struct Foo { + x : int +} + +def good_reinterpret_needs_unsafe() : void { + var f = Foo(x = 5) + let p = unsafe(reinterpret(unsafe(addr(f)))) // wrap justified — reinterpret cast + print("{*p}\n") +} + +def good_block_with_two_unsafe_ops() : void { + var f = Foo(x = 5) + var g = Foo(x = 7) + unsafe { + let pa = reinterpret(addr(f)) // reinterpret + addr need unsafe + let pb = reinterpret(addr(g)) // second one — STYLE025 silent (≥2) + print("{*pa} {*pb}\n") + } +} + +// --- Regression: class on stack requires unsafe (ast_infer_type_make.cpp) --- +// `MyClass(...)` is an ExprMakeStruct with makeType.isClass — the compiler +// rejects it outside unsafe scope, so the wrap is NOT redundant. STYLE024 +// must NOT fire here (Copilot review #3260356968). + +class private MyClass { + x : int +} + +def good_class_on_stack() : void { + unsafe { + var c1 = MyClass(x = 5) + var c2 = MyClass(x = 7) + print("{c1.x} {c2.x}\n") + } +} diff --git a/utils/lint/tests/style025_unsafe_block_narrow.das b/utils/lint/tests/style025_unsafe_block_narrow.das new file mode 100644 index 0000000000..866833e461 --- /dev/null +++ b/utils/lint/tests/style025_unsafe_block_narrow.das @@ -0,0 +1,78 @@ +options gen2 +// STYLE025: `unsafe { ... }` block where only one statement needs unsafe +// +// Problem: a multi-statement `unsafe` block can hide a single unsafe op +// behind a wide scope. Reads as if the whole block were dangerous; in +// fact only one expression is. CLAUDE.md states the gen2 preference: +// `unsafe(expr)` over `unsafe { block }`. When the block has exactly +// one unsafe-needing statement AND narrowing is semantically safe, +// rewrite it. +// +// Narrowing is feasible only when: +// - the unsafe-needing stmt is a *call-shaped* expression statement +// (ExprCall / ExprOp* / ExprNew / ExprInvoke / ExprNamedCall) — +// `unsafe(x = expr)` is a parse error, so ExprCopy/Move/Clone don't +// qualify even though they're expression statements, +// - AND no descendant of that stmt is itself unsafe-needing +// (`addr(x)`, `reinterpret<>`, variant write, table index, etc.). +// Block form unsafe propagates `unsafeDepth` to inner expressions; +// `unsafe(...)` expression form does not — narrowing a stmt with nested +// `addr(...)` would re-trigger the compiler's safeExpression error. +// +// Bad pattern: +// unsafe { +// my_unsafe_fn(plain_arg) +// my_plain_op(plain_arg) +// } +// +// Good pattern: +// unsafe(my_unsafe_fn(plain_arg)) +// my_plain_op(plain_arg) + +expect 31209:2 + +require daslib/style_lint + +[unsafe_operation] +def my_unsafe_void(a : int) : void { + // intentional empty — `[unsafe_operation]` is the attribute that + // requires callers to wrap in unsafe. + pass +} + +// --- Bad patterns --- + +def bad_single_stmt_unsafe_call() : void { + unsafe { // STYLE025 — single call to unsafe fn + my_unsafe_void(5) + } +} + +def bad_mixed_block() : void { + unsafe { // STYLE025 — only my_unsafe_void needs unsafe + my_unsafe_void(5) + print("after\n") + } +} + +// --- Good patterns (silent) --- + +def good_two_unsafe_calls() : void { + unsafe { // 2 unsafe-needing stmts — block justified + my_unsafe_void(5) + my_unsafe_void(7) + } +} + +def good_narrowed_expression_form() : void { + unsafe(my_unsafe_void(5)) // expression form — no STYLE025 +} + +def good_nested_addr_block() : void { + // Block form is required when nested `addr(x)` needs unsafeDepth to + // propagate — STYLE025 should NOT fire here. + var arr <- [1, 2, 3] + unsafe { + my_unsafe_void(*addr(arr[0])) + } +} diff --git a/utils/lint/tests/style026_nested_unsafe.das b/utils/lint/tests/style026_nested_unsafe.das new file mode 100644 index 0000000000..c11c4a416a --- /dev/null +++ b/utils/lint/tests/style026_nested_unsafe.das @@ -0,0 +1,76 @@ +options gen2 +// STYLE026: nested `unsafe { ... }` block +// +// Problem: `unsafe { ... unsafe { ... } ... }` — the outer block already +// elevates the entire scope to `unsafeDepth > 0`, so the inner block +// adds nothing and is pure noise. Drop the inner wrap. +// +// Bad pattern: +// unsafe { +// my_unsafe_void(5) +// unsafe { +// my_unsafe_void(7) // <-- redundant inner block +// } +// } +// +// Good pattern: +// unsafe { +// my_unsafe_void(5) +// my_unsafe_void(7) +// } +// +// Detection: a counter tracks the depth of currently-open ExprUnsafe nodes +// via `preVisitExprUnsafe` / `visitExprUnsafe`. Any second push (depth > 0 +// at entry) is a nested block. + +expect 31209:2 + +require daslib/style_lint + +[unsafe_operation] +def my_unsafe_void(a : int) : void { + pass +} + +// --- Bad patterns --- + +def bad_nested_block() : void { + unsafe { // outer + my_unsafe_void(5) + unsafe { // STYLE026 — nested + my_unsafe_void(7) + } + } +} + +def bad_deeply_nested() : void { + unsafe { + my_unsafe_void(1) + unsafe { // STYLE026 — depth 2 + my_unsafe_void(2) + my_unsafe_void(3) + } + } +} + +// --- Good patterns (silent) --- + +def good_flat_block() : void { + unsafe { // no nesting + my_unsafe_void(5) + my_unsafe_void(7) + } +} + +def good_sibling_blocks() : void { + // Two unrelated `unsafe { }` blocks in sequence — not nested, just + // consecutive. STYLE026 must NOT fire on either. + unsafe { + my_unsafe_void(1) + my_unsafe_void(2) + } + unsafe { + my_unsafe_void(3) + my_unsafe_void(4) + } +} diff --git a/utils/mcp/protocol.das b/utils/mcp/protocol.das index a6da748748..1f6511bfd5 100644 --- a/utils/mcp/protocol.das +++ b/utils/mcp/protocol.das @@ -170,6 +170,7 @@ struct ToolsListResult { } let PROJECT_PROP = PropertySchema(_type = "string", description = "Path to a .das_project file for custom module resolution") +let PROJECT_ROOT_PROP = PropertySchema(_type = "string", description = "Project root directory (parent of modules/) for daspkg-style module resolution; equivalent to daslang's -project_root CLI flag. Use when working on an external module via a /modules/ junction.") def make_file_tool(name, description : string) : MakeFileTool { return MakeFileTool( @@ -182,7 +183,8 @@ def make_file_tool(name, description : string) : MakeFileTool { _type = "string", description = "Path to the .das file" ), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, required = ["file"] ) @@ -209,6 +211,7 @@ def handle_tools_list(id_json : string) : string { { "file" => PropertySchema(_type = "string", description = "Path to .das file, comma-separated paths, or glob pattern (e.g. 'dir/*.das')"), "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP, "json" => PropertySchema(_type = "string", description = "If 'true', return structured JSON (array of CompileResult with file, success, errors, warnings)") }, ["file"] @@ -222,6 +225,7 @@ def handle_tools_list(id_json : string) : string { "file" => PropertySchema(_type = "string", description = "Path to the .das test file"), "timeout" => PropertySchema(_type = "string", description = "Timeout in seconds (default: 120). Process tree is killed if exceeded"), "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP, "json" => PropertySchema(_type = "string", description = "If 'true', return structured JSON (TestSummary with file, tests, total, passed, failed, errors, skipped, success, time)"), "failures_only" => PropertySchema(_type = "string", description = "If 'true', only show failed tests in output") }, @@ -243,7 +247,8 @@ def handle_tools_list(id_json : string) : string { "code" => PropertySchema(_type = "string", description = "Inline daslang code to run (written to temp file and executed)"), "timeout" => PropertySchema(_type = "string", description = "Timeout in seconds (default: 30). Process tree is killed if exceeded"), "track_allocations" => PropertySchema(_type = "string", description = "If 'true', enable heap allocation tracking and append a heap report at exit (shows where each allocation came from)"), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, [] )) @@ -253,7 +258,8 @@ def handle_tools_list(id_json : string) : string { { "expression" => PropertySchema(_type = "string", description = "daslang expression to evaluate (e.g. 'to_float(42) + 1.0', 'length(\"hello\")')"), "require" => PropertySchema(_type = "string", description = "Comma-separated module imports (e.g. 'math', 'strings, daslib/json'). On compile failure, single-token names are retried under 'daslib/'."), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, ["expression"] )) @@ -263,7 +269,8 @@ def handle_tools_list(id_json : string) : string { { "name" => PropertySchema(_type = "string", description = "Type name (e.g. 'TypeDecl', 'ExprCall', 'Type', 'FunctionFlags')"), "module" => PropertySchema(_type = "string", description = "Module to require for the type (e.g. 'ast', 'daslib/json'). Single-token names that fail to resolve are auto-retried as 'daslib/'."), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, ["name"] )) @@ -276,7 +283,8 @@ def handle_tools_list(id_json : string) : string { "function" => PropertySchema(_type = "string", description = "Function name to dump (optional, defaults to all functions)"), "mode" => PropertySchema(_type = "string", description = "Output mode: 'ast' (default) for S-expression, 'source' for post-macro daslang code"), "lineinfo" => PropertySchema(_type = "string", description = "If 'true', include LineInfo (file, line:col spans) on each AST node. Only for mode='ast'"), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, [] )) @@ -296,6 +304,7 @@ def handle_tools_list(id_json : string) : string { "kind" => PropertySchema(_type = "string", description = "Limit to kind: 'function', 'generic', 'struct', 'handled', 'field', 'enum', 'global'"), "file" => PropertySchema(_type = "string", description = "Optional .das file - if provided, searches all modules loaded by that file (including daslib)"), "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP, "with_cpp_source" => PropertySchema(_type = "string", description = "If 'true', for each result with a C++ implementation (builtin functions, handled types), append the resolved C++ source location via the cpp index. Adds ~2s on first call (lazy index build)."), "cpp_dirs" => PropertySchema(_type = "string", description = "Optional comma-separated repo-relative paths to scope the C++ source-redirect lookup to (e.g. 'src/builtin'). Only consulted when with_cpp_source='true'. Empty -> use the cached global C++ index over CPP_SEARCH_DIRS. Non-empty -> fresh scoped scan, no global cache touched. Useful when you know which subtree the symbol lives in and want to skip the multi-hundred-file index build.") }, @@ -307,6 +316,7 @@ def handle_tools_list(id_json : string) : string { { "file" => PropertySchema(_type = "string", description = "Path to the .das file"), "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP, "json" => PropertySchema(_type = "string", description = "If 'true', return structured JSON (RequiresResult with direct and transitive arrays of RequireEntry)") }, ["file"] @@ -329,6 +339,7 @@ def handle_tools_list(id_json : string) : string { "column" => PropertySchema(_type = "string", description = "Column number (1-based)"), "no_opt" => PropertySchema(_type = "string", description = "If 'true', disable optimizations to preserve original AST (constant folding, inlining)"), "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP, "with_cpp_source" => PropertySchema(_type = "string", description = "If 'true' and the resolved symbol is a builtin function or a handled type, append the resolved C++ source location. Adds ~2s on first call (lazy index build)."), "cpp_dirs" => PropertySchema(_type = "string", description = "Optional comma-separated repo-relative paths to scope the C++ source-redirect lookup to (e.g. 'src/builtin'). Only consulted when with_cpp_source='true'. Empty -> cached global C++ index. Non-empty -> fresh scoped scan, no global cache touched.") }, @@ -342,7 +353,8 @@ def handle_tools_list(id_json : string) : string { "line" => PropertySchema(_type = "string", description = "Line number (1-based)"), "column" => PropertySchema(_type = "string", description = "Column number (1-based)"), "no_opt" => PropertySchema(_type = "string", description = "If 'true', disable optimizations to preserve original AST (constant folding, inlining)"), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, ["file", "line", "column"] )) @@ -355,7 +367,8 @@ def handle_tools_list(id_json : string) : string { "column" => PropertySchema(_type = "string", description = "Column number (1-based)"), "scope" => PropertySchema(_type = "string", description = "Search scope: 'file' (default) for current file only, 'all' for all loaded modules"), "no_opt" => PropertySchema(_type = "string", description = "If 'true', disable optimizations to preserve original AST (constant folding, inlining)"), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, ["file", "line", "column"] )) @@ -365,7 +378,8 @@ def handle_tools_list(id_json : string) : string { { "file" => PropertySchema(_type = "string", description = "Path to the .das file to compile"), "function" => PropertySchema(_type = "string", description = "Limit output to a specific function name (optional)"), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, ["file"] )) @@ -377,7 +391,8 @@ def handle_tools_list(id_json : string) : string { "filter" => PropertySchema(_type = "string", description = "Substring filter on names (e.g. 'for_each', 'json')"), "section" => PropertySchema(_type = "string", description = "Limit to section: 'functions', 'generics', 'operators', 'structs', 'handled', 'enums', 'globals', 'annotations'"), "compact" => PropertySchema(_type = "string", description = "Set to 'true' for compact output: function signatures show types only (no arg names), structs/enums omit fields/values. Use with large modules like 'ast', then drill down with describe_type or filter"), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, ["module"] )) @@ -451,7 +466,8 @@ def handle_tools_list(id_json : string) : string { { "file" => PropertySchema(_type = "string", description = "Path to the .das file"), "function" => PropertySchema(_type = "string", description = "Function name to extract AOT for. Supports exact name, method name (matches ClassName`method), and generic origin."), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, ["file"] )) @@ -460,7 +476,8 @@ def handle_tools_list(id_json : string) : string { "Run lint and performance checks on daScript file(s). Combines paranoid lint (unused variables, const suggestions, unreachable code, naming, redundant reinterpret casts) with performance lint (PERF001-012: string concat in loops, character_at misuse, push without reserve, unnecessary string/get_ptr conversions, redundant move-return, string(das_string) in strings functions) and style lint (STYLE001-011: pipe/block syntax, rtti is, if true, decl-then-assign). Supports single file, comma-separated list, or glob pattern.", { "file" => PropertySchema(_type = "string", description = "Path to .das file, comma-separated paths, or glob pattern"), - "project" => PROJECT_PROP + "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP }, ["file"] )) @@ -576,6 +593,7 @@ def handle_tools_list(id_json : string) : string { { "file" => PropertySchema(_type = "string", description = "Path to the .das script file to run"), "project" => PROJECT_PROP, + "project_root" => PROJECT_ROOT_PROP, "port" => PropertySchema(_type = "string", description = "Live API port to check (default: 9090)") }, ["file"] @@ -604,43 +622,43 @@ def get_string_arg(args : JsonValue?; name : string) : string { return "" } -def dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project : string) : string { +def dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project, project_root : string) : string { if (tool_name == "compile_check") { - return do_compile_check(arg1, project, arg2 == "true") + return do_compile_check(arg1, project, project_root, arg2 == "true") } elif (tool_name == "list_functions") { - return do_list_functions(arg1, project) + return do_list_functions(arg1, project, project_root) } elif (tool_name == "list_types") { - return do_list_types(arg1, project) + return do_list_types(arg1, project, project_root) } elif (tool_name == "run_test") { - return do_run_test(arg1, project, arg2, arg3 == "true", arg4 == "true") + return do_run_test(arg1, project, project_root, arg2, arg3 == "true", arg4 == "true") } elif (tool_name == "format_file") { return do_format_file(arg1) } elif (tool_name == "run_script") { - return do_run_script(arg1, arg2, arg3, arg4 == "true", project) + return do_run_script(arg1, arg2, arg3, arg4 == "true", project, project_root) } elif (tool_name == "eval_expression") { - return do_eval_expression(arg1, arg2, project) + return do_eval_expression(arg1, arg2, project, project_root) } elif (tool_name == "describe_type") { - return do_describe_type(arg1, arg2, project) + return do_describe_type(arg1, arg2, project, project_root) } elif (tool_name == "ast_dump") { - return do_ast_dump(arg1, arg2, arg3, arg4, arg5, project) + return do_ast_dump(arg1, arg2, arg3, arg4, arg5, project, project_root) } elif (tool_name == "list_requires") { - return do_list_requires(arg1, project, arg2 == "true") + return do_list_requires(arg1, project, project_root, arg2 == "true") } elif (tool_name == "convert_to_gen2") { return do_convert_to_gen2(arg1, arg2 == "true") } elif (tool_name == "goto_definition") { - return do_goto_definition(arg1, arg2, arg3, arg4, project, arg5 == "true", arg6) + return do_goto_definition(arg1, arg2, arg3, arg4, project, project_root, arg5 == "true", arg6) } elif (tool_name == "type_of") { - return do_type_of(arg1, arg2, arg3, arg4, project) + return do_type_of(arg1, arg2, arg3, arg4, project, project_root) } elif (tool_name == "find_references") { - return do_find_references(arg1, arg2, arg3, arg4, arg5, project) + return do_find_references(arg1, arg2, arg3, arg4, arg5, project, project_root) } elif (tool_name == "program_log") { - return do_program_log(arg1, arg2, project) + return do_program_log(arg1, arg2, project, project_root) } elif (tool_name == "list_modules") { return do_list_modules(arg1 == "true") } elif (tool_name == "find_symbol") { - return do_find_symbol(arg1, arg2, arg3, project, arg4 == "true", arg5) + return do_find_symbol(arg1, arg2, arg3, project, project_root, arg4 == "true", arg5) } elif (tool_name == "list_module_api") { - return do_list_module_api(arg1, arg2, arg3, arg4, project) + return do_list_module_api(arg1, arg2, arg3, arg4, project, project_root) } elif (tool_name == "grep_usage") { return do_grep_usage(arg1, arg2, arg3, arg4) } elif (tool_name == "outline") { @@ -654,9 +672,9 @@ def dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project : strin } elif (tool_name == "cpp_goto_definition") { return do_cpp_goto_definition(arg1, arg2, arg3) } elif (tool_name == "aot") { - return do_aot(arg1, arg2, project) + return do_aot(arg1, arg2, project, project_root) } elif (tool_name == "lint") { - return do_lint(arg1, project) + return do_lint(arg1, project, project_root) } elif (tool_name == "detect_duplicates") { return do_detect_duplicates(arg1, arg2, arg3, arg4, arg5, arg6) } elif (tool_name == "export_corpus") { @@ -680,7 +698,7 @@ def dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project : strin } elif (tool_name == "live_shutdown") { return do_live_shutdown(arg1) } elif (tool_name == "live_launch") { - return do_live_launch(arg1, arg2, arg3) + return do_live_launch(arg1, arg2, project_root, arg3) } elif (tool_name == "shutdown") { unsafe { exit(0); } return make_tool_result("shutting down") @@ -689,18 +707,18 @@ def dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project : strin } } -def run_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project : string) : string { +def run_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project, project_root : string) : string { log_tool(tool_name, "{arg1} {arg2} {arg3} {arg4} {arg5} {arg6}") let t0 = get_clock() var result : string // Live tools run on the main thread — they use system() and sleep() which // don't work well from new_thread, and they don't need compilation context if (starts_with(tool_name, "live_")) { - result = dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project) + result = dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project, project_root) } else { with_channel(1) $(ch) { new_thread() <| @() { - let tool_result = dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project) + let tool_result = dispatch_tool(tool_name, arg1, arg2, arg3, arg4, arg5, arg6, project, project_root) ch |> push_clone(ToolMessage(text = tool_result)) ch |> notify_and_release() } @@ -721,6 +739,7 @@ def handle_tools_call(id_json : string; params : JsonValue?) : string { let name = tool_name_val.value as _string var arg1, arg2, arg3, arg4, arg5, arg6 : string let project = get_string_arg(args, "project") + let project_root = get_string_arg(args, "project_root") if (name == "compile_check") { arg1 = get_string_arg(args, "file") if (empty(arg1)) return jsonrpc::error(id_json, -32602, "missing 'file' argument") @@ -908,7 +927,7 @@ def handle_tools_call(id_json : string; params : JsonValue?) : string { } else { return jsonrpc::error(id_json, -32602, "unknown tool: {name}") } - return jsonrpc::response(id_json, run_tool(name, arg1, arg2, arg3, arg4, arg5, arg6, project)) + return jsonrpc::response(id_json, run_tool(name, arg1, arg2, arg3, arg4, arg5, arg6, project, project_root)) } // ── JSON-RPC dispatcher ────────────────────────────────────────────── diff --git a/utils/mcp/subtools/ast_dump.das b/utils/mcp/subtools/ast_dump.das new file mode 100644 index 0000000000..9c886634ba --- /dev/null +++ b/utils/mcp/subtools/ast_dump.das @@ -0,0 +1,88 @@ +options gen2 +options rtti +options indenting = 4 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require ../tools/common.das public +require daslib/ast_boost + +//! Subprocess form of ast_dump (file-based path). Argv (after daslang exe + script path): +//! +//! Prints the make_tool_result envelope to stdout. +//! The expression-based path stays in tools/ast_dump.das because it already +//! spawns its own short-lived subprocess via a generated stub. + +def format_func_body(func; use_source_mode : bool; use_lineinfo : bool = false) : string { + if (use_source_mode) { + return describe_expression(func.body) + } else { + var deFlags = bitfield(0) + if (use_lineinfo) { + deFlags = DebugExpressionFlags.lineInfo + } + return debug_expression(func.body, deFlags) + } +} + +def run_ast_dump_file(file, func_name : string; use_source_mode, use_lineinfo : bool; project : string = "") : string { + let proj_err = validate_project_arg(project) + if (!empty(proj_err)) return make_tool_result(proj_err, true) + return compile_program(file, true, false, project) $(program, issues) { + let thisMod = get_this_module(program) + var result = "" + var found = false + for_each_function(thisMod, "") $(func) { + if (func.flags.builtIn || func.flags._lambda || func.flags.generated + || (!empty(func_name) && func.name != func_name)) return + found = true + result += "def {func.name}" + var has_args = false + for (i in range(length(func.arguments))) { + if (!arg_needs_documenting(func.arguments[i]._type)) continue + if (!has_args) { + result += "(" + has_args = true + } else { + result += "; " + } + result += "{func.arguments[i].name} : {describe(func.arguments[i]._type)}" + } + if (has_args) { + result += ")" + } + if (func.result != null && !func.result.isVoid) { + result += " : {describe(func.result)}" + } + result += "\n" + result += format_func_body(func, use_source_mode, use_lineinfo) + result += "\n\n" + } + if (!found) { + if (!empty(func_name)) return "Function '{func_name}' not found in {file}" + return "No user functions found in {file}" + } + if (!empty(issues)) { + result += "Warnings:\n{issues}" + } + return result + } +} + +[export] +def main { + let raw <- get_command_line_arguments() + let args <- subtool_user_args(raw) + if (length(args) < 5) { + print(make_tool_result("ast_dump subtool: expected 5 args (file, func_name, mode, lineinfo, project), got {length(args)}", true)) + return + } + let file = string(args[0]) + let func_name = string(args[1]) + let mode = string(args[2]) + let lineinfo = string(args[3]) + let project = string(args[4]) + let use_source_mode = mode == "source" + let use_lineinfo = lineinfo == "true" + print(run_ast_dump_file(file, func_name, use_source_mode, use_lineinfo, project)) +} diff --git a/utils/mcp/subtools/find_references.das b/utils/mcp/subtools/find_references.das new file mode 100644 index 0000000000..bf8946302f --- /dev/null +++ b/utils/mcp/subtools/find_references.das @@ -0,0 +1,443 @@ +options gen2 +options rtti +options indenting = 4 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require ../tools/common.das public +require daslib/ast_cursor +require daslib/ast_boost + +//! Subprocess form of find_references. Argv (after daslang exe + script path): +//! +//! Prints the make_tool_result envelope to stdout. + +// A single reference location +struct Reference { + file : string + line : int + column : int + in_function : string // enclosing function name, or "" for global + context : string // short description: "call", "variable", "field", "type ref", "addr", etc. +} + +// What we're looking for +enum RefTargetKind { + none + func + variable + structure + field + enumeration + alias +} + +struct RefTarget { + kind : RefTargetKind + // all pointers stored as void? for identity comparison (avoids const mismatch) + target_ptr : void? + // for fields: struct identity + name + field_struct_ptr : void? + field_name : string + // for aliases: the alias name string + alias_name : string + // display name + name : string +} + +def to_void(p) : void? { + return unsafe(reinterpret p) +} + +// Build a RefTarget from a cursor hit +def identify_target(hit : CursorHit) : RefTarget { + let expr = hit.expr + // ExprCall → function + if (expr is ExprCall) { + let c = expr as ExprCall + if (c.func != null) return RefTarget(kind = RefTargetKind.func, target_ptr = to_void(c.func), name = string(c.func.name)) + } + // ExprAddr → function pointer + if (expr is ExprAddr) { + let a = expr as ExprAddr + if (a.func != null) return RefTarget(kind = RefTargetKind.func, target_ptr = to_void(a.func), name = string(a.func.name)) + } + // ExprInvoke → resolve the callable (first argument) + if (expr is ExprInvoke) { + let inv = expr as ExprInvoke + if (!empty(inv.arguments)) { + let callable = inv.arguments[0] + // first arg is a variable (function pointer parameter/local) + if (callable is ExprVar) { + let v = callable as ExprVar + if (v.variable != null) { + let variable = v.variable + return RefTarget(kind = RefTargetKind.variable, target_ptr = to_void(variable), name = string(variable.name)) + } + } + // first arg is a field access (struct method pointer) + if (callable is ExprField) { + let f = callable as ExprField + if (f.value != null && f.value._type != null) { + var vt = f.value._type + if (vt.structType == null && vt.firstType != null) { + vt = vt.firstType + } + if (vt.structType != null) return RefTarget( + kind = RefTargetKind.field, + field_struct_ptr = to_void(vt.structType), + field_name = string(f.name), + name = "{vt.structType.name}.{f.name}" + ) + } + } + } + } + // ExprVar → variable + if (expr is ExprVar) { + let v = expr as ExprVar + if (v.variable != null) { + let variable = v.variable + return RefTarget(kind = RefTargetKind.variable, target_ptr = to_void(variable), name = string(variable.name)) + } + } + // ExprField / ExprSafeField → field + if (expr is ExprField || expr is ExprSafeField) { + var fname : string + var value_type : TypeDecl const? + if (expr is ExprField) { + let f = expr as ExprField + fname = string(f.name) + if (f.value != null && f.value._type != null) { + value_type = f.value._type + } + } else { + let f = expr as ExprSafeField + fname = string(f.name) + if (f.value != null && f.value._type != null) { + value_type = f.value._type + } + } + // unwrap pointer types (Foo? → Foo) + if (value_type != null && value_type.structType == null && value_type.firstType != null) { + value_type = value_type.firstType + } + if (value_type != null && value_type.structType != null) return RefTarget( + kind = RefTargetKind.field, + field_struct_ptr = to_void(value_type.structType), + field_name = fname, + name = "{value_type.structType.name}.{fname}" + ) + } + // ExprConstEnumeration → enum + if (expr is ExprConstEnumeration) { + let e = expr as ExprConstEnumeration + if (e.enumType != null) return RefTarget(kind = RefTargetKind.enumeration, target_ptr = to_void(e.enumType), name = string(e.enumType.name)) + } + // ExprConstBitfield → alias (bitfield type) + if (expr is ExprConstBitfield) { + if (expr._type != null) { + let td = expr._type + if (!empty(td.alias)) return RefTarget(kind = RefTargetKind.alias, alias_name = string(td.alias), name = string(td.alias)) + } + } + // Type alias reference (typedef, bitfield, variant, tuple) + if (expr._type != null) { + let td = expr._type + if (!empty(td.alias)) return RefTarget(kind = RefTargetKind.alias, alias_name = string(td.alias), name = string(td.alias)) + } + // Fallback: type reference + if (expr._type != null) { + let td = expr._type + if (td.structType != null) return RefTarget(kind = RefTargetKind.structure, target_ptr = to_void(td.structType), name = string(td.structType.name)) + if (td.enumType != null) return RefTarget(kind = RefTargetKind.enumeration, target_ptr = to_void(td.enumType), name = string(td.enumType.name)) + } + return RefTarget() +} + +// Visitor that collects all references to a given target +class RefVisitor : AstVisitor { + target : RefTarget + refs : array + currentFunc : Function const? + file_filter : string // if non-empty, only collect refs from this file + + def RefVisitor(t : RefTarget; file_flt : string) { + target = t + currentFunc = null + file_filter = file_flt + } + + def add_ref(at : LineInfo; context : string) : void { + if (int(at.line) == 0) return + // file filter + if (!empty(file_filter) && at.fileInfo != null) { + let fname = string(at.fileInfo.name) + if (find(fname, file_filter) < 0) return + } + refs |> emplace(Reference( + file = at.fileInfo != null ? string(at.fileInfo.name) : "", + line = int(at.line), + column = int(at.column), + in_function = currentFunc != null ? string(currentFunc.name) : "", + context = context + )) + } + + def override preVisitFunction(fun : FunctionPtr) : void { + currentFunc = fun + } + + def override visitFunction(var fun : FunctionPtr) : FunctionPtr { + currentFunc = null + return <- fun + } + + // ExprCall → function reference + def override preVisitExprCall(expr : ExprCall?) : void { + if (target.kind == RefTargetKind.func) { + if (expr.func != null && to_void(expr.func) == target.target_ptr) { + add_ref(expr.at, "call") + } + } + } + + // ExprAddr → function pointer reference (@@func) + def override preVisitExprAddr(expr : ExprAddr?) : void { + if (target.kind == RefTargetKind.func) { + if (expr.func != null && to_void(expr.func) == target.target_ptr) { + add_ref(expr.at, "addr") + } + } + } + + // ExprVar → variable reference + def override preVisitExprVar(expr : ExprVar?) : void { + if (target.kind == RefTargetKind.variable) { + if (expr.variable != null) { + let vptr = expr.variable + if (to_void(vptr) == target.target_ptr) { + add_ref(expr.at, "variable") + } + } + } + } + + // ExprField → field reference + def override preVisitExprField(expr : ExprField?) : void { + if (target.kind == RefTargetKind.field) { + if (expr.name == target.field_name && expr.value != null && expr.value._type != null) { + var vt = expr.value._type + if (vt.structType == null && vt.firstType != null) { + vt = vt.firstType + } + if (vt.structType != null && to_void(vt.structType) == target.field_struct_ptr) { + add_ref(expr.atField, "field") + } + } + } + } + + // ExprSafeField → field reference (?.field) + def override preVisitExprSafeField(expr : ExprSafeField?) : void { + if (target.kind == RefTargetKind.field) { + if (expr.name == target.field_name && expr.value != null && expr.value._type != null) { + var vt = expr.value._type + if (vt.structType == null && vt.firstType != null) { + vt = vt.firstType + } + if (vt.structType != null && to_void(vt.structType) == target.field_struct_ptr) { + add_ref(expr.atField, "safe field") + } + } + } + } + + // ExprConstEnumeration → enum value reference + def override preVisitExprConstEnumeration(expr : ExprConstEnumeration?) : void { + if (target.kind == RefTargetKind.enumeration) { + if (expr.enumType != null && to_void(expr.enumType) == target.target_ptr) { + add_ref(expr.at, "enum value") + } + } + } + + // ExprConstBitfield → bitfield value reference + def override preVisitExprConstBitfield(expr : ExprConstBitfield?) : void { + if (target.kind == RefTargetKind.alias) { + if (expr._type != null && !empty(expr._type.alias) && expr._type.alias == target.alias_name) { + add_ref(expr.at, "bitfield value") + } + } + } + + // TypeDecl → struct/enum/alias type references (in variable types, return types, etc.) + def override preVisitTypeDecl(typ : TypeDeclPtr) : void { + let td = typ + if (target.kind == RefTargetKind.structure && td.structType != null) { + if (to_void(td.structType) == target.target_ptr) { + add_ref(td.at, "type ref") + } + } + if (target.kind == RefTargetKind.enumeration && td.enumType != null) { + if (to_void(td.enumType) == target.target_ptr) { + add_ref(td.at, "type ref") + } + } + if (target.kind == RefTargetKind.alias && !empty(td.alias)) { + if (td.alias == target.alias_name) { + add_ref(td.at, "alias ref") + } + } + } +} + +def format_references(refs : array; target : RefTarget) : string { + return build_string() $(var w) { + write(w, "Found {length(refs)} reference(s) to {target.kind} '{target.name}':\n") + for (r in refs) { + write(w, " {r.file}:{r.line}:{r.column}") + if (!empty(r.in_function)) { + write(w, " in {r.in_function}") + } + write(w, " [{r.context}]\n") + } + } +} + +def private fileinfo_name_contains(name : das_string; needle : string) : bool { + var hit = false + peek(name) $(s) { + hit = find(s, needle) >= 0 + } + return hit +} + +// Try to identify a target from declarations at the cursor line +// (function defs, struct defs, enum defs — not expressions) +def identify_declaration_target(program : smart_ptr; file : string; line : int) : RefTarget { + let thisMod = get_this_module(program) + var result : RefTarget + // check function declarations + ast::for_each_function(thisMod, "") $(var func : FunctionPtr) { + if (result.kind != RefTargetKind.none) return + let f = func + if (int(f.at.line) == line && f.at.fileInfo != null) { + if (fileinfo_name_contains(f.at.fileInfo.name, file)) { + result = RefTarget(kind = RefTargetKind.func, target_ptr = to_void(f), name = string(f.name)) + } + } + } + if (result.kind != RefTargetKind.none) return result + // check structure declarations + ast::for_each_structure(thisMod) $(var st : StructurePtr) { + if (result.kind != RefTargetKind.none) return + let s = st + if (int(s.at.line) == line && s.at.fileInfo != null) { + if (fileinfo_name_contains(s.at.fileInfo.name, file)) { + result = RefTarget(kind = RefTargetKind.structure, target_ptr = to_void(s), name = string(s.name)) + } + } + } + if (result.kind != RefTargetKind.none) return result + // check enumeration declarations + ast::for_each_enumeration(thisMod) $(var en : EnumerationPtr) { + if (result.kind != RefTargetKind.none) return + let e = en + if (int(e.at.line) == line && e.at.fileInfo != null) { + if (fileinfo_name_contains(e.at.fileInfo.name, file)) { + result = RefTarget(kind = RefTargetKind.enumeration, target_ptr = to_void(e), name = string(e.name)) + } + } + } + if (result.kind != RefTargetKind.none) return result + // check typedef declarations (typedef, bitfield, variant, tuple) + ast::for_each_typedef(thisMod) $(name : string#; var value : TypeDeclPtr) { + if (result.kind != RefTargetKind.none) return + let td = value + if (int(td.at.line) == line && td.at.fileInfo != null) { + if (fileinfo_name_contains(td.at.fileInfo.name, file)) { + result = RefTarget(kind = RefTargetKind.alias, alias_name = string(name), name = string(name)) + } + } + } + if (result.kind != RefTargetKind.none) return result + // check global variable declarations + ast::for_each_global(thisMod) $(var value : VariablePtr) { + if (result.kind != RefTargetKind.none) return + let v = value + if (int(v.at.line) == line && v.at.fileInfo != null) { + if (fileinfo_name_contains(v.at.fileInfo.name, file)) { + result = RefTarget(kind = RefTargetKind.variable, target_ptr = to_void(v), name = string(v.name)) + } + } + } + return result +} + +def run_find_references(file : string; line_str, col_str, scope_str, no_opt_str : string; project : string = "") : string { + let proj_err = validate_project_arg(project) + if (!empty(proj_err)) return make_tool_result(proj_err, true) + let line = to_int(line_str) + let col = to_int(col_str) + if (line <= 0 || col <= 0) return make_tool_result("Invalid line or column (must be >= 1)", true) + let all_modules = scope_str == "all" + let no_opt = no_opt_str == "true" + return compile_program(file, true, no_opt, project) $(program; issues) { + // Step 1: identify the target from cursor (expression-based) + var target : RefTarget + var hits <- find_at_cursor(program, file, line, col) + if (!empty(hits)) { + for (hit in hits) { + target = identify_target(hit) + if (target.kind != RefTargetKind.none) break + } + } + // Step 1b: if no expression found, try declaration-based lookup + if (target.kind == RefTargetKind.none) { + target = identify_declaration_target(program, file, line) + } + if (target.kind == RefTargetKind.none) { + if (empty(hits)) return "No expression or declaration found at {file}:{line}:{col}" + return build_string() $(var w) { + write(w, "Could not identify a referenceable symbol at {file}:{line}:{col}\n") + write(w, "Found expressions:\n") + for (hit in hits) { + write(w, " {describe(hit)}\n") + } + } + } + // Step 2: walk AST to find all references + let file_filter = all_modules ? "" : file + var visitor = new RefVisitor(target, file_filter) + make_visitor(*visitor) $(adapter) { + if (all_modules) { + visit_modules(program, adapter) + } else { + visit(program, adapter) + } + } + let result = format_references(visitor.refs, target) + unsafe { + delete visitor + } + return result + } +} + +[export] +def main { + let raw <- get_command_line_arguments() + let args <- subtool_user_args(raw) + if (length(args) < 6) { + print(make_tool_result("find_references subtool: expected 6 args (file, line, col, scope, no_opt, project), got {length(args)}", true)) + return + } + let file = string(args[0]) + let line_str = string(args[1]) + let col_str = string(args[2]) + let scope_str = string(args[3]) + let no_opt_str = string(args[4]) + let project = string(args[5]) + print(run_find_references(file, line_str, col_str, scope_str, no_opt_str, project)) +} diff --git a/utils/mcp/subtools/goto_definition.das b/utils/mcp/subtools/goto_definition.das new file mode 100644 index 0000000000..08625e4991 --- /dev/null +++ b/utils/mcp/subtools/goto_definition.das @@ -0,0 +1,327 @@ +options gen2 +options rtti +options indenting = 4 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require ../tools/common.das public +require ../tools/cpp_common.das public +require daslib/ast_cursor +require daslib/ast_boost +require math + +//! Subprocess form of goto_definition. Argv (after daslang exe + script path): +//! +//! Prints the make_tool_result envelope to stdout. + +// Result of resolving a definition location +struct DefinitionResult { + found : bool + kind : string // "variable", "function", "field", "struct", "enum", "builtin" + name : string + file : string + line : int + column : int + snippet : string // source lines around the definition + cppName : string // C++ symbol name for builtin functions and handled types. +} + +// Read a few lines from a file around a given line number +def read_source_snippet(file_path : string; line : int; context_lines : int = 3) : string { + if (empty(file_path) || line <= 0) return "" + var f = fopen(file_path, "r") + if (f == null) return "" + var lines : array + while (!feof(f)) { + var ln = fgets(f) + // strip trailing newline/cr + while (!empty(ln)) { + let last_ch = character_at(ln, length(ln) - 1) // nolint:PERF003 + if (last_ch == '\n' || last_ch == '\r') { + ln = slice(ln, 0, length(ln) - 1) + } else { + break + } + } + lines |> push(ln) + } + fclose(f) + if (empty(lines)) return "" + let first = max(0, line - 1 - context_lines) + let last = min(length(lines) - 1, line - 1 + context_lines) + return build_string() $(var w) { + for (idx in range(first, last + 1)) { + write(w, "{idx + 1}: {lines[idx]}\n") + } + } +} + +// Get file path from LineInfo +def lineinfo_file(at : LineInfo) : string { + if (at.fileInfo != null) return string(at.fileInfo.name) + return "" +} + +// Resolve definition from a CursorHit +def resolve_definition(hit : CursorHit) : DefinitionResult { + let expr = hit.expr + // ExprVar → variable declaration + if (expr is ExprVar) { + let v = expr as ExprVar + if (v.variable != null) { + let variable = v.variable + let file = lineinfo_file(variable.at) + let line = int(variable.at.line) + return DefinitionResult( + found = true, + kind = "variable", + name = string(variable.name), + file = file, + line = line, + column = int(variable.at.column), + snippet = read_source_snippet(file, line) + ) + } + } + // ExprCall / ExprCallFunc → function declaration + if (expr is ExprCall) { + let c = expr as ExprCall + if (c.func != null) { + if (c.func.flags.builtIn) { + var cppName : string + let bfn = c.func as BuiltInFunction + if (bfn != null) { + cppName = string(bfn.cppName) + } + return DefinitionResult( + found = true, + kind = "builtin", + name = string(c.func.name), + snippet = build_string() $(var w) { + write(w, "builtin def {c.func.name}") + write_func_signature(w, *c.func) + }, + cppName = cppName + ) + } + let file = lineinfo_file(c.func.at) + let line = int(c.func.at.line) + return DefinitionResult( + found = true, + kind = "function", + name = string(c.func.name), + file = file, + line = line, + column = int(c.func.at.column), + snippet = read_source_snippet(file, line) + ) + } + } + // ExprField / ExprSafeField → struct field declaration + if (expr is ExprField || expr is ExprSafeField) { + var field_name : string + var value_type : TypeDecl const? + if (expr is ExprField) { + let f = expr as ExprField + field_name = string(f.name) + if (f.value != null && f.value._type != null) { + value_type = f.value._type + } + } else { + let f = expr as ExprSafeField + field_name = string(f.name) + if (f.value != null && f.value._type != null) { + value_type = f.value._type + } + } + // unwrap pointer types (Foo? → Foo) + if (value_type != null && value_type.structType == null && value_type.firstType != null) { + value_type = value_type.firstType + } + if (value_type != null && value_type.structType != null) { + let st = value_type.structType + for (fld in st.fields) { + if (fld.name == field_name) { + let file = lineinfo_file(fld.at) + let line = int(fld.at.line) + return DefinitionResult( + found = true, + kind = "field", + name = "{st.name}.{field_name}", + file = file, + line = line, + column = int(fld.at.column), + snippet = read_source_snippet(file, line) + ) + } + } + } + } + // ExprConstEnumeration → enum declaration + if (expr is ExprConstEnumeration) { + let e = expr as ExprConstEnumeration + if (e.enumType != null) { + let en = e.enumType + let file = lineinfo_file(en.at) + let line = int(en.at.line) + return DefinitionResult( + found = true, + kind = "enum", + name = "{en.name}.{e.value}", + file = file, + line = line, + column = int(en.at.column), + snippet = read_source_snippet(file, line) + ) + } + } + // Type alias → resolve to typedef declaration + if (expr._type != null) { + let td = expr._type + if (!empty(td.alias)) { + let file = lineinfo_file(td.at) + let line = int(td.at.line) + return DefinitionResult( + found = true, + kind = "typedef", + name = string(td.alias), + file = file, + line = line, + column = int(td.at.column), + snippet = read_source_snippet(file, line) + ) + } + } + // Fallback: resolve type reference from _type + if (expr._type != null) { + let td = expr._type + if (td.isHandle && td.annotation != null) { + let ann = td.annotation + return DefinitionResult( + found = true, + kind = "handled", + name = string(ann.name), + cppName = string(ann.cppName) + ) + } + if (td.structType != null) { + let st = td.structType + let file = lineinfo_file(st.at) + let line = int(st.at.line) + return DefinitionResult( + found = true, + kind = "struct", + name = string(st.name), + file = file, + line = line, + column = int(st.at.column), + snippet = read_source_snippet(file, line) + ) + } + if (td.enumType != null) { + let en = td.enumType + let file = lineinfo_file(en.at) + let line = int(en.at.line) + return DefinitionResult( + found = true, + kind = "enum", + name = string(en.name), + file = file, + line = line, + column = int(en.at.column), + snippet = read_source_snippet(file, line) + ) + } + } + return DefinitionResult() +} + +def format_result(defn : DefinitionResult; hit : CursorHit; with_cpp_source : bool; cpp_dirs : array) : string { + return build_string() $(var w) { + write(w, "Symbol: {defn.name}\n") + write(w, "Kind: {defn.kind}\n") + if (!empty(defn.file)) { + write(w, "File: {defn.file}\n") + write(w, "Line: {defn.line}\n") + write(w, "Column: {defn.column}\n") + } + if (!empty(defn.cppName)) { + write(w, "C++: {defn.cppName}") + if (with_cpp_source) { + var match : CppMatch + let found = empty(cpp_dirs) ? cpp_lookup_by_name(defn.cppName, match) : cpp_lookup_by_name_scoped(defn.cppName, cpp_dirs, match) + if (found) { + write(w, " ({match.file}:{match.line})") + } elif (empty(cpp_dirs)) { + let why = cpp_index_status() + if (!empty(why)) { + write(w, " (index unavailable: {why})") + } else { + write(w, " (not located)") + } + } else { + let dirs_csv = join(cpp_dirs, ",") + write(w, " (not located in {dirs_csv})") + } + } + write(w, "\n") + } + if (hit.func != null) { + write(w, "In function: {hit.func.name}\n") + } + if (!empty(defn.snippet)) { + write(w, "\n{defn.snippet}") + } + } +} + +def run_goto_definition(file : string; line_str, col_str, no_opt_str, with_cpp_source_str, cpp_dirs, project : string) : string { + let proj_err = validate_project_arg(project) + if (!empty(proj_err)) return make_tool_result(proj_err, true) + let line = to_int(line_str) + let col = to_int(col_str) + if (line <= 0 || col <= 0) return make_tool_result("Invalid line or column (must be >= 1)", true) + let no_opt = no_opt_str == "true" + let with_cpp_source = with_cpp_source_str == "true" + var cpp_dirs_arr : array + if (!empty(cpp_dirs)) { + for (part in split(cpp_dirs, ",")) { + let trimmed = strip(part) + if (!empty(trimmed)) { + cpp_dirs_arr |> push(trimmed) + } + } + } + return compile_program(file, true, no_opt, project) $(program; issues) { + var hits <- find_at_cursor(program, file, line, col) + if (empty(hits)) return "No expression found at {file}:{line}:{col}" + for (hit in hits) { + let defn = resolve_definition(hit) + if (defn.found) return format_result(defn, hit, with_cpp_source, cpp_dirs_arr) + } + return build_string() $(var w) { + write(w, "Found expression at cursor but could not resolve definition:\n") + for (hit in hits) { + write(w, " {describe(hit)}\n") + } + } + } +} + +[export] +def main { + let raw <- get_command_line_arguments() + let args <- subtool_user_args(raw) + if (length(args) < 7) { + print(make_tool_result("goto_definition subtool: expected 7 args (file, line, col, no_opt, with_cpp_source, cpp_dirs, project), got {length(args)}", true)) + return + } + let file = string(args[0]) + let line_str = string(args[1]) + let col_str = string(args[2]) + let no_opt_str = string(args[3]) + let with_cpp_source_str = string(args[4]) + let cpp_dirs = string(args[5]) + let project = string(args[6]) + print(run_goto_definition(file, line_str, col_str, no_opt_str, with_cpp_source_str, cpp_dirs, project)) +} diff --git a/utils/mcp/subtools/list_requires.das b/utils/mcp/subtools/list_requires.das new file mode 100644 index 0000000000..a78c2dd0b4 --- /dev/null +++ b/utils/mcp/subtools/list_requires.das @@ -0,0 +1,128 @@ +options gen2 +options rtti +options indenting = 4 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require ../tools/common.das public +require daslib/json_boost + +//! Subprocess form of list_requires. Argv (after daslang exe + script path): +//! (json = "true" or "false") +//! Prints the make_tool_result envelope to stdout. + +struct RequireEntry { + name : string + is_public : bool + source : string // "builtin" or file path +} + +struct RequiresResult { + direct : array + transitive : array +} + +def module_kind(mod : Module?) : string { + if (mod == null) return "missing" + if (!empty(mod.fileName)) return string(mod.fileName) + return "builtin" +} + +def run_list_requires(file : string; project : string = ""; json : bool = false) : string { + let proj_err = validate_project_arg(project) + if (!empty(proj_err)) return make_tool_result(proj_err, true) + return compile_and_simulate(file, project) $(program; issues) { + // direct requires from allRequireDecl + var direct_text : array + var direct_entries : array + var direct_modules : array + for_each_require_declaration(program) $(var mod : Module?; mod_name : string#; mod_file : string#; var is_public : bool; at : LineInfo&) { + let kind = module_kind(mod) + if (!json) { + let pub_marker = is_public ? " public" : "" + direct_text |> push(" require {mod_name}{pub_marker} // {kind}") + } else { + direct_entries |> emplace(RequireEntry(name = string(mod_name), is_public = is_public, source = kind)) + } + direct_modules |> push(string(mod_name)) + } + // transitive: walk dependencies of the main module, skip "$" (always implicit) + var transitive_text : array + var transitive_entries : array + let thisMod = get_this_module(program) + var visited : table + for (dm in direct_modules) { + visited |> insert(dm, true) + } + visited |> insert("$", true) + var queue : array + module_for_each_dependency(thisMod) $(var dep : Module?; var is_pub : bool) { + let dep_name = string(dep.name) + if (!key_exists(visited, dep_name)) { + visited |> insert(dep_name, true) + queue |> push(dep) + } + } + var idx = 0 + while (idx < length(queue)) { + let mod = queue[idx] + idx++ + let mod_name = string(mod.name) + let kind = module_kind(mod) + if (!json) { + transitive_text |> push(" {mod_name} // {kind}") + } else { + transitive_entries |> emplace(RequireEntry(name = mod_name, source = kind)) + } + module_for_each_dependency(mod) $(var dep : Module?; var is_pub : bool) { + let dep_name = string(dep.name) + if (!key_exists(visited, dep_name)) { + visited |> insert(dep_name, true) + queue |> push(dep) + } + } + } + if (json) { + sort(transitive_entries) $(a, b : RequireEntry) { + return a.name < b.name + } + let result = RequiresResult(direct <- direct_entries, transitive <- transitive_entries) + return sprint_json(result, false) + } + sort(transitive_text) + return build_string() $(var w) { + if (!empty(direct_text)) { + write(w, "Direct requires:\n") + for (d in direct_text) { + write(w, "{d}\n") + } + } + if (!empty(transitive_text)) { + if (!empty(direct_text)) { + write(w, "\n") + } + write(w, "Transitive dependencies ({length(transitive_text)}):\n") + for (t in transitive_text) { + write(w, "{t}\n") + } + } + if (empty(direct_text) && empty(transitive_text)) { + write(w, "No requires found.\n") + } + } + } +} + +[export] +def main { + let raw <- get_command_line_arguments() + let args <- subtool_user_args(raw) + if (length(args) < 3) { + print(make_tool_result("list_requires subtool: expected 3 args (file, project, json), got {length(args)}", true)) + return + } + let file = string(args[0]) + let project = string(args[1]) + let json = string(args[2]) == "true" + print(run_list_requires(file, project, json)) +} diff --git a/utils/mcp/subtools/program_log.das b/utils/mcp/subtools/program_log.das new file mode 100644 index 0000000000..ef71317c68 --- /dev/null +++ b/utils/mcp/subtools/program_log.das @@ -0,0 +1,54 @@ +options gen2 +options rtti +options indenting = 4 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require ../tools/common.das public +require daslib/ast_boost + +//! Subprocess form of program_log. Argv (after daslang exe + script path): +//! +//! Prints the make_tool_result envelope to stdout. + +def run_program_log(file, func_name : string; project : string = "") : string { + let proj_err = validate_project_arg(project) + if (!empty(proj_err)) return make_tool_result(proj_err, true) + // filter to specific function — use ast_dump source mode approach + if (!empty(func_name)) return compile_and_simulate(file, project) $(program, issues) { + let thisMod = get_this_module(program) + var result = "" + var found = false + for_each_function(thisMod, "") $(func) { + if (func.flags.builtIn || func.flags._lambda || func.flags.generated + || func.name != func_name) return + found = true + result += describe_function(func) + result += "\n" + } + if (!found) return "Function '{func_name}' not found in {file}" + return result + } + // full program log — use C++ Program::operator<< + return compile_and_simulate(file, project) $(program, issues) { + var result = describe_program(program) + if (!empty(issues)) { + result += "\nWarnings:\n{issues}" + } + return result + } +} + +[export] +def main { + let raw <- get_command_line_arguments() + let args <- subtool_user_args(raw) + if (length(args) < 3) { + print(make_tool_result("program_log subtool: expected 3 args (file, func_name, project), got {length(args)}", true)) + return + } + let file = string(args[0]) + let func_name = string(args[1]) + let project = string(args[2]) + print(run_program_log(file, func_name, project)) +} diff --git a/utils/mcp/subtools/type_of.das b/utils/mcp/subtools/type_of.das new file mode 100644 index 0000000000..61828d17e7 --- /dev/null +++ b/utils/mcp/subtools/type_of.das @@ -0,0 +1,77 @@ +options gen2 +options rtti +options indenting = 4 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require ../tools/common.das public +require daslib/ast_cursor +require daslib/ast_boost + +//! Subprocess form of type_of. Argv (after daslang exe + script path): +//! +//! Prints the make_tool_result envelope to stdout. + +def lineinfo_file(at : LineInfo) : string { + if (at.fileInfo != null) return string(at.fileInfo.name) + return "" +} + +def run_type_of(file : string; line_str, col_str, no_opt_str, project : string) : string { + let proj_err = validate_project_arg(project) + if (!empty(proj_err)) return make_tool_result(proj_err, true) + let line = to_int(line_str) + let col = to_int(col_str) + if (line <= 0 || col <= 0) return make_tool_result("Invalid line or column (must be >= 1)", true) + let no_opt = no_opt_str == "true" + return compile_program(file, true, no_opt, project) $(program; issues) { + var hits <- find_at_cursor(program, file, line, col) + if (empty(hits)) return "No expression found at {file}:{line}:{col}" + return build_string() $(var w) { + for (hit in hits) { + write(w, "{hit.rtti}") + if (!empty(hit.name)) { + write(w, "({hit.name})") + } + if (hit.expr != null && hit.expr._type != null) { + write(w, " : {describe(hit.expr._type)}") + // add extra detail for struct/enum types + let td = hit.expr._type + if (td.structType != null) { + let st = td.structType + let st_file = lineinfo_file(st.at) + if (!empty(st_file)) { + write(w, " // {st_file}:{int(st.at.line)}") + } + } + if (td.enumType != null) { + let en = td.enumType + let en_file = lineinfo_file(en.at) + if (!empty(en_file)) { + write(w, " // {en_file}:{int(en.at.line)}") + } + } + } else { + write(w, " : (no type)") + } + write(w, "\n") + } + } + } +} + +[export] +def main { + let raw <- get_command_line_arguments() + let args <- subtool_user_args(raw) + if (length(args) < 5) { + print(make_tool_result("type_of subtool: expected 5 args (file, line, col, no_opt, project), got {length(args)}", true)) + return + } + let file = string(args[0]) + let line_str = string(args[1]) + let col_str = string(args[2]) + let no_opt_str = string(args[3]) + let project = string(args[4]) + print(run_type_of(file, line_str, col_str, no_opt_str, project)) +} diff --git a/utils/mcp/test_tools.das b/utils/mcp/test_tools.das index 8949200a11..096edadd87 100644 --- a/utils/mcp/test_tools.das +++ b/utils/mcp/test_tools.das @@ -34,6 +34,8 @@ require tools/cpp_outline public require tools/cpp_goto_definition public require tools/aot public require tools/lint_tool public +require tools/program_log public +require tools/live public require tools/detect_duplicates public require tools/export_corpus public @@ -856,7 +858,7 @@ def test_find_references_function(t : T?) { var text : string var is_error = false // cursor on add_points call at line 46 col 14 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "46", "14", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "46", "14", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "func") >= 0, "should identify as function") t |> success(find(text, "add_points") >= 0, "should name add_points") @@ -871,7 +873,7 @@ def test_find_references_variable(t : T?) { var text : string var is_error = false // cursor on p1 variable usage at line 46 col 25 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "46", "25", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "46", "25", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "variable") >= 0, "should identify as variable") t |> success(find(text, "p1") >= 0, "should name p1") @@ -885,7 +887,7 @@ def test_find_references_field(t : T?) { var text : string var is_error = false // cursor on a.x field at line 15 col 22 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "15", "22", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "15", "22", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "Point.x") >= 0, "should identify as Point.x field") t |> success(find(text, "[field]") >= 0, "should find field references") @@ -900,7 +902,7 @@ def test_find_references_addr(t : T?) { var text : string var is_error = false // cursor on @@add_points at line 48 col 30 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "48", "30", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "48", "30", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "func") >= 0, "should identify as function") t |> success(find(text, "add_points") >= 0, "should name add_points") @@ -915,7 +917,7 @@ def test_find_references_invoke(t : T?) { var text : string var is_error = false // cursor on invoke(f, a, b) at line 23 col 12 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "23", "12", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "23", "12", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "variable") >= 0, "should identify as variable") t |> success(find(text, "'f'") >= 0, "should name f") @@ -928,7 +930,7 @@ def test_find_references_from_declaration(t : T?) { var text : string var is_error = false // cursor on 'def add_points' at line 14 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "14", "5", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "14", "5", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "func") >= 0, "should identify as function") t |> success(find(text, "add_points") >= 0, "should name add_points") @@ -938,7 +940,7 @@ def test_find_references_from_declaration(t : T?) { var text : string var is_error = false // cursor on 'struct Point' at line 9 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "9", "1", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "9", "1", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "structure") >= 0, "should identify as structure") t |> success(find(text, "Point") >= 0, "should name Point") @@ -952,7 +954,7 @@ def test_find_references_alias(t : T?) { var text : string var is_error = false // cursor on 'bitfield Flags' declaration at line 26 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "26", "1", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "26", "1", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "alias") >= 0, "should identify as alias") t |> success(find(text, "Flags") >= 0, "should name Flags") @@ -962,7 +964,7 @@ def test_find_references_alias(t : T?) { var text : string var is_error = false // cursor on Value(i = 10) at line 51 col 13 - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "51", "13", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "51", "13", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "alias") >= 0, "should identify as alias") t |> success(find(text, "Value") >= 0, "should name Value") @@ -988,7 +990,7 @@ def test_find_references_no_expression(t : T?) { t |> run("reports no expression at empty position") <| @(t : T?) { var text : string var is_error = false - parse_result(do_find_references(fixture_path("_fixture_refs.das"), "2", "1", "file"), text, is_error) + parse_result(do_find_references(fixture_path("_fixture_refs.das"), "2", "1", "file", ""), text, is_error) t |> success(!is_error, "should not be error") t |> success(find(text, "No expression or declaration found") >= 0, "should report nothing found") } @@ -1385,7 +1387,7 @@ def test_compile_check_json(t : T?) { t |> run("json mode returns CompileResult array") <| @(t : T?) { var text : string var is_error = false - let ok = parse_result(do_compile_check(fixture_path("_fixture_valid.das"), "", true), text, is_error) + let ok = parse_result(do_compile_check(fixture_path("_fixture_valid.das"), "", "", true), text, is_error) t |> success(ok, "parse result JSON") t |> success(!is_error, "should not be error") t |> success(find(text, "\"success\":true") >= 0, "should contain success:true") @@ -1394,7 +1396,7 @@ def test_compile_check_json(t : T?) { t |> run("json mode reports errors") <| @(t : T?) { var text : string var is_error = false - parse_result(do_compile_check(fixture_path("_fixture_error.das"), "", true), text, is_error) + parse_result(do_compile_check(fixture_path("_fixture_error.das"), "", "", true), text, is_error) t |> success(find(text, "\"success\":false") >= 0, "should contain success:false") t |> success(find(text, "\"errors\":") >= 0, "should contain errors field") } @@ -1402,7 +1404,7 @@ def test_compile_check_json(t : T?) { var text : string var is_error = false let batch = fixture_path("_fixture_valid.das") + "," + fixture_path("_fixture_error.das") - parse_result(do_compile_check(batch, "", true), text, is_error) + parse_result(do_compile_check(batch, "", "", true), text, is_error) // Should be a JSON array with 2 elements let arr_start = "[" + "\{" t |> success(find(text, arr_start) >= 0, "should start with array") @@ -1416,7 +1418,7 @@ def test_run_test_json(t : T?) { t |> run("json mode returns TestSummary") <| @(t : T?) { var text : string var is_error = false - let ok = parse_result(do_run_test(fixture_path("_fixture_test.das"), "", "", true), text, is_error) + let ok = parse_result(do_run_test(fixture_path("_fixture_test.das"), "", "", "", true), text, is_error) t |> success(ok, "parse result JSON") // NOTE: popen stdout capture broken when run from dastest on Windows (see wip.das) // t |> success(!is_error, "should not be error") @@ -1445,7 +1447,7 @@ def test_list_requires_json(t : T?) { t |> run("json mode returns RequiresResult") <| @(t : T?) { var text : string var is_error = false - let ok = parse_result(do_list_requires(fixture_path("_fixture_valid.das"), "", true), text, is_error) + let ok = parse_result(do_list_requires(fixture_path("_fixture_valid.das"), "", "", true), text, is_error) t |> success(ok, "parse result JSON") t |> success(!is_error, "should not be error") t |> success(find(text, "\"direct\":") >= 0, "should contain direct field") @@ -2253,7 +2255,7 @@ def test_find_symbol_with_cpp_source(t : T?) { t |> run("redirect off (default) — no '→ cpp:' line") <| @(t : T?) { var text : string var is_error = false - parse_result(do_find_symbol("=print", "", "", "", false), text, is_error) + parse_result(do_find_symbol("=print", "", "", "", "", false), text, is_error) t |> success(!is_error, "should not be error -- {cpp_diag(text)}") t |> success(find(text, "print") >= 0, "should still find print -- {cpp_diag(text)}") t |> success(find(text, "→ cpp:") < 0, "no redirect line when with_cpp_source=false -- {cpp_diag(text)}") @@ -2264,7 +2266,7 @@ def test_find_symbol_with_cpp_source(t : T?) { // cpp_dirs="src/builtin" narrows the C++ scan from full CPP_SEARCH_DIRS // (~947 files) to ~33 files where `print` lives. Drops this test from // ~19s to ~1s. - parse_result(do_find_symbol("=print", "function", "", "", true, "src/builtin"), text, is_error) + parse_result(do_find_symbol("=print", "function", "", "", "", true, "src/builtin"), text, is_error) t |> success(!is_error, "should not be error -- {cpp_diag(text)}") // Either we located print's cpp source OR we report (not located). // Both prove the redirect path fired. @@ -2278,7 +2280,7 @@ def test_goto_definition_with_cpp_source(t : T?) { var text : string var is_error = false // line 34 col 5 is the 'p' of print(...) on `print("{d}\n")` — resolves to a builtin - parse_result(do_goto_definition(fixture_path("_fixture_goto.das"), "34", "5", "", "", false), text, is_error) + parse_result(do_goto_definition(fixture_path("_fixture_goto.das"), "34", "5", "", "", "", false), text, is_error) t |> success(!is_error, "should not be error -- {cpp_diag(text)}") t |> success(find(text, "Kind: builtin") >= 0, "should resolve to a builtin -- {cpp_diag(text)}") } @@ -2286,7 +2288,7 @@ def test_goto_definition_with_cpp_source(t : T?) { var text : string var is_error = false // cpp_dirs="src/builtin" narrows the redirect lookup; drops this from ~17s to ~1s. - parse_result(do_goto_definition(fixture_path("_fixture_goto.das"), "34", "5", "", "", true, "src/builtin"), text, is_error) + parse_result(do_goto_definition(fixture_path("_fixture_goto.das"), "34", "5", "", "", "", true, "src/builtin"), text, is_error) t |> success(!is_error, "should not be error -- {cpp_diag(text)}") t |> success(find(text, "Kind: builtin") >= 0, "should still resolve to a builtin -- {cpp_diag(text)}") t |> success(find(text, "C++:") >= 0, "C++ line should appear when with_cpp_source=true -- {cpp_diag(text)}") @@ -2431,7 +2433,7 @@ def test_run_mcp_subtool_timeout(t : T?) { t |> run("subtool exceeding timeout surfaces 'timed out'") <| @(t : T?) { // Sleep 5s, allow 0.5s — guaranteed timeout. Wall clock cost ~0.5s // (process is killed at the deadline, not after the full sleep). - let raw = run_mcp_subtool("_test_sleep", ["5000"], 0.5) + let raw = run_mcp_subtool("_test_sleep", ["5000"], [timeout_sec = 0.5]) var text : string var is_error = false parse_result(raw, text, is_error) @@ -2455,3 +2457,395 @@ def test_run_mcp_subtool_nonzero_exit(t : T?) { "captured stdout before the panic should be in the error payload -- text={text}") } } + +// ── project_root end-to-end coverage ──────────────────────────────── +// Tier 1: every MCP tool that accepts `project_root` should resolve the +// pretend_mod fixture's `require pretend_mod/greet` chain when the arg is +// set, and fail with `missing prerequisite 'pretend_mod` when it isn't. +// Fixture: utils/mcp/tests/_pretend_root/{consumer.das, modules/pretend_mod/{.das_module, greet.das}}. + +let PROOT_CONSUMER = "_pretend_root/consumer.das" +let PROOT = "_pretend_root" +let PROOT_MISSING = "missing prerequisite 'pretend_mod" + +def assert_project_root_pair(t : T?; without_root, with_root : string) { + var t1 : string; var e1 = false + parse_result(without_root, t1, e1) + t |> success(e1, "without project_root: should be error -- text={t1}") + t |> success(find(t1, PROOT_MISSING) >= 0, + "without project_root: should mention missing prerequisite -- text={t1}") + var t2 : string; var e2 = false + parse_result(with_root, t2, e2) + t |> success(!e2, "with project_root: should NOT be error -- text={t2}") +} + +[test] +def test_project_root_compile_check(t : T?) { + t |> run("compile_check honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_compile_check(fixture_path(PROOT_CONSUMER)), + do_compile_check(fixture_path(PROOT_CONSUMER), "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_lint(t : T?) { + t |> run("lint honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_lint(fixture_path(PROOT_CONSUMER)), + do_lint(fixture_path(PROOT_CONSUMER), "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_aot(t : T?) { + t |> run("aot honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_aot(fixture_path(PROOT_CONSUMER), ""), + do_aot(fixture_path(PROOT_CONSUMER), "", "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_list_functions(t : T?) { + t |> run("list_functions honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_list_functions(fixture_path(PROOT_CONSUMER)), + do_list_functions(fixture_path(PROOT_CONSUMER), "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_list_types(t : T?) { + t |> run("list_types honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_list_types(fixture_path(PROOT_CONSUMER)), + do_list_types(fixture_path(PROOT_CONSUMER), "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_list_requires(t : T?) { + t |> run("list_requires honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_list_requires(fixture_path(PROOT_CONSUMER)), + do_list_requires(fixture_path(PROOT_CONSUMER), "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_program_log(t : T?) { + t |> run("program_log honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_program_log(fixture_path(PROOT_CONSUMER), "main"), + do_program_log(fixture_path(PROOT_CONSUMER), "main", "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_ast_dump(t : T?) { + t |> run("ast_dump honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_ast_dump("", fixture_path(PROOT_CONSUMER), "main", "source", ""), + do_ast_dump("", fixture_path(PROOT_CONSUMER), "main", "source", "", "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_find_references(t : T?) { + t |> run("find_references honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_find_references(fixture_path(PROOT_CONSUMER), "7", "17", "file", ""), + do_find_references(fixture_path(PROOT_CONSUMER), "7", "17", "file", "", "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_goto_definition(t : T?) { + t |> run("goto_definition honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_goto_definition(fixture_path(PROOT_CONSUMER), "7", "17"), + do_goto_definition(fixture_path(PROOT_CONSUMER), "7", "17", "", "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_type_of(t : T?) { + t |> run("type_of honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_type_of(fixture_path(PROOT_CONSUMER), "7", "17"), + do_type_of(fixture_path(PROOT_CONSUMER), "7", "17", "", "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_find_symbol(t : T?) { + t |> run("find_symbol honors project_root") <| @(t : T?) { + assert_project_root_pair(t, + do_find_symbol("hello", "", fixture_path(PROOT_CONSUMER)), + do_find_symbol("hello", "", fixture_path(PROOT_CONSUMER), "", fixture_path(PROOT))) + } +} + +[test] +def test_project_root_list_module_api(t : T?) { + t |> run("list_module_api honors project_root") <| @(t : T?) { + // list_module_api compiles a stub `require `, so the + // missing-prereq error mentions the module key, not 'pretend_mod'. + var t1 : string; var e1 = false + parse_result(do_list_module_api("pretend_mod/greet"), t1, e1) + t |> success(e1, "without project_root: should be error -- text={t1}") + t |> success(find(t1, "missing prerequisite 'pretend_mod/greet") >= 0, + "without project_root: should mention missing prereq -- text={t1}") + var t2 : string; var e2 = false + parse_result(do_list_module_api("pretend_mod/greet", "", "", "", "", fixture_path(PROOT)), t2, e2) + t |> success(!e2, "with project_root: should NOT be error -- text={t2}") + t |> success(find(t2, "hello") >= 0, + "with project_root: should list hello() -- text={t2}") + } +} + +[test] +def test_project_root_describe_type(t : T?) { + t |> run("describe_type honors project_root") <| @(t : T?) { + // describe_type uses module_name in the stub require chain; + // without project_root the require fails to resolve. + var t1 : string; var e1 = false + parse_result(do_describe_type("FooBar", "pretend_mod/greet"), t1, e1) + t |> success(e1, "without project_root: should be error -- text={t1}") + t |> success(find(t1, "missing prerequisite 'pretend_mod/greet") >= 0, + "without project_root: should mention missing prereq -- text={t1}") + // FooBar doesn't exist; tool reports "Type 'FooBar' not found" but is_error=false + // because the compile itself succeeded — that's the project_root flow we're proving. + var t2 : string; var e2 = false + parse_result(do_describe_type("FooBar", "pretend_mod/greet", "", fixture_path(PROOT)), t2, e2) + t |> success(!e2, "with project_root: should NOT be error -- text={t2}") + } +} + +[test] +def test_project_root_eval_expression(t : T?) { + t |> run("eval_expression honors project_root") <| @(t : T?) { + // eval_expression has its own subprocess path (not via run_mcp_subtool), + // so this test exercises that separate argv construction path too. + var t1 : string; var e1 = false + parse_result(do_eval_expression("hello()", "pretend_mod/greet"), t1, e1) + t |> success(e1, "without project_root: should be error -- text={t1}") + t |> success(find(t1, "missing prerequisite 'pretend_mod/greet") >= 0, + "without project_root: should mention missing prereq -- text={t1}") + var t2 : string; var e2 = false + parse_result(do_eval_expression("hello()", "pretend_mod/greet", "", fixture_path(PROOT)), t2, e2) + t |> success(!e2, "with project_root: should NOT be error -- text={t2}") + } +} + +[test] +def test_project_root_run_script(t : T?) { + t |> run("run_script honors project_root") <| @(t : T?) { + // Use INLINE code (not file path). daslang.exe auto-deduces project_root + // from files.front()'s directory (utils/daScript/main.cpp:390); when + // run_script is given a file path, that's the consumer's own directory + // which auto-resolves modules/. Inline code writes a temp stub at + // get_das_root(), whose modules tree doesn't include pretend_mod. + let inline_code = "options gen2\nrequire pretend_mod/greet\n[export]\ndef main() \{\n print(\"\{hello()\}\\n\")\n\}\n" + var t1 : string; var e1 = false + parse_result(do_run_script("", inline_code), t1, e1) + t |> success(e1, "without project_root: should be error -- text={t1}") + t |> success(find(t1, PROOT_MISSING) >= 0, + "without project_root: should mention missing prereq -- text={t1}") + var t2 : string; var e2 = false + parse_result(do_run_script("", inline_code, "", false, "", fixture_path(PROOT)), t2, e2) + t |> success(!e2, "with project_root: should NOT be error -- text={t2}") + } +} + +[test] +def test_project_root_run_test(t : T?) { + t |> run("run_test honors project_root") <| @(t : T?) { + // consumer.das has no [test] funcs, but the compile-failure vs + // compile-succeed gate is what we're testing. + var t1 : string; var e1 = false + parse_result(do_run_test(fixture_path(PROOT_CONSUMER)), t1, e1) + t |> success(e1, "without project_root: should be error -- text={t1}") + t |> success(find(t1, PROOT_MISSING) >= 0, + "without project_root: should mention missing prereq -- text={t1}") + var t2 : string; var e2 = false + parse_result(do_run_test(fixture_path(PROOT_CONSUMER), "", fixture_path(PROOT)), t2, e2) + // No [test] functions in consumer.das, so dastest reports "no tests" + // but the compile succeeded. Some implementations may mark "no tests" + // as is_error=true; assert based on actual observed behavior, not + // is_error. The key check: no missing-prereq message. + t |> success(find(t2, PROOT_MISSING) < 0, + "with project_root: should NOT mention missing prereq -- text={t2}") + } +} + +// ── build_live_script_args unit tests ──────────────────────────────── +// Pins daslang-live script_args composition. Flag-ordering matters because +// when both `-project` and `-project_root` are set, `-project_root` must end +// up to the LEFT (prepended last by build_live_script_args). Slash-replace +// only happens when is_windows=true. + +[test] +def test_live_args_neither(t : T?) { + t |> run("no project, no project_root: just -cwd") <| @(t : T?) { + let s = build_live_script_args("/path/to/foo.das", "", "", false) + t |> equal(s, "-cwd \"/path/to/foo.das\"", "should be just -cwd") + } +} + +[test] +def test_live_args_project_only(t : T?) { + t |> run("project set, project_root empty: -project before -cwd") <| @(t : T?) { + let s = build_live_script_args("/path/foo.das", "/proj/P.das_project", "", false) + t |> equal(s, "-project \"/proj/P.das_project\" -cwd \"/path/foo.das\"", + "should prepend -project before -cwd") + } +} + +[test] +def test_live_args_project_root_only(t : T?) { + t |> run("project_root set, project empty: -project_root before -cwd") <| @(t : T?) { + let s = build_live_script_args("/path/foo.das", "", "/proj_root", false) + t |> equal(s, "-project_root \"/proj_root\" -cwd \"/path/foo.das\"", + "should prepend -project_root before -cwd") + } +} + +[test] +def test_live_args_both(t : T?) { + t |> run("both set: -project_root before -project before -cwd") <| @(t : T?) { + let s = build_live_script_args("/path/foo.das", "/p.das_project", "/proj_root", false) + t |> equal(s, "-project_root \"/proj_root\" -project \"/p.das_project\" -cwd \"/path/foo.das\"", + "project_root prepended last ends up leftmost") + } +} + +[test] +def test_live_args_windows_paths(t : T?) { + t |> run("is_windows=true: forward slashes become backslashes") <| @(t : T?) { + let s = build_live_script_args("D:/path/foo.das", "D:/p.das_project", "D:/root", true) + t |> equal(s, "-project_root \"D:\\root\" -project \"D:\\p.das_project\" -cwd \"D:\\path\\foo.das\"", + "windows: slashes replaced in all three quoted paths") + } + t |> run("is_windows=false: forward slashes preserved") <| @(t : T?) { + let s = build_live_script_args("D:/path/foo.das", "", "", false) + t |> success(find(s, "/path/foo.das") >= 0, "non-windows: keep forward slashes -- s={s}") + t |> success(find(s, "\\path") < 0, "non-windows: no backslash conversion -- s={s}") + } +} + +// ── build_subtool_argv unit tests ──────────────────────────────────── +// Pins the `-project_root

` argv invariant: flag MUST be inserted BEFORE +// the subtool script path. Otherwise daslang treats it as a positional .das +// file to load, which silently breaks dyn-module discovery. + +[test] +def test_build_subtool_argv_no_project_root(t : T?) { + t |> run("argv without project_root: [exe, script, --, ...args]") <| @(t : T?) { + let args <- ["alpha", "beta"] + let argv <- build_subtool_argv("daslang.exe", "sub.das", args, "") + t |> equal(length(argv), 5, "argv length should be 5") + t |> equal(argv[0], "daslang.exe", "argv[0]=exe") + t |> equal(argv[1], "sub.das", "argv[1]=script_path (no flag before it)") + t |> equal(argv[2], "--", "argv[2]=--") + t |> equal(argv[3], "alpha", "argv[3]=alpha") + t |> equal(argv[4], "beta", "argv[4]=beta") + } +} + +[test] +def test_build_subtool_argv_with_project_root(t : T?) { + t |> run("argv with project_root: flag inserted BEFORE script path") <| @(t : T?) { + // build_subtool_argv does pure string composition — the path doesn't + // need to exist on the test host, just be passed through verbatim. + let args <- ["alpha"] + let argv <- build_subtool_argv("daslang.exe", "sub.das", args, "/proj/root") + t |> equal(length(argv), 6, "argv length should be 6") + t |> equal(argv[0], "daslang.exe", "argv[0]=exe") + t |> equal(argv[1], "-project_root", "argv[1]=-project_root flag") + t |> equal(argv[2], "/proj/root", "argv[2]=project_root path") + t |> equal(argv[3], "sub.das", "argv[3]=script_path (AFTER the flag)") + t |> equal(argv[4], "--", "argv[4]=--") + t |> equal(argv[5], "alpha", "argv[5]=alpha") + } +} + +[test] +def test_build_subtool_argv_empty_args(t : T?) { + t |> run("empty args still yields valid argv ending in --") <| @(t : T?) { + let args : array + let argv <- build_subtool_argv("daslang.exe", "sub.das", args, "") + t |> equal(length(argv), 3, "argv length should be 3") + t |> equal(argv[0], "daslang.exe", "argv[0]=exe") + t |> equal(argv[1], "sub.das", "argv[1]=script") + t |> equal(argv[2], "--", "argv[2]=--") + } + t |> run("empty args + project_root still yields valid argv") <| @(t : T?) { + let args : array + let argv <- build_subtool_argv("daslang.exe", "sub.das", args, "/root") + t |> equal(length(argv), 5, "argv length should be 5") + t |> equal(argv[3], "sub.das", "script comes after flag") + t |> equal(argv[4], "--", "trailing -- still present") + } +} + +// ── program_log smoke tests ────────────────────────────────────────── +// Pre-existing gap (no tests existed for program_log). Locks basic behavior +// after the in-process-to-subprocess refactor in commit 8eb959ed5. + +[test] +def test_program_log_basic(t : T?) { + t |> run("program_log returns post-compile program text for a named function") <| @(t : T?) { + var text : string + var is_error = false + parse_result(do_program_log(fixture_path("_fixture_valid.das"), "main"), text, is_error) + t |> success(!is_error, "should not be error -- text={text}") + t |> success(find(text, "def public main") >= 0, + "should contain post-infer main signature -- text={text}") + } + t |> run("program_log with empty func dumps full program") <| @(t : T?) { + var text : string + var is_error = false + parse_result(do_program_log(fixture_path("_fixture_valid.das"), ""), text, is_error) + t |> success(!is_error, "should not be error -- text={text}") + t |> success(find(text, "def public greet") >= 0, + "should list greet -- text={text}") + t |> success(find(text, "def public main") >= 0, + "should list main -- text={text}") + } +} + +[test] +def test_program_log_missing_func(t : T?) { + t |> run("program_log reports unfound function") <| @(t : T?) { + // program_log returns is_error=false but message text indicates failure. + // This is the current contract — pin it. + var text : string + var is_error = false + parse_result(do_program_log(fixture_path("_fixture_valid.das"), + "no_such_function_xyzzy"), + text, is_error) + t |> success(find(text, "not found") >= 0, + "should report 'not found' for bogus function -- text={text}") + } +} + +[test] +def test_project_root_invalid_dir(t : T?) { + t |> run("invalid project_root: pins current daslang behavior") <| @(t : T?) { + // Locks in whatever daslang.exe does when -project_root points at a + // nonexistent directory: today it silently no-ops the dyn-modules scan + // and the consumer compile fails with missing-prereq. If daslang ever + // changes to error explicitly on bad -project_root, this test catches + // the behavior shift. + var text : string + var is_error = false + parse_result(do_compile_check(fixture_path(PROOT_CONSUMER), "", + fixture_path("nonexistent_xyz_pretend_root")), + text, is_error) + t |> success(is_error, "should be error -- text={text}") + t |> success(find(text, PROOT_MISSING) >= 0, + "should still report missing prereq -- text={text}") + } +} diff --git a/utils/mcp/tests/_pretend_root/consumer.das b/utils/mcp/tests/_pretend_root/consumer.das new file mode 100644 index 0000000000..2f41dc1a40 --- /dev/null +++ b/utils/mcp/tests/_pretend_root/consumer.das @@ -0,0 +1,8 @@ +options gen2 + +require pretend_mod/greet + +[export] +def main() { + print("{hello()}\n") +} diff --git a/utils/mcp/tests/_pretend_root/modules/pretend_mod/.das_module b/utils/mcp/tests/_pretend_root/modules/pretend_mod/.das_module new file mode 100644 index 0000000000..c5cb4eec4a --- /dev/null +++ b/utils/mcp/tests/_pretend_root/modules/pretend_mod/.das_module @@ -0,0 +1,8 @@ +options gen2 + +require fio + +[export] +def initialize(project_path : string) { + register_native_path("pretend_mod", "greet", "{project_path}/greet.das") +} diff --git a/utils/mcp/tests/_pretend_root/modules/pretend_mod/greet.das b/utils/mcp/tests/_pretend_root/modules/pretend_mod/greet.das new file mode 100644 index 0000000000..224bec1ff3 --- /dev/null +++ b/utils/mcp/tests/_pretend_root/modules/pretend_mod/greet.das @@ -0,0 +1,7 @@ +options gen2 + +module greet shared public + +def hello() : string { + return "hello from pretend_mod" +} diff --git a/utils/mcp/tools/aot.das b/utils/mcp/tools/aot.das index e21d7f8eac..4ededed122 100644 --- a/utils/mcp/tools/aot.das +++ b/utils/mcp/tools/aot.das @@ -7,6 +7,6 @@ require common public //! Thin popen wrapper. Real logic lives in subtools/aot.das so macro //! state from compile_file doesn't leak across MCP calls. -def do_aot(file, func_name : string; project : string = "") : string { - return run_mcp_subtool("aot", [file, func_name, project]) +def do_aot(file, func_name : string; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("aot", [file, func_name, project], project_root) } diff --git a/utils/mcp/tools/ast_dump.das b/utils/mcp/tools/ast_dump.das index dc796d9e3e..7af1c3f5b8 100644 --- a/utils/mcp/tools/ast_dump.das +++ b/utils/mcp/tools/ast_dump.das @@ -3,9 +3,12 @@ options no_unused_function_arguments = false options no_unused_block_arguments = false require common public -require daslib/ast_boost -def do_ast_dump_expression(expression : string) : string { +//! Dispatcher: ast_dump has two paths — expression-based (spawns its own +//! daslang.exe subprocess with a generated stub) and file-based (forwards to +//! subtools/ast_dump.das). Both isolate compile state from the MCP server. + +def do_ast_dump_expression(expression, project_root : string) : string { let exe = get_daslang_exe() if (empty(exe)) return make_tool_result("Cannot determine daslang executable path", true) let script_path = make_temp_das_file() @@ -18,74 +21,23 @@ def do_ast_dump_expression(expression : string) : string { } if (!write_ok) return make_tool_result("Cannot write temp file", true) var output : string - let argv <- [exe, script_path] + var argv <- [exe] + if (!empty(project_root)) { + argv |> push("-project_root") + argv |> push(project_root) + } + argv |> push(script_path) let exit_code = run_and_capture(argv, output) remove(script_path) if (exit_code != 0) return make_tool_result("AST dump failed (exit code {exit_code}):\n{output}", true) return make_tool_result(output) } -def format_func_body(func; use_source_mode : bool; use_lineinfo : bool = false) : string { - if (use_source_mode) { - return describe_expression(func.body) - } else { - var deFlags = bitfield(0) - if (use_lineinfo) { - deFlags = DebugExpressionFlags.lineInfo - } - return debug_expression(func.body, deFlags) - } -} - -def do_ast_dump_file(file, func_name : string; use_source_mode, use_lineinfo : bool; project : string = "") : string { - return compile_program(file, true, false, project) $(program, issues) { - let thisMod = get_this_module(program) - var result = "" - var found = false - for_each_function(thisMod, "") $(func) { - if (func.flags.builtIn || func.flags._lambda || func.flags.generated - || (!empty(func_name) && func.name != func_name)) return - found = true - result += "def {func.name}" - var has_args = false - for (i in range(length(func.arguments))) { - if (!arg_needs_documenting(func.arguments[i]._type)) continue - if (!has_args) { - result += "(" - has_args = true - } else { - result += "; " - } - result += "{func.arguments[i].name} : {describe(func.arguments[i]._type)}" - } - if (has_args) { - result += ")" - } - if (func.result != null && !func.result.isVoid) { - result += " : {describe(func.result)}" - } - result += "\n" - result += format_func_body(func, use_source_mode, use_lineinfo) - result += "\n\n" - } - if (!found) { - if (!empty(func_name)) return "Function '{func_name}' not found in {file}" - return "No user functions found in {file}" - } - if (!empty(issues)) { - result += "Warnings:\n{issues}" - } - return result - } -} - -def do_ast_dump(expression, file, func_name, mode, lineinfo : string; project : string = "") : string { +def do_ast_dump(expression, file, func_name, mode, lineinfo : string; project : string = ""; project_root : string = "") : string { if (!empty(expression)) { - return do_ast_dump_expression(expression) + return do_ast_dump_expression(expression, project_root) } elif (!empty(file)) { - let use_source_mode = mode == "source" - let use_lineinfo = lineinfo == "true" - return do_ast_dump_file(file, func_name, use_source_mode, use_lineinfo, project) + return run_mcp_subtool("ast_dump", [file, func_name, mode, lineinfo, project], project_root) } else { return make_tool_result("Provide either 'expression' or 'file' argument", true) } diff --git a/utils/mcp/tools/common.das b/utils/mcp/tools/common.das index 376902ecb1..918c6ba18c 100644 --- a/utils/mcp/tools/common.das +++ b/utils/mcp/tools/common.das @@ -347,18 +347,37 @@ def subtool_user_args(args : array) : array { return <- out } -def run_mcp_subtool(subtool_name : string; args : array; timeout_sec : float = 120.0) : string { - let exe = get_daslang_exe() - if (empty(exe)) return make_tool_result("Cannot determine daslang executable path", true) - let subtool_path = path_join(get_das_root(), "utils/mcp/subtools/{subtool_name}.das") - // `--` is critical: without it, daslang treats positional argv as extra - // .das files to load AND auto-runs each. With `--`, daslang stops parsing - // its own options and just exposes the rest via get_command_line_arguments(). - var argv <- [exe, subtool_path, "--"] +// daslang.exe CLI flags must come BEFORE the script path. `-project_root

` +// triggers `require_dynamic_modules` at daslang startup against

/modules/*/.das_module, +// so the subtool's compile_file() picks up daspkg-style native paths +// (e.g. require imgui/X resolving via D:/IMGUI_DEMO/modules/dasImgui/.das_module). +// `--` is critical: without it, daslang treats positional argv as extra +// .das files to load AND auto-runs each. With `--`, daslang stops parsing +// its own options and just exposes the rest via get_command_line_arguments(). +def public build_subtool_argv(exe, subtool_path : string; + args : array; + project_root : string) : array { + var argv <- [exe] + if (!empty(project_root)) { + argv |> push("-project_root") + argv |> push(project_root) + } + argv |> push(subtool_path) + argv |> push("--") argv |> reserve(length(argv) + length(args)) for (a in args) { argv |> push(a) } + return <- argv +} + +def run_mcp_subtool(subtool_name : string; args : array; + project_root : string = ""; + timeout_sec : float = 120.0) : string { + let exe = get_daslang_exe() + if (empty(exe)) return make_tool_result("Cannot determine daslang executable path", true) + let subtool_path = path_join(get_das_root(), "utils/mcp/subtools/{subtool_name}.das") + let argv <- build_subtool_argv(exe, subtool_path, args, project_root) var output : string let exit_code = run_and_capture(argv, output, timeout_sec) if (exit_code == popen_timed_out) return make_tool_result("MCP subtool '{subtool_name}' timed out after {timeout_sec}s:\n{output}", true) diff --git a/utils/mcp/tools/compile_check.das b/utils/mcp/tools/compile_check.das index 948c67b65d..20f2462c9e 100644 --- a/utils/mcp/tools/compile_check.das +++ b/utils/mcp/tools/compile_check.das @@ -10,6 +10,6 @@ require common public //! [call_macro] state across calls. The subtool's stdout (already a //! make_tool_result(...) JSON envelope) is returned verbatim. -def do_compile_check(file : string; project : string = ""; json : bool = false) : string { - return run_mcp_subtool("compile_check", [file, project, json ? "true" : "false"]) +def do_compile_check(file : string; project : string = ""; project_root : string = ""; json : bool = false) : string { + return run_mcp_subtool("compile_check", [file, project, json ? "true" : "false"], project_root) } diff --git a/utils/mcp/tools/describe_type.das b/utils/mcp/tools/describe_type.das index 54ff1a65c5..d6adc05e91 100644 --- a/utils/mcp/tools/describe_type.das +++ b/utils/mcp/tools/describe_type.das @@ -7,6 +7,6 @@ require common public //! Thin popen wrapper. Real logic lives in subtools/describe_type.das so //! macro state from compile_file doesn't leak across MCP calls. -def do_describe_type(name, module_name : string; project : string = "") : string { - return run_mcp_subtool("describe_type", [name, module_name, project]) +def do_describe_type(name, module_name : string; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("describe_type", [name, module_name, project], project_root) } diff --git a/utils/mcp/tools/eval_expression.das b/utils/mcp/tools/eval_expression.das index 826a466625..b578accbfc 100644 --- a/utils/mcp/tools/eval_expression.das +++ b/utils/mcp/tools/eval_expression.das @@ -50,7 +50,7 @@ def private rewrite_modules_in_set(req_modules : string; missing : array return join(out_parts, ", ") => rewritten } -def private try_eval_expression(exe, expression, req_modules, project : string; var output : string&) : int { +def private try_eval_expression(exe, expression, req_modules, project, project_root : string; var output : string&) : int { let stub_path = make_temp_das_file() let script = build_string() $(var w) { write(w, "options gen2\n") @@ -81,6 +81,10 @@ def private try_eval_expression(exe, expression, req_modules, project : string; return -1 } var argv <- [exe] + if (!empty(project_root)) { + argv |> push("-project_root") + argv |> push(project_root) + } if (!empty(project)) { argv |> push("-project") argv |> push(string(project)) @@ -91,14 +95,14 @@ def private try_eval_expression(exe, expression, req_modules, project : string; return exit_code } -def do_eval_expression(expression, req_modules : string; project : string = "") : string { +def do_eval_expression(expression, req_modules : string; project : string = ""; project_root : string = "") : string { if (empty(expression)) return make_tool_result("missing 'expression' argument", true) let project_err = validate_project_arg(project) if (!empty(project_err)) return make_tool_result(project_err, true) let exe = get_daslang_exe() if (empty(exe)) return make_tool_result("Cannot determine daslang executable path", true) var output : string - let exit_code = try_eval_expression(exe, expression, req_modules, project, output) + let exit_code = try_eval_expression(exe, expression, req_modules, project, project_root, output) if (exit_code == 0) return make_tool_result(strip(output)) // exit_code < 0 means try_eval_expression failed before invoking the daslang subprocess // (e.g. couldn't write the temp file). Surface the message directly — no retry would help. @@ -114,7 +118,7 @@ def do_eval_expression(expression, req_modules : string; project : string = "") let did_rewrite = rewrite._1 if (did_rewrite) { var output2 : string - let exit_code2 = try_eval_expression(exe, expression, new_modules, project, output2) + let exit_code2 = try_eval_expression(exe, expression, new_modules, project, project_root, output2) if (exit_code2 == 0) return make_tool_result("[resolved unqualified modules under daslib/: {new_modules}]\n{strip(output2)}") if (exit_code2 < 0) return make_tool_result(output2, true) // Retry also failed at the daslang level — surface the retry's output, since the rewrite diff --git a/utils/mcp/tools/find_references.das b/utils/mcp/tools/find_references.das index 76a92f81b1..f75d4b6d3d 100644 --- a/utils/mcp/tools/find_references.das +++ b/utils/mcp/tools/find_references.das @@ -3,420 +3,12 @@ options no_unused_function_arguments = false options no_unused_block_arguments = false require common public -require daslib/ast_cursor -require daslib/ast_boost -// A single reference location -struct Reference { - file : string - line : int - column : int - in_function : string // enclosing function name, or "" for global - context : string // short description: "call", "variable", "field", "type ref", "addr", etc. -} - -// What we're looking for -enum RefTargetKind { - none - func - variable - structure - field - enumeration - alias -} - -struct RefTarget { - kind : RefTargetKind - // all pointers stored as void? for identity comparison (avoids const mismatch) - target_ptr : void? - // for fields: struct identity + name - field_struct_ptr : void? - field_name : string - // for aliases: the alias name string - alias_name : string - // display name - name : string -} - -def to_void(p) : void? { - return unsafe(reinterpret p) -} - -// Build a RefTarget from a cursor hit -def identify_target(hit : CursorHit) : RefTarget { - let expr = hit.expr - // ExprCall → function - if (expr is ExprCall) { - let c = expr as ExprCall - if (c.func != null) return RefTarget(kind = RefTargetKind.func, target_ptr = to_void(c.func), name = string(c.func.name)) - } - // ExprAddr → function pointer - if (expr is ExprAddr) { - let a = expr as ExprAddr - if (a.func != null) return RefTarget(kind = RefTargetKind.func, target_ptr = to_void(a.func), name = string(a.func.name)) - } - // ExprInvoke → resolve the callable (first argument) - if (expr is ExprInvoke) { - let inv = expr as ExprInvoke - if (!empty(inv.arguments)) { - let callable = inv.arguments[0] - // first arg is a variable (function pointer parameter/local) - if (callable is ExprVar) { - let v = callable as ExprVar - if (v.variable != null) { - let variable = v.variable - return RefTarget(kind = RefTargetKind.variable, target_ptr = to_void(variable), name = string(variable.name)) - } - } - // first arg is a field access (struct method pointer) - if (callable is ExprField) { - let f = callable as ExprField - if (f.value != null && f.value._type != null) { - var vt = f.value._type - if (vt.structType == null && vt.firstType != null) { - vt = vt.firstType - } - if (vt.structType != null) return RefTarget( - kind = RefTargetKind.field, - field_struct_ptr = to_void(vt.structType), - field_name = string(f.name), - name = "{vt.structType.name}.{f.name}" - ) - } - } - } - } - // ExprVar → variable - if (expr is ExprVar) { - let v = expr as ExprVar - if (v.variable != null) { - let variable = v.variable - return RefTarget(kind = RefTargetKind.variable, target_ptr = to_void(variable), name = string(variable.name)) - } - } - // ExprField / ExprSafeField → field - if (expr is ExprField || expr is ExprSafeField) { - var fname : string - var value_type : TypeDecl const? - if (expr is ExprField) { - let f = expr as ExprField - fname = string(f.name) - if (f.value != null && f.value._type != null) { - value_type = f.value._type - } - } else { - let f = expr as ExprSafeField - fname = string(f.name) - if (f.value != null && f.value._type != null) { - value_type = f.value._type - } - } - // unwrap pointer types (Foo? → Foo) - if (value_type != null && value_type.structType == null && value_type.firstType != null) { - value_type = value_type.firstType - } - if (value_type != null && value_type.structType != null) return RefTarget( - kind = RefTargetKind.field, - field_struct_ptr = to_void(value_type.structType), - field_name = fname, - name = "{value_type.structType.name}.{fname}" - ) - } - // ExprConstEnumeration → enum - if (expr is ExprConstEnumeration) { - let e = expr as ExprConstEnumeration - if (e.enumType != null) return RefTarget(kind = RefTargetKind.enumeration, target_ptr = to_void(e.enumType), name = string(e.enumType.name)) - } - // ExprConstBitfield → alias (bitfield type) - if (expr is ExprConstBitfield) { - if (expr._type != null) { - let td = expr._type - if (!empty(td.alias)) return RefTarget(kind = RefTargetKind.alias, alias_name = string(td.alias), name = string(td.alias)) - } - } - // Type alias reference (typedef, bitfield, variant, tuple) - if (expr._type != null) { - let td = expr._type - if (!empty(td.alias)) return RefTarget(kind = RefTargetKind.alias, alias_name = string(td.alias), name = string(td.alias)) - } - // Fallback: type reference - if (expr._type != null) { - let td = expr._type - if (td.structType != null) return RefTarget(kind = RefTargetKind.structure, target_ptr = to_void(td.structType), name = string(td.structType.name)) - if (td.enumType != null) return RefTarget(kind = RefTargetKind.enumeration, target_ptr = to_void(td.enumType), name = string(td.enumType.name)) - } - return RefTarget() -} - -// Visitor that collects all references to a given target -class RefVisitor : AstVisitor { - target : RefTarget - refs : array - currentFunc : Function const? - file_filter : string // if non-empty, only collect refs from this file - - def RefVisitor(t : RefTarget; file_flt : string) { - target = t - currentFunc = null - file_filter = file_flt - } - - def add_ref(at : LineInfo; context : string) : void { - if (int(at.line) == 0) return - // file filter - if (!empty(file_filter) && at.fileInfo != null) { - let fname = string(at.fileInfo.name) - if (find(fname, file_filter) < 0) return - } - refs |> emplace(Reference( - file = at.fileInfo != null ? string(at.fileInfo.name) : "", - line = int(at.line), - column = int(at.column), - in_function = currentFunc != null ? string(currentFunc.name) : "", - context = context - )) - } - - def override preVisitFunction(fun : FunctionPtr) : void { - currentFunc = fun - } - - def override visitFunction(var fun : FunctionPtr) : FunctionPtr { - currentFunc = null - return <- fun - } - - // ExprCall → function reference - def override preVisitExprCall(expr : ExprCall?) : void { - if (target.kind == RefTargetKind.func) { - if (expr.func != null && to_void(expr.func) == target.target_ptr) { - add_ref(expr.at, "call") - } - } - } - - // ExprAddr → function pointer reference (@@func) - def override preVisitExprAddr(expr : ExprAddr?) : void { - if (target.kind == RefTargetKind.func) { - if (expr.func != null && to_void(expr.func) == target.target_ptr) { - add_ref(expr.at, "addr") - } - } - } - - // ExprVar → variable reference - def override preVisitExprVar(expr : ExprVar?) : void { - if (target.kind == RefTargetKind.variable) { - if (expr.variable != null) { - let vptr = expr.variable - if (to_void(vptr) == target.target_ptr) { - add_ref(expr.at, "variable") - } - } - } - } - - // ExprField → field reference - def override preVisitExprField(expr : ExprField?) : void { - if (target.kind == RefTargetKind.field) { - if (expr.name == target.field_name && expr.value != null && expr.value._type != null) { - var vt = expr.value._type - if (vt.structType == null && vt.firstType != null) { - vt = vt.firstType - } - if (vt.structType != null && to_void(vt.structType) == target.field_struct_ptr) { - add_ref(expr.atField, "field") - } - } - } - } - - // ExprSafeField → field reference (?.field) - def override preVisitExprSafeField(expr : ExprSafeField?) : void { - if (target.kind == RefTargetKind.field) { - if (expr.name == target.field_name && expr.value != null && expr.value._type != null) { - var vt = expr.value._type - if (vt.structType == null && vt.firstType != null) { - vt = vt.firstType - } - if (vt.structType != null && to_void(vt.structType) == target.field_struct_ptr) { - add_ref(expr.atField, "safe field") - } - } - } - } - - // ExprConstEnumeration → enum value reference - def override preVisitExprConstEnumeration(expr : ExprConstEnumeration?) : void { - if (target.kind == RefTargetKind.enumeration) { - if (expr.enumType != null && to_void(expr.enumType) == target.target_ptr) { - add_ref(expr.at, "enum value") - } - } - } - - // ExprConstBitfield → bitfield value reference - def override preVisitExprConstBitfield(expr : ExprConstBitfield?) : void { - if (target.kind == RefTargetKind.alias) { - if (expr._type != null && !empty(expr._type.alias) && expr._type.alias == target.alias_name) { - add_ref(expr.at, "bitfield value") - } - } - } - - // TypeDecl → struct/enum/alias type references (in variable types, return types, etc.) - def override preVisitTypeDecl(typ : TypeDeclPtr) : void { - let td = typ - if (target.kind == RefTargetKind.structure && td.structType != null) { - if (to_void(td.structType) == target.target_ptr) { - add_ref(td.at, "type ref") - } - } - if (target.kind == RefTargetKind.enumeration && td.enumType != null) { - if (to_void(td.enumType) == target.target_ptr) { - add_ref(td.at, "type ref") - } - } - if (target.kind == RefTargetKind.alias && !empty(td.alias)) { - if (td.alias == target.alias_name) { - add_ref(td.at, "alias ref") - } - } - } -} - -def format_references(refs : array; target : RefTarget) : string { - return build_string() $(var w) { - write(w, "Found {length(refs)} reference(s) to {target.kind} '{target.name}':\n") - for (r in refs) { - write(w, " {r.file}:{r.line}:{r.column}") - if (!empty(r.in_function)) { - write(w, " in {r.in_function}") - } - write(w, " [{r.context}]\n") - } - } -} - -def private fileinfo_name_contains(name : das_string; needle : string) : bool { - var hit = false - peek(name) $(s) { - hit = find(s, needle) >= 0 - } - return hit -} - -// Try to identify a target from declarations at the cursor line -// (function defs, struct defs, enum defs — not expressions) -def identify_declaration_target(program : smart_ptr; file : string; line : int) : RefTarget { - let thisMod = get_this_module(program) - var result : RefTarget - // check function declarations - ast::for_each_function(thisMod, "") $(var func : FunctionPtr) { - if (result.kind != RefTargetKind.none) return - let f = func - if (int(f.at.line) == line && f.at.fileInfo != null) { - if (fileinfo_name_contains(f.at.fileInfo.name, file)) { - result = RefTarget(kind = RefTargetKind.func, target_ptr = to_void(f), name = string(f.name)) - } - } - } - if (result.kind != RefTargetKind.none) return result - // check structure declarations - ast::for_each_structure(thisMod) $(var st : StructurePtr) { - if (result.kind != RefTargetKind.none) return - let s = st - if (int(s.at.line) == line && s.at.fileInfo != null) { - if (fileinfo_name_contains(s.at.fileInfo.name, file)) { - result = RefTarget(kind = RefTargetKind.structure, target_ptr = to_void(s), name = string(s.name)) - } - } - } - if (result.kind != RefTargetKind.none) return result - // check enumeration declarations - ast::for_each_enumeration(thisMod) $(var en : EnumerationPtr) { - if (result.kind != RefTargetKind.none) return - let e = en - if (int(e.at.line) == line && e.at.fileInfo != null) { - if (fileinfo_name_contains(e.at.fileInfo.name, file)) { - result = RefTarget(kind = RefTargetKind.enumeration, target_ptr = to_void(e), name = string(e.name)) - } - } - } - if (result.kind != RefTargetKind.none) return result - // check typedef declarations (typedef, bitfield, variant, tuple) - ast::for_each_typedef(thisMod) $(name : string#; var value : TypeDeclPtr) { - if (result.kind != RefTargetKind.none) return - let td = value - if (int(td.at.line) == line && td.at.fileInfo != null) { - if (fileinfo_name_contains(td.at.fileInfo.name, file)) { - result = RefTarget(kind = RefTargetKind.alias, alias_name = string(name), name = string(name)) - } - } - } - if (result.kind != RefTargetKind.none) return result - // check global variable declarations - ast::for_each_global(thisMod) $(var value : VariablePtr) { - if (result.kind != RefTargetKind.none) return - let v = value - if (int(v.at.line) == line && v.at.fileInfo != null) { - if (fileinfo_name_contains(v.at.fileInfo.name, file)) { - result = RefTarget(kind = RefTargetKind.variable, target_ptr = to_void(v), name = string(v.name)) - } - } - } - return result -} - -def public do_find_references(file : string; line_str, col_str, scope_str : string) : string { - return do_find_references(file, line_str, col_str, scope_str, "") -} +//! Thin popen wrapper. Real logic lives in subtools/find_references.das so +//! macro state from compile_file doesn't leak across MCP calls AND so +//! any shared module the compiled program requires (dasModuleImgui, etc.) +//! loads + unloads on the subtool's process lifecycle, not the MCP server's. -def public do_find_references(file : string; line_str, col_str, scope_str, no_opt_str : string; project : string = "") : string { - let line = to_int(line_str) - let col = to_int(col_str) - if (line <= 0 || col <= 0) return make_tool_result("Invalid line or column (must be >= 1)", true) - let all_modules = scope_str == "all" - let no_opt = no_opt_str == "true" - return compile_program(file, true, no_opt, project) $(program; issues) { - // Step 1: identify the target from cursor (expression-based) - var target : RefTarget - var hits <- find_at_cursor(program, file, line, col) - if (!empty(hits)) { - for (hit in hits) { - target = identify_target(hit) - if (target.kind != RefTargetKind.none) break - } - } - // Step 1b: if no expression found, try declaration-based lookup - if (target.kind == RefTargetKind.none) { - target = identify_declaration_target(program, file, line) - } - if (target.kind == RefTargetKind.none) { - if (empty(hits)) return "No expression or declaration found at {file}:{line}:{col}" - return build_string() $(var w) { - write(w, "Could not identify a referenceable symbol at {file}:{line}:{col}\n") - write(w, "Found expressions:\n") - for (hit in hits) { - write(w, " {describe(hit)}\n") - } - } - } - // Step 2: walk AST to find all references - let file_filter = all_modules ? "" : file - var visitor = new RefVisitor(target, file_filter) - make_visitor(*visitor) $(adapter) { - if (all_modules) { - visit_modules(program, adapter) - } else { - visit(program, adapter) - } - } - let result = format_references(visitor.refs, target) - unsafe { - delete visitor - } - return result - } +def public do_find_references(file : string; line_str, col_str, scope_str, no_opt_str : string; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("find_references", [file, line_str, col_str, scope_str, no_opt_str, project], project_root) } diff --git a/utils/mcp/tools/find_symbol.das b/utils/mcp/tools/find_symbol.das index 32dab13022..a4e0f18dca 100644 --- a/utils/mcp/tools/find_symbol.das +++ b/utils/mcp/tools/find_symbol.das @@ -7,6 +7,6 @@ require common public //! Thin popen wrapper. Real logic lives in subtools/find_symbol.das so //! macro state from compile_file doesn't leak across MCP calls. -def do_find_symbol(query : string; kind : string = ""; file : string = ""; project : string = ""; with_cpp_source : bool = false; cpp_dirs : string = "") : string { - return run_mcp_subtool("find_symbol", [query, kind, file, project, with_cpp_source ? "true" : "false", cpp_dirs]) +def do_find_symbol(query : string; kind : string = ""; file : string = ""; project : string = ""; project_root : string = ""; with_cpp_source : bool = false; cpp_dirs : string = "") : string { + return run_mcp_subtool("find_symbol", [query, kind, file, project, with_cpp_source ? "true" : "false", cpp_dirs], project_root) } diff --git a/utils/mcp/tools/goto_definition.das b/utils/mcp/tools/goto_definition.das index 249827d895..3123ddbc6d 100644 --- a/utils/mcp/tools/goto_definition.das +++ b/utils/mcp/tools/goto_definition.das @@ -3,347 +3,14 @@ options no_unused_function_arguments = false options no_unused_block_arguments = false require common public -require cpp_common public -require daslib/ast_cursor -require daslib/ast_boost -require math -// Result of resolving a definition location -struct DefinitionResult { - found : bool - kind : string // "variable", "function", "field", "struct", "enum", "builtin" - name : string - file : string - line : int - column : int - snippet : string // source lines around the definition - cppName : string // C++ symbol name for builtin functions and handled types. - // Set by: - // - ExprCall to a builtin function → BuiltInFunction.cppName - // - Type fallback on a handled type → TypeAnnotation.cppName - // NOT yet populated for ExprVar of a handled type or ExprField - // on a handled-type parent — see "v2 roadmap" in the file footer. -} - -// Read a few lines from a file around a given line number -def read_source_snippet(file_path : string; line : int; context_lines : int = 3) : string { - if (empty(file_path) || line <= 0) return "" - var f = fopen(file_path, "r") - if (f == null) return "" - var lines : array - while (!feof(f)) { - var ln = fgets(f) - // strip trailing newline/cr - while (!empty(ln)) { - let last_ch = character_at(ln, length(ln) - 1) // nolint:PERF003 - if (last_ch == '\n' || last_ch == '\r') { - ln = slice(ln, 0, length(ln) - 1) - } else { - break - } - } - lines |> push(ln) - } - fclose(f) - if (empty(lines)) return "" - let first = max(0, line - 1 - context_lines) - let last = min(length(lines) - 1, line - 1 + context_lines) - return build_string() $(var w) { - for (idx in range(first, last + 1)) { - write(w, "{idx + 1}: {lines[idx]}\n") - } - } -} - -// Get file path from LineInfo -def lineinfo_file(at : LineInfo) : string { - if (at.fileInfo != null) return string(at.fileInfo.name) - return "" -} +//! Thin popen wrapper. Real logic lives in subtools/goto_definition.das so +//! macro state from compile_file doesn't leak across MCP calls AND so any +//! shared module the compiled program requires (dasModuleImgui, etc.) +//! loads + unloads on the subtool's process lifecycle, not the MCP server's. -// Resolve definition from a CursorHit -def resolve_definition(hit : CursorHit) : DefinitionResult { - let expr = hit.expr - // ExprVar → variable declaration - if (expr is ExprVar) { - let v = expr as ExprVar - if (v.variable != null) { - let variable = v.variable - let file = lineinfo_file(variable.at) - let line = int(variable.at.line) - return DefinitionResult( - found = true, - kind = "variable", - name = string(variable.name), - file = file, - line = line, - column = int(variable.at.column), - snippet = read_source_snippet(file, line) - ) - } - } - // ExprCall / ExprCallFunc → function declaration - if (expr is ExprCall) { - let c = expr as ExprCall - if (c.func != null) { - if (c.func.flags.builtIn) { - var cppName : string - let bfn = c.func as BuiltInFunction - if (bfn != null) { - cppName = string(bfn.cppName) - } - return DefinitionResult( - found = true, - kind = "builtin", - name = string(c.func.name), - snippet = build_string() $(var w) { - write(w, "builtin def {c.func.name}") - write_func_signature(w, *c.func) - }, - cppName = cppName - ) - } - let file = lineinfo_file(c.func.at) - let line = int(c.func.at.line) - return DefinitionResult( - found = true, - kind = "function", - name = string(c.func.name), - file = file, - line = line, - column = int(c.func.at.column), - snippet = read_source_snippet(file, line) - ) - } - } - // ExprField / ExprSafeField → struct field declaration - // Note: `is` checks exact type, not inheritance — must handle both - if (expr is ExprField || expr is ExprSafeField) { - var field_name : string - var value_type : TypeDecl const? - if (expr is ExprField) { - let f = expr as ExprField - field_name = string(f.name) - if (f.value != null && f.value._type != null) { - value_type = f.value._type - } - } else { - let f = expr as ExprSafeField - field_name = string(f.name) - if (f.value != null && f.value._type != null) { - value_type = f.value._type - } - } - // unwrap pointer types (Foo? → Foo) - if (value_type != null && value_type.structType == null && value_type.firstType != null) { - value_type = value_type.firstType - } - if (value_type != null && value_type.structType != null) { - let st = value_type.structType - for (fld in st.fields) { - if (fld.name == field_name) { - let file = lineinfo_file(fld.at) - let line = int(fld.at.line) - return DefinitionResult( - found = true, - kind = "field", - name = "{st.name}.{field_name}", - file = file, - line = line, - column = int(fld.at.column), - snippet = read_source_snippet(file, line) - ) - } - } - } - } - // ExprConstEnumeration → enum declaration - if (expr is ExprConstEnumeration) { - let e = expr as ExprConstEnumeration - if (e.enumType != null) { - let en = e.enumType - let file = lineinfo_file(en.at) - let line = int(en.at.line) - return DefinitionResult( - found = true, - kind = "enum", - name = "{en.name}.{e.value}", - file = file, - line = line, - column = int(en.at.column), - snippet = read_source_snippet(file, line) - ) - } - } - // Type alias → resolve to typedef declaration - if (expr._type != null) { - let td = expr._type - if (!empty(td.alias)) { - let file = lineinfo_file(td.at) - let line = int(td.at.line) - return DefinitionResult( - found = true, - kind = "typedef", - name = string(td.alias), - file = file, - line = line, - column = int(td.at.column), - snippet = read_source_snippet(file, line) - ) - } - } - // Fallback: resolve type reference from _type - if (expr._type != null) { - let td = expr._type - // Handled type (C++-backed via MAKE_TYPE_FACTORY / addAnnotation) → - // emit kind=handled with cppName so `with_cpp_source` can redirect. - // This mirrors the path `find_symbol` already takes for handled - // type entries. - if (td.isHandle && td.annotation != null) { - let ann = td.annotation - return DefinitionResult( - found = true, - kind = "handled", - name = string(ann.name), - cppName = string(ann.cppName) - ) - } - if (td.structType != null) { - let st = td.structType - let file = lineinfo_file(st.at) - let line = int(st.at.line) - return DefinitionResult( - found = true, - kind = "struct", - name = string(st.name), - file = file, - line = line, - column = int(st.at.column), - snippet = read_source_snippet(file, line) - ) - } - if (td.enumType != null) { - let en = td.enumType - let file = lineinfo_file(en.at) - let line = int(en.at.line) - return DefinitionResult( - found = true, - kind = "enum", - name = string(en.name), - file = file, - line = line, - column = int(en.at.column), - snippet = read_source_snippet(file, line) - ) - } - } - return DefinitionResult() +def public do_goto_definition(file : string; line_str, col_str : string; no_opt_str : string = ""; project : string = ""; project_root : string = ""; with_cpp_source : bool = false; cpp_dirs : string = "") : string { + return run_mcp_subtool("goto_definition", + [file, line_str, col_str, no_opt_str, with_cpp_source ? "true" : "false", cpp_dirs, project], + project_root) } - -def format_result(defn : DefinitionResult; hit : CursorHit; with_cpp_source : bool; cpp_dirs : array) : string { - return build_string() $(var w) { - write(w, "Symbol: {defn.name}\n") - write(w, "Kind: {defn.kind}\n") - if (!empty(defn.file)) { - write(w, "File: {defn.file}\n") - write(w, "Line: {defn.line}\n") - write(w, "Column: {defn.column}\n") - } - if (!empty(defn.cppName)) { - write(w, "C++: {defn.cppName}") - if (with_cpp_source) { - var match : CppMatch - let found = empty(cpp_dirs) ? cpp_lookup_by_name(defn.cppName, match) : cpp_lookup_by_name_scoped(defn.cppName, cpp_dirs, match) - if (found) { - write(w, " ({match.file}:{match.line})") - } elif (empty(cpp_dirs)) { - let why = cpp_index_status() - if (!empty(why)) { - write(w, " (index unavailable: {why})") - } else { - write(w, " (not located)") - } - } else { - let dirs_csv = join(cpp_dirs, ",") - write(w, " (not located in {dirs_csv})") - } - } - write(w, "\n") - } - if (hit.func != null) { - write(w, "In function: {hit.func.name}\n") - } - if (!empty(defn.snippet)) { - write(w, "\n{defn.snippet}") - } - } -} - -def public do_goto_definition(file : string; line_str, col_str : string; no_opt_str : string = ""; project : string = ""; with_cpp_source : bool = false; cpp_dirs : string = "") : string { - let line = to_int(line_str) - let col = to_int(col_str) - if (line <= 0 || col <= 0) return make_tool_result("Invalid line or column (must be >= 1)", true) - let no_opt = no_opt_str == "true" - var cpp_dirs_arr : array - if (!empty(cpp_dirs)) { - for (part in split(cpp_dirs, ",")) { - let trimmed = strip(part) - if (!empty(trimmed)) { - cpp_dirs_arr |> push(trimmed) - } - } - } - return compile_program(file, true, no_opt, project) $(program; issues) { - var hits <- find_at_cursor(program, file, line, col) - if (empty(hits)) return "No expression found at {file}:{line}:{col}" - // try each hit from innermost outward until we resolve a definition - for (hit in hits) { - let defn = resolve_definition(hit) - if (defn.found) return format_result(defn, hit, with_cpp_source, cpp_dirs_arr) - } - // nothing resolved — show what we found at cursor - return build_string() $(var w) { - write(w, "Found expression at cursor but could not resolve definition:\n") - for (hit in hits) { - write(w, " {describe(hit)}\n") - } - } - } -} - -// ── v2 roadmap (cppName population gaps) ───────────────────────────── -// -// `with_cpp_source` currently fires the cpp redirect for: -// ✓ ExprCall to a builtin function (BuiltInFunction.cppName) -// ✓ Type fallback on a handled type (TypeAnnotation.cppName) -// -// Not yet wired up — drop into `resolve_definition` when needed: -// -// • ExprVar of a handled type -// e.g. `var ctx : Context; ctx |> ...` — goto on `ctx` returns -// kind=variable with no cppName. Could populate from -// `variable._type.annotation.cppName` when `variable._type.isHandle`. -// -// • ExprField on a handled-type parent -// e.g. `func.name` where `func : Function?` (handled type) — goto on -// `name` falls through (no `structType`) and returns "not found". -// Needs an `is_basic_structure_annotation`-aware path that walks the -// annotation's fields and emits cppName for the matched field. -// -// • ExprCall to an addExtern'd non-builtin -// Most addExterns are flagged builtIn so the existing branch catches -// them. If a future overload escapes that flag, fall back to -// `c.func.cppName` (Function.cppName). -// -// The shared theme: every code path that already produces a kind!=builtin -// DefinitionResult could optionally fold in cppName when the underlying -// declaration has C++ backing — but keep `with_cpp_source` semantics narrow -// (one redirect per result, no speculative scanning). -// -// Test gap: the handled-type fallback path is currently exercised only -// indirectly (via the parallel `find_symbol` test path that uses the same -// `TypeAnnotation.cppName`). A direct goto test needs a fixture that uses -// a handled type without pulling in `rtti`/`ast` (they're not registered as -// user-script-visible modules in the default MCP context). When v2 lands, -// add such a fixture (e.g. via a small custom-typed module or a scratch -// file written at test time) and assert `→ cpp:` resolves. diff --git a/utils/mcp/tools/lint_tool.das b/utils/mcp/tools/lint_tool.das index 3c8e484b6c..836c701ac4 100644 --- a/utils/mcp/tools/lint_tool.das +++ b/utils/mcp/tools/lint_tool.das @@ -7,6 +7,6 @@ require common public //! Thin popen wrapper. Real logic lives in subtools/lint_tool.das so //! macro state from compile_file doesn't leak across MCP calls. -def do_lint(file : string; project : string = "") : string { - return run_mcp_subtool("lint_tool", [file, project]) +def do_lint(file : string; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("lint_tool", [file, project], project_root) } diff --git a/utils/mcp/tools/list_functions.das b/utils/mcp/tools/list_functions.das index a36e93c515..db025e06ab 100644 --- a/utils/mcp/tools/list_functions.das +++ b/utils/mcp/tools/list_functions.das @@ -7,6 +7,6 @@ require common public //! Thin popen wrapper. Real logic lives in subtools/list_functions.das so //! macro state from compile_file doesn't leak across MCP calls. -def do_list_functions(file : string; project : string = "") : string { - return run_mcp_subtool("list_functions", [file, project]) +def do_list_functions(file : string; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("list_functions", [file, project], project_root) } diff --git a/utils/mcp/tools/list_module_api.das b/utils/mcp/tools/list_module_api.das index ad7d5c0cc1..c852483f83 100644 --- a/utils/mcp/tools/list_module_api.das +++ b/utils/mcp/tools/list_module_api.das @@ -7,6 +7,6 @@ require common public //! Thin popen wrapper. Real logic lives in subtools/list_module_api.das so //! macro state from compile_file doesn't leak across MCP calls. -def do_list_module_api(module_name : string; filter : string = ""; section : string = ""; compact_str : string = ""; project : string = "") : string { - return run_mcp_subtool("list_module_api", [module_name, filter, section, compact_str, project]) +def do_list_module_api(module_name : string; filter : string = ""; section : string = ""; compact_str : string = ""; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("list_module_api", [module_name, filter, section, compact_str, project], project_root) } diff --git a/utils/mcp/tools/list_requires.das b/utils/mcp/tools/list_requires.das index ebcd91b52a..6e37081377 100644 --- a/utils/mcp/tools/list_requires.das +++ b/utils/mcp/tools/list_requires.das @@ -1,107 +1,14 @@ options gen2 -options rtti options no_unused_function_arguments = false options no_unused_block_arguments = false require common public -require daslib/json_boost -struct RequireEntry { - name : string - is_public : bool - source : string // "builtin" or file path -} - -struct RequiresResult { - direct : array - transitive : array -} - -def module_kind(mod : Module?) : string { - if (mod == null) return "missing" - if (!empty(mod.fileName)) return string(mod.fileName) - return "builtin" -} +//! Thin popen wrapper. Real logic lives in subtools/list_requires.das so +//! macro state from compile_file doesn't leak across MCP calls AND so +//! any shared module the compiled program requires (dasModuleImgui, etc.) +//! loads + unloads on the subtool's process lifecycle, not the MCP server's. -def do_list_requires(file : string; project : string = ""; json : bool = false) : string { - return compile_program(file, true, false, project) $(program; issues) { - // direct requires from allRequireDecl - var direct_text : array - var direct_entries : array - var direct_modules : array - for_each_require_declaration(program) $(var mod : Module?; mod_name : string#; mod_file : string#; var is_public : bool; at : LineInfo&) { - let kind = module_kind(mod) - if (!json) { - let pub_marker = is_public ? " public" : "" - direct_text |> push(" require {mod_name}{pub_marker} // {kind}") - } else { - direct_entries |> emplace(RequireEntry(name = string(mod_name), is_public = is_public, source = kind)) - } - direct_modules |> push(string(mod_name)) - } - // transitive: walk dependencies of the main module, skip "$" (always implicit) - var transitive_text : array - var transitive_entries : array - let thisMod = get_this_module(program) - var visited : table - for (dm in direct_modules) { - visited |> insert(dm, true) - } - visited |> insert("$", true) - var queue : array - module_for_each_dependency(thisMod) $(var dep : Module?; var is_pub : bool) { - let dep_name = string(dep.name) - if (!key_exists(visited, dep_name)) { - visited |> insert(dep_name, true) - queue |> push(dep) - } - } - var idx = 0 - while (idx < length(queue)) { - let mod = queue[idx] - idx++ - let mod_name = string(mod.name) - let kind = module_kind(mod) - if (!json) { - transitive_text |> push(" {mod_name} // {kind}") - } else { - transitive_entries |> emplace(RequireEntry(name = mod_name, source = kind)) - } - module_for_each_dependency(mod) $(var dep : Module?; var is_pub : bool) { - let dep_name = string(dep.name) - if (!key_exists(visited, dep_name)) { - visited |> insert(dep_name, true) - queue |> push(dep) - } - } - } - if (json) { - sort(transitive_entries) $(a, b : RequireEntry) { - return a.name < b.name - } - let result = RequiresResult(direct <- direct_entries, transitive <- transitive_entries) - return sprint_json(result, false) - } - sort(transitive_text) - return build_string() $(var w) { - if (!empty(direct_text)) { - write(w, "Direct requires:\n") - for (d in direct_text) { - write(w, "{d}\n") - } - } - if (!empty(transitive_text)) { - if (!empty(direct_text)) { - write(w, "\n") - } - write(w, "Transitive dependencies ({length(transitive_text)}):\n") - for (t in transitive_text) { - write(w, "{t}\n") - } - } - if (empty(direct_text) && empty(transitive_text)) { - write(w, "No requires found.\n") - } - } - } +def do_list_requires(file : string; project : string = ""; project_root : string = ""; json : bool = false) : string { + return run_mcp_subtool("list_requires", [file, project, json ? "true" : "false"], project_root) } diff --git a/utils/mcp/tools/list_types.das b/utils/mcp/tools/list_types.das index 114c46fde6..bb949b89ce 100644 --- a/utils/mcp/tools/list_types.das +++ b/utils/mcp/tools/list_types.das @@ -7,6 +7,6 @@ require common public //! Thin popen wrapper. Real logic lives in subtools/list_types.das so //! macro state from compile_file doesn't leak across MCP calls. -def do_list_types(file : string; project : string = "") : string { - return run_mcp_subtool("list_types", [file, project]) +def do_list_types(file : string; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("list_types", [file, project], project_root) } diff --git a/utils/mcp/tools/live.das b/utils/mcp/tools/live.das index 4ec8c3ffac..14441f3117 100644 --- a/utils/mcp/tools/live.das +++ b/utils/mcp/tools/live.das @@ -107,7 +107,30 @@ def live_is_running(port : string) : bool { return running } -def do_live_launch(file, project, port : string) : string { +// daslang-live script_args composition. Extracted so tests can pin the +// flag-ordering invariants without spawning a subprocess. Slash-replace on +// Windows happens here so the resulting string is shell-ready for Start-Process. +def public build_live_script_args(file, project, project_root : string; + is_windows : bool) : string { + var launch_file = clone_string(file) + var launch_project = clone_string(project) + var launch_project_root = clone_string(project_root) + if (is_windows) { + launch_file = replace(launch_file, "/", "\\") + launch_project = replace(launch_project, "/", "\\") + launch_project_root = replace(launch_project_root, "/", "\\") + } + var script_args = "-cwd \"{launch_file}\"" + if (!empty(launch_project)) { + script_args = "-project \"{launch_project}\" {script_args}" + } + if (!empty(launch_project_root)) { + script_args = "-project_root \"{launch_project_root}\" {script_args}" + } + return script_args +} + +def do_live_launch(file, project, project_root, port : string) : string { let p = empty(port) ? "9090" : port // Check if already running if (live_is_running(p)) { @@ -124,19 +147,20 @@ def do_live_launch(file, project, port : string) : string { live_exe = "{root}/bin/{exe_name}" } if (!stat(live_exe).is_valid) return make_tool_result("daslang-live not found in {root}/bin/Release/ or {root}/bin/", true) - // Build launch command — daslang-live -cwd handles working directory + // Build launch command — daslang-live -cwd handles working directory. + // CRITICAL: resolve file / project / project_root to absolute paths BEFORE + // composing the args. daslang-live's `-cwd` flag chdir's to the script's + // folder (utils/daslang-live/main.cpp:696-707) BEFORE running + // require_dynamic_modules — so a relative `-project_root

` would + // re-resolve against the new cwd and fail to find

/modules/. var launch_exe = clone_string(live_exe) - var launch_file = clone_string(file) - var launch_project = clone_string(project) if (is_windows) { launch_exe = replace(launch_exe, "/", "\\") - launch_file = replace(launch_file, "/", "\\") - launch_project = replace(launch_project, "/", "\\") - } - var script_args = "-cwd \"{launch_file}\"" - if (!empty(launch_project)) { - script_args = "-project \"{launch_project}\" {script_args}" } + let abs_file = resolve_path(file) + let abs_project = empty(project) ? "" : resolve_path(project) + let abs_project_root = empty(project_root) ? "" : resolve_path(project_root) + let script_args = build_live_script_args(abs_file, abs_project, abs_project_root, is_windows) let launcher = "{root}/_mcp_live_launch" if (is_windows) { let ps1 = replace("{launcher}.ps1", "/", "\\") diff --git a/utils/mcp/tools/program_log.das b/utils/mcp/tools/program_log.das index dc387af2eb..4398029b0c 100644 --- a/utils/mcp/tools/program_log.das +++ b/utils/mcp/tools/program_log.das @@ -3,30 +3,12 @@ options no_unused_function_arguments = false options no_unused_block_arguments = false require common public -require daslib/ast_boost -def do_program_log(file, func_name : string; project : string = "") : string { - // filter to specific function — use ast_dump source mode approach - if (!empty(func_name)) return compile_program(file, true, false, project) $(program, issues) { - let thisMod = get_this_module(program) - var result = "" - var found = false - for_each_function(thisMod, "") $(func) { - if (func.flags.builtIn || func.flags._lambda || func.flags.generated - || func.name != func_name) return - found = true - result += describe_function(func) - result += "\n" - } - if (!found) return "Function '{func_name}' not found in {file}" - return result - } - // full program log — use C++ Program::operator<< - return compile_program(file, true, false, project) $(program, issues) { - var result = describe_program(program) - if (!empty(issues)) { - result += "\nWarnings:\n{issues}" - } - return result - } +//! Thin popen wrapper. Real logic lives in subtools/program_log.das so +//! macro state from compile_file doesn't leak across MCP calls AND so +//! any shared module the compiled program requires (dasModuleImgui, etc.) +//! loads + unloads on the subtool's process lifecycle, not the MCP server's. + +def do_program_log(file, func_name : string; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("program_log", [file, func_name, project], project_root) } diff --git a/utils/mcp/tools/run_script.das b/utils/mcp/tools/run_script.das index 75db6690b4..e2dbf3fe02 100644 --- a/utils/mcp/tools/run_script.das +++ b/utils/mcp/tools/run_script.das @@ -4,7 +4,7 @@ options no_unused_block_arguments = false require common public -def do_run_script(file, code : string; timeout_str : string = ""; track_allocations : bool = false; project : string = "") : string { +def do_run_script(file, code : string; timeout_str : string = ""; track_allocations : bool = false; project : string = ""; project_root : string = "") : string { let project_err = validate_project_arg(project) if (!empty(project_err)) return make_tool_result(project_err, true) let exe = get_daslang_exe() @@ -27,6 +27,10 @@ def do_run_script(file, code : string; timeout_str : string = ""; track_allocati let timeout_sec = empty(timeout_str) ? 30.0 : float(to_int(timeout_str)) var output : string var argv <- [exe] + if (!empty(project_root)) { + argv |> push("-project_root") + argv |> push(project_root) + } if (!empty(project)) { argv |> push("-project") argv |> push(string(project)) diff --git a/utils/mcp/tools/run_test.das b/utils/mcp/tools/run_test.das index 88016531ad..20bdbbd3e0 100644 --- a/utils/mcp/tools/run_test.das +++ b/utils/mcp/tools/run_test.das @@ -5,12 +5,21 @@ options no_unused_block_arguments = false require common public require daslib/fio -def do_run_test(file : string; project : string = ""; timeout_str : string = ""; json : bool = false; failures_only : bool = false) : string { +def do_run_test(file : string; project : string = ""; project_root : string = ""; timeout_str : string = ""; json : bool = false; failures_only : bool = false) : string { let exe = get_daslang_exe() if (empty(exe)) return make_tool_result("Cannot determine daslang executable path", true) let test_file = resolve_path(file) let dastest_main = "{get_das_root()}/dastest/dastest.das" - var argv <- [exe, dastest_main, "--", "--test", test_file] + // -project_root is a daslang CLI flag and must come before the script path. + var argv <- [exe] + if (!empty(project_root)) { + argv |> push("-project_root") + argv |> push(project_root) + } + argv |> push(dastest_main) + argv |> push("--") + argv |> push("--test") + argv |> push(test_file) if (!empty(project)) { argv |> push("--project") argv |> push(string(project)) diff --git a/utils/mcp/tools/type_of.das b/utils/mcp/tools/type_of.das index 6e960486e7..4536b63da2 100644 --- a/utils/mcp/tools/type_of.das +++ b/utils/mcp/tools/type_of.das @@ -3,52 +3,12 @@ options no_unused_function_arguments = false options no_unused_block_arguments = false require common public -require daslib/ast_cursor -require daslib/ast_boost -def public do_type_of(file : string; line_str, col_str : string; no_opt_str : string = ""; project : string = "") : string { - let line = to_int(line_str) - let col = to_int(col_str) - if (line <= 0 || col <= 0) return make_tool_result("Invalid line or column (must be >= 1)", true) - let no_opt = no_opt_str == "true" - return compile_program(file, true, no_opt, project) $(program; issues) { - var hits <- find_at_cursor(program, file, line, col) - if (empty(hits)) return "No expression found at {file}:{line}:{col}" - return build_string() $(var w) { - for (hit in hits) { - write(w, "{hit.rtti}") - if (!empty(hit.name)) { - write(w, "({hit.name})") - } - if (hit.expr != null && hit.expr._type != null) { - write(w, " : {describe(hit.expr._type)}") - // add extra detail for struct/enum types - let td = hit.expr._type - if (td.structType != null) { - let st = td.structType - let st_file = lineinfo_file(st.at) - if (!empty(st_file)) { - write(w, " // {st_file}:{int(st.at.line)}") - } - } - if (td.enumType != null) { - let en = td.enumType - let en_file = lineinfo_file(en.at) - if (!empty(en_file)) { - write(w, " // {en_file}:{int(en.at.line)}") - } - } - } else { - write(w, " : (no type)") - } - write(w, "\n") - } - } - } -} +//! Thin popen wrapper. Real logic lives in subtools/type_of.das so macro +//! state from compile_file doesn't leak across MCP calls AND so any shared +//! module the compiled program requires (dasModuleImgui, etc.) loads + +//! unloads on the subtool's process lifecycle, not the MCP server's. -// reuse lineinfo_file from goto_definition — but since modules are separate, define locally -def lineinfo_file(at : LineInfo) : string { - if (at.fileInfo != null) return string(at.fileInfo.name) - return "" +def public do_type_of(file : string; line_str, col_str : string; no_opt_str : string = ""; project : string = ""; project_root : string = "") : string { + return run_mcp_subtool("type_of", [file, line_str, col_str, no_opt_str, project], project_root) }