diff --git a/daslib/ast_match.das b/daslib/ast_match.das index 5715f3bf74..7f16bd4c63 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. @@ -282,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]) @@ -361,6 +366,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] @@ -795,7 +841,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 } } @@ -1040,12 +1086,46 @@ 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, 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) { + 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) - 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) @@ -1071,6 +1151,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..4f65014dcb --- /dev/null +++ b/tests/ast_match/test_comprehensions.das @@ -0,0 +1,298 @@ +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 +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") +} + +// ── 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") +} + +[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") + } +} diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 13e2542aae..2b5164ad45 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,39 @@ 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. +# 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) - 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 +78,11 @@ ADD_CUSTOM_TARGET(run_utils_tests WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMENT "Running utils tests" ) + +# 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} OPTIONAL) +endforeach() +install(PROGRAMS ${UTIL_BIN_DIR}/dastest.exe + DESTINATION ${DAS_INSTALL_BINDIR} OPTIONAL)