From 868c8ae0d16235525d55ea8c62906c4b202c0c92 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 24 Mar 2026 20:54:55 -0700 Subject: [PATCH 1/8] disable aot on ast match until cows come home --- daslib/ast_match.das | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daslib/ast_match.das b/daslib/ast_match.das index 5715f3bf74..8c3ed3e548 100644 --- a/daslib/ast_match.das +++ b/daslib/ast_match.das @@ -5,6 +5,10 @@ options no_unused_function_arguments = false options strict_smart_pointers = true options stack = 1_048_576 +// this module is not AOT compiled. there are major issues with current ExpressionPtr and TypeDeclPtr +// return-call patterns, which we are not going to address - until we get rid of smart_ptr altogether on das side +options no_aot + module ast_match shared public //! AST pattern matching — reverse reification. From 0200e6565615664d6baaf22643f4bcd1d004e502 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 24 Mar 2026 21:09:00 -0700 Subject: [PATCH 2/8] correct output folders for all_utils.exe --- utils/CMakeLists.txt | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 13e2542aae..d31be10df7 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -1,6 +1,7 @@ # Build daslang utilities as standalone executables using -exe mode. # Each utility with a main.das is compiled via daslang -exe and the -# resulting binary is placed into ${PROJECT_SOURCE_DIR}/bin/. +# resulting binary is placed next to the daslang compiler binary +# (e.g. bin/Release/ on multi-config generators, bin/ on single-config). set(DAS_UTILS aot @@ -18,33 +19,36 @@ endif() file(GLOB DASLIB_SOURCES ${PROJECT_SOURCE_DIR}/daslib/*.das) file(GLOB DASLLVM_DASLIB_SOURCES ${PROJECT_SOURCE_DIR}/modules/dasLLVM/daslib/*.das) -# daslang -exe appends `.exe` to the output path +# daslang -exe appends `.exe` to the output path. +# CMAKE_CFG_INTDIR resolves to the config name (Release, Debug, etc.) +# on multi-config generators, and to "." on single-config generators — +# so utilities always land next to the compiler binary. +set(UTIL_BIN_DIR ${PROJECT_SOURCE_DIR}/bin/${CMAKE_CFG_INTDIR}) + foreach(util ${DAS_UTILS}) set(UTIL_MAIN ${PROJECT_SOURCE_DIR}/utils/${util}/main.das) - set(UTIL_OUT ${PROJECT_SOURCE_DIR}/bin/${util}.exe) add_custom_command( - OUTPUT ${UTIL_OUT} - COMMAND daslang -exe -output ${PROJECT_SOURCE_DIR}/bin/${util} ${UTIL_MAIN} + OUTPUT ${UTIL_BIN_DIR}/${util}.exe + COMMAND daslang -exe -output ${UTIL_BIN_DIR}/${util} ${UTIL_MAIN} DEPENDS ${UTIL_MAIN} daslang ${DASLIB_SOURCES} ${DASLLVM_DASLIB_SOURCES} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMENT "Building ${util} executable (daslang -exe)" ) - add_custom_target(${util}_exe DEPENDS ${UTIL_OUT}) + add_custom_target(${util}_exe DEPENDS ${UTIL_BIN_DIR}/${util}.exe) endforeach() SET(TEST_MAIN ${PROJECT_SOURCE_DIR}/dastest/dastest.das) -SET(TEST_OUT ${PROJECT_SOURCE_DIR}/bin/dastest.exe) add_custom_command( - OUTPUT ${TEST_OUT} - COMMAND daslang -exe -output ${PROJECT_SOURCE_DIR}/bin/dastest ${TEST_MAIN} + OUTPUT ${UTIL_BIN_DIR}/dastest.exe + COMMAND daslang -exe -output ${UTIL_BIN_DIR}/dastest ${TEST_MAIN} DEPENDS ${TEST_MAIN} daslang ${DASLIB_SOURCES} ${DASLLVM_DASLIB_SOURCES} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMENT "Building dastest executable (daslang -exe)" ) -add_custom_target(dastest_exe DEPENDS ${TEST_OUT}) +add_custom_target(dastest_exe DEPENDS ${UTIL_BIN_DIR}/dastest.exe) add_custom_target(all_utils_exe) @@ -71,3 +75,11 @@ ADD_CUSTOM_TARGET(run_utils_tests WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMENT "Running utils tests" ) + +# Install utility executables into the flat bin/ directory. +foreach(util ${DAS_UTILS}) + install(PROGRAMS ${UTIL_BIN_DIR}/${util}.exe + DESTINATION ${DAS_INSTALL_BINDIR}) +endforeach() +install(PROGRAMS ${UTIL_BIN_DIR}/dastest.exe + DESTINATION ${DAS_INSTALL_BINDIR}) From 06430ee51d2c3fed9be15e7dd0061820cd3105ef Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 24 Mar 2026 22:04:22 -0700 Subject: [PATCH 3/8] Add comprehension and mmFlags matching to ast_match Support ExprArrayComprehension patterns (array, table, generator) with correct flag enforcement and ExprMakeBlock mmFlags matching to distinguish blocks, lambdas, and local functions. 24 new tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- daslib/ast_match.das | 99 ++++++++++++ tests/ast_match/test_comprehensions.das | 202 ++++++++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 tests/ast_match/test_comprehensions.das diff --git a/daslib/ast_match.das b/daslib/ast_match.das index 8c3ed3e548..39ebd9d930 100644 --- a/daslib/ast_match.das +++ b/daslib/ast_match.das @@ -1046,6 +1046,29 @@ def private generate_match(pattern : Expression?; actual_var : string; var body if (pattern is ExprMakeBlock) { body |> emplace_new <| qm_rtti_guard(at, actual_var, "ExprMakeBlock") let pat_mkblk = pattern as ExprMakeBlock + // Match mmFlags (isLambda, isLocalFunction) to distinguish block/lambda/local-function + let mkblk_cast = next_var(index) + var inscope as_val_mkb <- make_var_ref(at, actual_var) + var inscope as_cast_mkb <- new ExprAsVariant(at = at, value <- as_val_mkb, name := "ExprMakeBlock") + body |> emplace_new <| qmacro_expr(${ var $i(mkblk_cast) = $e(as_cast_mkb); }) + if (pat_mkblk.mmFlags.isLambda) { + var inscope cl <- qmacro(!$i(mkblk_cast).mmFlags.isLambda) + var inscope fl <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, cl, fl) + } else { + var inscope cl <- qmacro($i(mkblk_cast).mmFlags.isLambda) + var inscope fl <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, cl, fl) + } + if (pat_mkblk.mmFlags.isLocalFunction) { + var inscope clf <- qmacro(!$i(mkblk_cast).mmFlags.isLocalFunction) + var inscope flf <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, clf, flf) + } else { + var inscope clf <- qmacro($i(mkblk_cast).mmFlags.isLocalFunction) + var inscope flf <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, clf, flf) + } var pat_block = get_ptr(pat_mkblk._block) as ExprBlock // Unwrap ExprMakeBlock → ExprBlock at runtime let blk_var = next_var(index) @@ -1075,6 +1098,82 @@ def private generate_match(pattern : Expression?; actual_var : string; var body return } + // ── ExprArrayComprehension — array/table/generator comprehension ─ + if (pattern is ExprArrayComprehension) { + let pat_ac = pattern as ExprArrayComprehension + let pat_gen = pat_ac.generatorSyntax + let pat_tbl = pat_ac.tableSyntax + // RTTI guard + body |> emplace_new <| qm_rtti_guard(at, actual_var, "ExprArrayComprehension") + // Cast actual to ExprArrayComprehension + let cast_var = next_var(index) + var inscope as_val_ac <- make_var_ref(at, actual_var) + var inscope as_cast_ac <- new ExprAsVariant(at = at, value <- as_val_ac, name := "ExprArrayComprehension") + body |> emplace_new <| qmacro_expr(${ var $i(cast_var) = $e(as_cast_ac); }) + // Match generatorSyntax (both true and false enforced) + if (pat_gen) { + var inscope cg <- qmacro($i(cast_var).generatorSyntax == false) + var inscope fg <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, cg, fg) + } else { + var inscope cg <- qmacro($i(cast_var).generatorSyntax) + var inscope fg <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, cg, fg) + } + // Match tableSyntax (both true and false enforced) + if (pat_tbl) { + var inscope ct <- qmacro($i(cast_var).tableSyntax == false) + var inscope ft <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, ct, ft) + } else { + var inscope ct <- qmacro($i(cast_var).tableSyntax) + var inscope ft <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, ct, ft) + } + // Recurse into exprFor + let pat_for = get_ptr(pat_ac.exprFor) + if (pat_for != null) { + let for_var = next_var(index) + body |> emplace_new <| qmacro_expr(${ var inscope $i(for_var) <- clone_expression($i(cast_var).exprFor); }) + if (!is_wildcard_call(pat_for) && !(pat_for is ExprTag)) { + var inscope nc <- qmacro($i(for_var) == null) + var inscope nf <- qmacro(qm_fail_null()) + body |> emplace_new <| qm_guard(at, nc, nf) + } + generate_match(pat_for, for_var, body, index, at) + } + // Recurse into subexpr + let pat_sub = get_ptr(pat_ac.subexpr) + if (pat_sub != null) { + let sub_var = next_var(index) + body |> emplace_new <| qmacro_expr(${ var inscope $i(sub_var) <- clone_expression($i(cast_var).subexpr); }) + if (!is_wildcard_call(pat_sub) && !(pat_sub is ExprTag)) { + var inscope nc <- qmacro($i(sub_var) == null) + var inscope nf <- qmacro(qm_fail_null()) + body |> emplace_new <| qm_guard(at, nc, nf) + } + generate_match(pat_sub, sub_var, body, index, at) + } + // Handle exprWhere + if (pat_ac.exprWhere != null) { + let pat_where = get_ptr(pat_ac.exprWhere) + let where_var = next_var(index) + body |> emplace_new <| qmacro_expr(${ var inscope $i(where_var) <- clone_expression($i(cast_var).exprWhere); }) + if (!is_wildcard_call(pat_where) && !(pat_where is ExprTag)) { + var inscope nc <- qmacro($i(where_var) == null) + var inscope nf <- qmacro(qm_fail_null()) + body |> emplace_new <| qm_guard(at, nc, nf) + } + generate_match(pat_where, where_var, body, index, at) + } else { + // Pattern has no where clause — actual shouldn't either + var inscope nc <- qmacro($i(cast_var).exprWhere != null) + var inscope nf <- qmacro(qm_fail_field($i(actual_var))) + body |> emplace_new <| qm_guard(at, nc, nf) + } + return + } + // ── ExprFor — handle iteratorsTags for $i capture ───────── if (pattern is ExprFor) { var pat_for = pattern as ExprFor diff --git a/tests/ast_match/test_comprehensions.das b/tests/ast_match/test_comprehensions.das new file mode 100644 index 0000000000..dde6797d5c --- /dev/null +++ b/tests/ast_match/test_comprehensions.das @@ -0,0 +1,202 @@ +options gen2 +options indenting = 4 +options rtti + +require daslib/ast +require daslib/ast_match +require daslib/ast_boost +require daslib/templates_boost +require dastest/testing_boost public + +// ── Array comprehension ───────────────────────────────────────────────── + +[test] +def test_array_comprehension_match(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x]) + let r = qmatch(expr, [for (x in range(10)); x]) + t |> success(r.matched, "array comprehension should match") +} + +[test] +def test_array_comprehension_wrong_source(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x]) + let r = qmatch(expr, [for (x in range(5)); x]) + t |> success(!r.matched, "wrong source should fail") +} + +[test] +def test_array_comprehension_wrong_subexpr(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x]) + let r = qmatch(expr, [for (x in range(10)); y]) + t |> success(!r.matched, "wrong subexpr should fail") +} + +[test] +def test_array_comprehension_with_where(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x; where x > 5]) + let r = qmatch(expr, [for (x in range(10)); x; where x > 5]) + t |> success(r.matched, "array comprehension with where should match") +} + +[test] +def test_array_comprehension_where_mismatch(t : T?) { + // Pattern has where, actual doesn't + var inscope expr <- qmacro([for (x in range(10)); x]) + let r = qmatch(expr, [for (x in range(10)); x; where x > 5]) + t |> success(!r.matched, "pattern with where vs actual without should fail") +} + +[test] +def test_array_comprehension_no_where_mismatch(t : T?) { + // Pattern has no where, actual does + var inscope expr <- qmacro([for (x in range(10)); x; where x > 5]) + let r = qmatch(expr, [for (x in range(10)); x]) + t |> success(!r.matched, "pattern without where vs actual with should fail") +} + +[test] +def test_array_comprehension_capture_subexpr(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x * 2]) + var inscope sub : ExpressionPtr + let r = qmatch(expr, [for (x in range(10)); $e(sub)]) + t |> success(r.matched, "should match with $e capture on subexpr") + if (r.matched) { + t |> equal(string(get_ptr(sub).__rtti), "ExprOp2") + } +} + +[test] +def test_array_comprehension_capture_iterator(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x]) + var name : string + let r = qmatch(expr, [for ($i(name) in range(10)); x]) + t |> success(r.matched, "should match with $i on iterator") + if (r.matched) { + t |> equal(name, "x") + } +} + +[test] +def test_array_comprehension_wildcard_subexpr(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x * x + 1]) + let r = qmatch(expr, [for (x in range(10)); _any()]) + t |> success(r.matched, "wildcard subexpr should match anything") +} + +// ── Array vs table mismatch ───────────────────────────────────────────── + +[test] +def test_array_vs_table_mismatch(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x]) + let r = qmatch(expr, {for (x in range(10)); x}) + t |> success(!r.matched, "array pattern vs table actual should fail") +} + +[test] +def test_table_vs_array_mismatch(t : T?) { + var inscope expr <- qmacro({for (x in range(10)); x}) + let r = qmatch(expr, [for (x in range(10)); x]) + t |> success(!r.matched, "table pattern vs array actual should fail") +} + +// ── Table comprehension ───────────────────────────────────────────────── + +[test] +def test_table_comprehension_match(t : T?) { + var inscope expr <- qmacro({for (x in range(10)); x => x * x}) + let r = qmatch(expr, {for (x in range(10)); x => x * x}) + t |> success(r.matched, "table comprehension should match") +} + +[test] +def test_table_comprehension_key_only(t : T?) { + var inscope expr <- qmacro({for (x in range(10)); x}) + let r = qmatch(expr, {for (x in range(10)); x}) + t |> success(r.matched, "table comprehension key-only should match") +} + +[test] +def test_table_comprehension_capture_subexpr(t : T?) { + var inscope expr <- qmacro({for (x in range(10)); x => x * x}) + var inscope sub : ExpressionPtr + let r = qmatch(expr, {for (x in range(10)); $e(sub)}) + t |> success(r.matched, "should match with $e capture on table subexpr") + if (r.matched) { + // subexpr is x => x * x, which is ExprMakeTuple + t |> success(get_ptr(sub) != null, "captured subexpr should not be null") + } +} + +[test] +def test_table_comprehension_wrong_subexpr(t : T?) { + var inscope expr <- qmacro({for (x in range(10)); x => x * x}) + let r = qmatch(expr, {for (x in range(10)); x => x + x}) + t |> success(!r.matched, "wrong table subexpr should fail") +} + +// ── Generator comprehension ───────────────────────────────────────────── + +[test] +def test_generator_comprehension_match(t : T?) { + var inscope expr <- qmacro([iterator for(x in range(10)); x]) + let r = qmatch(expr, [iterator for(x in range(10)); x]) + t |> success(r.matched, "generator comprehension should match") +} + +[test] +def test_generator_vs_array_mismatch(t : T?) { + var inscope expr <- qmacro([iterator for(x in range(10)); x]) + let r = qmatch(expr, [for (x in range(10)); x]) + t |> success(!r.matched, "generator pattern vs array should fail") +} + +[test] +def test_array_vs_generator_mismatch(t : T?) { + var inscope expr <- qmacro([for (x in range(10)); x]) + let r = qmatch(expr, [iterator for(x in range(10)); x]) + t |> success(!r.matched, "array pattern vs generator should fail") +} + +// ── ExprMakeBlock mmFlags ─────────────────────────────────────────────── + +[test] +def test_block_matches_block(t : T?) { + var inscope expr <- qmacro($ { return 1; }) + let r = qmatch(expr, $ { return 1; }) + t |> success(r.matched, "plain block should match plain block") +} + +[test] +def test_block_not_lambda(t : T?) { + var inscope expr <- qmacro(@() { return 1; }) + let r = qmatch(expr, $ { return 1; }) + t |> success(!r.matched, "plain block pattern should not match lambda") +} + +[test] +def test_lambda_matches_lambda(t : T?) { + var inscope expr <- qmacro(@(x) { return x + 1; }) + let r = qmatch(expr, @(x) { return x + 1; }) + t |> success(r.matched, "lambda should match lambda") +} + +[test] +def test_lambda_not_block(t : T?) { + var inscope expr <- qmacro($ { return 1; }) + let r = qmatch(expr, @() { return 1; }) + t |> success(!r.matched, "lambda pattern should not match plain block") +} + +[test] +def test_local_function_matches(t : T?) { + var inscope expr <- qmacro(@@(x) { return x + 1; }) + let r = qmatch(expr, @@(x) { return x + 1; }) + t |> success(r.matched, "local function should match local function") +} + +[test] +def test_local_function_not_lambda(t : T?) { + var inscope expr <- qmacro(@(x) { return x + 1; }) + let r = qmatch(expr, @@(x) { return x + 1; }) + t |> success(!r.matched, "local function pattern should not match lambda") +} From 2454822ee56f5bb16bfd81391bb2afd3f65bb2d3 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 24 Mar 2026 22:29:23 -0700 Subject: [PATCH 4/8] Match local functions through ExprAddr (post-inference) Add qm_convert_local_function to reverse generateLocalFunction: ExprAddr to ExprMakeBlock(isLocalFunction=true). When pattern has @@() and actual is ExprAddr, the handler resolves it transparently. Also replace __rtti string comparisons with is/as where applicable. Co-Authored-By: Claude Opus 4.6 (1M context) --- daslib/ast_match.das | 60 +++++++++++++++++++++++-- tests/ast_match/test_comprehensions.das | 43 ++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/daslib/ast_match.das b/daslib/ast_match.das index 39ebd9d930..5a4d7dd449 100644 --- a/daslib/ast_match.das +++ b/daslib/ast_match.das @@ -365,6 +365,47 @@ def qm_make_zero_const(call_name : string) : ExpressionPtr { return <- default } +def qm_convert_local_function(var expr : ExpressionPtr) : ExpressionPtr { + //! Reverse generateLocalFunction: ExprAddr → ExprMakeBlock(isLocalFunction=true). + //! Returns null if the expression is not a generated local function. + if (!(expr is ExprAddr)) { + return <- default + } + let ea = expr as ExprAddr + if (ea.func == null || !ea.func.flags.generated) { + return <- default + } + if (find(string(ea.func.name), "`function") < 0) { + return <- default + } + let fn = ea.func + // Reconstruct ExprBlock with args + return type + body + var inscope blk <- new ExprBlock(at = fn.at) + blk.returnType |> move_new <| clone_type(fn.result) + for (arg in fn.arguments) { + blk.arguments |> emplace_new <| clone_variable(arg) + } + // Clone body statements + var inscope fn_body <- clone_expression(fn.body) + let fn_blk = fn_body as ExprBlock + if (fn_blk != null) { + for (stmt in fn_blk.list) { + blk.list |> emplace_new <| clone_expression(stmt) + } + } + // Wrap in ExprMakeBlock with isLocalFunction flag + return <- new ExprMakeBlock(at = fn.at, _block <- blk, mmFlags = ExprMakeBlockFlags.isLocalFunction) +} + +def qm_resolve_local_function(var expr : ExpressionPtr) : ExpressionPtr { + //! If expr is ExprMakeBlock, clone it. If ExprAddr (post-inference local function), + //! convert back to ExprMakeBlock. Returns null if neither works. + if (expr is ExprMakeBlock) { + return <- clone_expression(expr) + } + return <- qm_convert_local_function(expr) +} + // ── Compile-time AST builders ─────────────────────────────────────────── [macro_function] @@ -799,7 +840,7 @@ def private has_tags_in_args(call : ExprCall?) : bool { // would prevent constant folding, so the call stays as ExprCall and hits the // normal matching path instead of reaching here. for (arg in call.arguments) { - if (arg != null && arg.__rtti == "ExprTag") { + if (arg != null && arg is ExprTag) { return true } } @@ -1044,11 +1085,22 @@ def private generate_match(pattern : Expression?; actual_var : string; var body // ── ExprMakeBlock — lightweight inline block matching ─────── if (pattern is ExprMakeBlock) { - body |> emplace_new <| qm_rtti_guard(at, actual_var, "ExprMakeBlock") let pat_mkblk = pattern as ExprMakeBlock + // For local function patterns, also accept ExprAddr (post-inference form) + var eff_actual = actual_var + if (pat_mkblk.mmFlags.isLocalFunction) { + let eff_var = next_var(index) + body |> emplace_new <| qmacro_expr(${ var inscope $i(eff_var) <- qm_resolve_local_function($i(actual_var)); }) + var inscope nc <- qmacro($i(eff_var) == null) + var inscope nf <- qmacro(qm_fail_rtti($i(actual_var))) + body |> emplace_new <| qm_guard(at, nc, nf) + eff_actual = eff_var + } else { + body |> emplace_new <| qm_rtti_guard(at, actual_var, "ExprMakeBlock") + } // Match mmFlags (isLambda, isLocalFunction) to distinguish block/lambda/local-function let mkblk_cast = next_var(index) - var inscope as_val_mkb <- make_var_ref(at, actual_var) + var inscope as_val_mkb <- make_var_ref(at, eff_actual) var inscope as_cast_mkb <- new ExprAsVariant(at = at, value <- as_val_mkb, name := "ExprMakeBlock") body |> emplace_new <| qmacro_expr(${ var $i(mkblk_cast) = $e(as_cast_mkb); }) if (pat_mkblk.mmFlags.isLambda) { @@ -1072,7 +1124,7 @@ def private generate_match(pattern : Expression?; actual_var : string; var body var pat_block = get_ptr(pat_mkblk._block) as ExprBlock // Unwrap ExprMakeBlock → ExprBlock at runtime let blk_var = next_var(index) - body |> emplace_new <| qmacro_expr(${ var inscope $i(blk_var) <- clone_expression(($i(actual_var) as ExprMakeBlock)._block); }) + body |> emplace_new <| qmacro_expr(${ var inscope $i(blk_var) <- clone_expression(($i(eff_actual) as ExprMakeBlock)._block); }) body |> emplace_new <| qm_rtti_guard(at, blk_var, "ExprBlock") let cast_var = next_var(index) var inscope as_val_b <- make_var_ref(at, blk_var) diff --git a/tests/ast_match/test_comprehensions.das b/tests/ast_match/test_comprehensions.das index dde6797d5c..330163d634 100644 --- a/tests/ast_match/test_comprehensions.das +++ b/tests/ast_match/test_comprehensions.das @@ -1,8 +1,10 @@ options gen2 options indenting = 4 options rtti +options no_coverage require daslib/ast +require daslib/rtti require daslib/ast_match require daslib/ast_boost require daslib/templates_boost @@ -200,3 +202,44 @@ def test_local_function_not_lambda(t : T?) { let r = qmatch(expr, @@(x) { return x + 1; }) t |> success(!r.matched, "local function pattern should not match lambda") } + +// ── Post-inference local function matching (ExprAddr fallback) ────────── + +[export] +def target_with_local_fn() { + var f = @@(x : int) { return x + 1; } + feint("{f}") +} + +[export] +def target_with_local_fn_wrong() { + var f = @@(x : int) { return x - 1; } + feint("{f}") +} + +[test] +def test_local_fn_post_inference(t : T?) { + var inscope func <- find_module_function_via_rtti(compiling_module(), @@target_with_local_fn) + t |> success(get_ptr(func) != null, "should find target_with_local_fn") + if (get_ptr(func) == null) { + return + } + let r = qmatch_function(func) $() { + var f = @@(x : int) { return x + 1; } + feint("{f}") + } + t |> success(r.matched, "should match post-inference local function via ExprAddr") +} + +[test] +def test_local_fn_post_inference_wrong_body(t : T?) { + var inscope func <- find_module_function_via_rtti(compiling_module(), @@target_with_local_fn) + if (get_ptr(func) == null) { + return + } + let r = qmatch_function(func) $() { + var f = @@(x : int) { return x - 1; } + feint("{f}") + } + t |> success(!r.matched, "wrong body in local function should fail") +} From 97087dfc93f8de70a0875c808f721a2cfda1d3ae Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 24 Mar 2026 22:32:59 -0700 Subject: [PATCH 5/8] Add tag capture tests for post-inference local functions Test $e capture on body, $i on let variable name, and multi-arg matching. Use is/as instead of __rtti string comparisons. Use emplace_new and direct field init for cleaner code. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/ast_match/test_comprehensions.das | 53 +++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/ast_match/test_comprehensions.das b/tests/ast_match/test_comprehensions.das index 330163d634..4f65014dcb 100644 --- a/tests/ast_match/test_comprehensions.das +++ b/tests/ast_match/test_comprehensions.das @@ -243,3 +243,56 @@ def test_local_fn_post_inference_wrong_body(t : T?) { } t |> success(!r.matched, "wrong body in local function should fail") } + +[test] +def test_local_fn_capture_body(t : T?) { + var inscope func <- find_module_function_via_rtti(compiling_module(), @@target_with_local_fn) + if (get_ptr(func) == null) { + return + } + var inscope body_expr : ExpressionPtr + let r = qmatch_function(func) $() { + var f = @@(x : int) { $e(body_expr); } + feint("{f}") + } + t |> success(r.matched, "should match with $e capture on local fn body") + if (r.matched) { + t |> equal(string(get_ptr(body_expr).__rtti), "ExprReturn") + } +} + +[export] +def target_with_local_fn_two_args() { + var f = @@(a : int; b : float) { return float(a) + b; } + feint("{f}") +} + +[test] +def test_local_fn_match_two_args(t : T?) { + var inscope func <- find_module_function_via_rtti(compiling_module(), @@target_with_local_fn_two_args) + if (get_ptr(func) == null) { + return + } + let r = qmatch_function(func) $() { + var f = @@(a : int; b : float) { return float(a) + b; } + feint("{f}") + } + t |> success(r.matched, "should match local fn with two args") +} + +[test] +def test_local_fn_capture_var_name(t : T?) { + var inscope func <- find_module_function_via_rtti(compiling_module(), @@target_with_local_fn) + if (get_ptr(func) == null) { + return + } + var vname : string + let r = qmatch_function(func) $() { + var $i(vname) = @@(x : int) { return x + 1; } + feint("{f}") + } + t |> success(r.matched, "should match with $i capture on let variable name") + if (r.matched) { + t |> equal(vname, "f") + } +} From fcc742b1b38f425d1363b49a335abf2bee443442 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 24 Mar 2026 22:57:44 -0700 Subject: [PATCH 6/8] Guard utils CMakeLists.txt with MSVC check daslang -exe requires MSVC toolchain, skip on other platforms. Co-Authored-By: Claude Opus 4.6 (1M context) --- utils/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index d31be10df7..56518759ba 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -2,6 +2,11 @@ # Each utility with a main.das is compiled via daslang -exe and the # resulting binary is placed next to the daslang compiler binary # (e.g. bin/Release/ on multi-config generators, bin/ on single-config). +# Currently Windows/MSVC only — daslang -exe requires MSVC toolchain. + +if(NOT MSVC) + return() +endif() set(DAS_UTILS aot From be45cbfa2f8e21bb3fb63d602aa6c26cc9c08252 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 24 Mar 2026 22:59:10 -0700 Subject: [PATCH 7/8] Fix utils CMakeLists.txt path for non-MSVC builds Use bin// on multi-config generators (MSVC) and plain bin/ on single-config generators (Make/Ninja). Fixes bin/./ path issue on Linux CI. Co-Authored-By: Claude Opus 4.6 (1M context) --- utils/CMakeLists.txt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 56518759ba..34e1e132c6 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -2,11 +2,6 @@ # Each utility with a main.das is compiled via daslang -exe and the # resulting binary is placed next to the daslang compiler binary # (e.g. bin/Release/ on multi-config generators, bin/ on single-config). -# Currently Windows/MSVC only — daslang -exe requires MSVC toolchain. - -if(NOT MSVC) - return() -endif() set(DAS_UTILS aot @@ -25,10 +20,13 @@ file(GLOB DASLIB_SOURCES ${PROJECT_SOURCE_DIR}/daslib/*.das) file(GLOB DASLLVM_DASLIB_SOURCES ${PROJECT_SOURCE_DIR}/modules/dasLLVM/daslib/*.das) # daslang -exe appends `.exe` to the output path. -# CMAKE_CFG_INTDIR resolves to the config name (Release, Debug, etc.) -# on multi-config generators, and to "." on single-config generators — -# so utilities always land next to the compiler binary. -set(UTIL_BIN_DIR ${PROJECT_SOURCE_DIR}/bin/${CMAKE_CFG_INTDIR}) +# On multi-config generators (MSVC), place next to daslang in bin//. +# On single-config generators (Make/Ninja), place in bin/. +if(MSVC) + set(UTIL_BIN_DIR ${PROJECT_SOURCE_DIR}/bin/${CMAKE_CFG_INTDIR}) +else() + set(UTIL_BIN_DIR ${PROJECT_SOURCE_DIR}/bin) +endif() foreach(util ${DAS_UTILS}) set(UTIL_MAIN ${PROJECT_SOURCE_DIR}/utils/${util}/main.das) From cce11275b768ea1e9a5da8e939531cb9534195d6 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 24 Mar 2026 23:03:35 -0700 Subject: [PATCH 8/8] Add reserve before emplace loop in qm_extract_stmts Fixes PERF006: emplace in loop without prior reserve. Co-Authored-By: Claude Opus 4.6 (1M context) --- daslib/ast_match.das | 1 + utils/CMakeLists.txt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/daslib/ast_match.das b/daslib/ast_match.das index 5a4d7dd449..7f16bd4c63 100644 --- a/daslib/ast_match.das +++ b/daslib/ast_match.das @@ -286,6 +286,7 @@ def qm_real_call_arg_index(args : dasvector`smart_ptr`Expression; logical_idx : def qm_extract_stmts(var blk_expr : ExpressionPtr; from_pos : int; to_pos : int; var result : array&) : void { //! Extract statements [from_pos, to_pos) from a block into an array (cloned). var blk = blk_expr as ExprBlock + result |> reserve(to_pos - from_pos) for (i in range(from_pos, to_pos)) { { var inscope cloned <- clone_expression(blk.list[i]) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 34e1e132c6..2b5164ad45 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -79,10 +79,10 @@ ADD_CUSTOM_TARGET(run_utils_tests COMMENT "Running utils tests" ) -# Install utility executables into the flat bin/ directory. +# Install utility executables (OPTIONAL — only present when -exe targets are built). foreach(util ${DAS_UTILS}) install(PROGRAMS ${UTIL_BIN_DIR}/${util}.exe - DESTINATION ${DAS_INSTALL_BINDIR}) + DESTINATION ${DAS_INSTALL_BINDIR} OPTIONAL) endforeach() install(PROGRAMS ${UTIL_BIN_DIR}/dastest.exe - DESTINATION ${DAS_INSTALL_BINDIR}) + DESTINATION ${DAS_INSTALL_BINDIR} OPTIONAL)