diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a77098f22f..2205f26387 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -143,8 +143,14 @@ jobs: cp web/output/daslang_static.wasm _site/playground/ cp web/output/daslang_static.js _site/files/wasm/ cp web/output/daslang_static.wasm _site/files/wasm/ - # Copy sample data if present - cp -r web/examples/ui/samples _site/playground/samples 2>/dev/null || true + # Pull samples from site/playground/samples — that directory is the + # single staged source of truth: stage_site_playground populates + # the .das sources, stage_site_playground_wasm overlays the + # cross-compiled .wasm artifacts. The HEAD probe in main.js + # (updateEngineAvailability) enables the JIT radio only when the + # .wasm is reachable, so dropping the .wasm here permanently + # disables the toggle in production. + cp -r site/playground/samples _site/playground/samples 2>/dev/null || true # Our Forge-skinned playground/index.html + skin CSS + init shim take precedence. # CodeMirror bundle (codemirror.min.js/css, simple-mode.js, daslang-*.js, # cm-forge.css) is served from /files/cm/ — copied via `cp -r site/files` diff --git a/daslib/linq_fold.das b/daslib/linq_fold.das index 6734e3beb3..c916fd67b7 100644 --- a/daslib/linq_fold.das +++ b/daslib/linq_fold.das @@ -537,15 +537,11 @@ def private emit_counter_lane(var top : Expression?; srcName, accName, itName, s var srcParamType = invoke_src_param_type(top) var bodyStmts : array append_ranges_prelude(bodyStmts, skipExpr, takeExpr, skipWhileCond, skipName, takeCountName, skippingName) - bodyStmts |> push <| qmacro_expr() { + bodyStmts |> push_from <| qmacro_block_to_array() { var $i(accName) = 0 - } - bodyStmts |> push <| qmacro_expr() { for ($i(itName) in $i(srcName)) { $e(loopBody) } - } - bodyStmts |> push <| qmacro_expr() { return $i(accName) } var res = qmacro(invoke($($i(srcName) : $t(srcParamType)) { @@ -638,9 +634,9 @@ def private emit_accumulator_lane( accType.flags.constant = false accType.flags.ref = false } - let valBindName = "`val`{at.line}`{at.column}" - let firstName = "`first`{at.line}`{at.column}" - let cntName = "`cnt`{at.line}`{at.column}" + let valBindName = qn("val", at) + let firstName = qn("first", at) + let cntName = qn("cnt", at) var preludeStmts : array var perMatchStmts : array var returnExpr : Expression? @@ -650,7 +646,7 @@ def private emit_accumulator_lane( } // Value is unused; mirror counter-lane discipline and bind only when projection has if (projection != null && has_sideeffects(projection)) { - let finalBindName = "`vfinal`{at.line}`{at.column}" + let finalBindName = qn("vfinal", at) perMatchStmts |> push <| qmacro_expr() { var $i(finalBindName) = $e(projection) } @@ -771,7 +767,7 @@ def private emit_early_exit_lane( // Ring 2 early-exit lane: first / first_or_default / any / all / contains. var perMatchStmts : array var valueName = itName - let projBindName = "`vproj`{at.line}`{at.column}" + let projBindName = qn("vproj", at) if (projection != null) { perMatchStmts |> push <| qmacro_expr() { let $i(projBindName) = $e(projection) @@ -803,7 +799,7 @@ def private emit_early_exit_lane( } } elif (opName == "first_or_default") { // Bind `d` once at the top of the block (eager evaluation, matches linq.das line 2397). - let defaultName = "`dval`{at.line}`{at.column}" + let defaultName = qn("dval", at) var defaultExpr = clone_expression(terminatorCall.arguments[1]) preludeStmts |> push <| qmacro_expr() { let $i(defaultName) = $e(defaultExpr) @@ -843,7 +839,7 @@ def private emit_early_exit_lane( } } elif (opName == "contains") { // `v` is bound once at the top of the block; per-element compare uses the bound name - let containsValName = "`cval`{at.line}`{at.column}" + let containsValName = qn("cval", at) var valExpr = clone_expression(terminatorCall.arguments[1]) preludeStmts |> push <| qmacro_expr() { let $i(containsValName) = $e(valExpr) @@ -868,18 +864,14 @@ def private emit_early_exit_lane( retType.flags.constant = false retType.flags.ref = false } - let foundName = "`found`{at.line}`{at.column}" - let lastBindName = "`lst`{at.line}`{at.column}" - preludeStmts |> push <| qmacro_expr() { + let foundName = qn("found", at) + let lastBindName = qn("lst", at) + preludeStmts |> push_from <| qmacro_block_to_array() { var $i(foundName) = false - } - preludeStmts |> push <| qmacro_expr() { var $i(lastBindName) : $t(retType) } - perMatchStmts |> push <| qmacro_expr() { + perMatchStmts |> push_from <| qmacro_block_to_array() { $i(foundName) = true - } - perMatchStmts |> push <| qmacro_expr() { $i(lastBindName) := $i(valueName) } if (opName == "last") { @@ -891,7 +883,7 @@ def private emit_early_exit_lane( } } else { // Bind `d` once at the top (eager, matches linq.das line 2621). - let defaultName = "`dval`{at.line}`{at.column}" + let defaultName = qn("dval", at) var defaultExpr = clone_expression(terminatorCall.arguments[1]) preludeStmts |> push <| qmacro_expr() { let $i(defaultName) = $e(defaultExpr) @@ -915,24 +907,18 @@ def private emit_early_exit_lane( retType.flags.constant = false retType.flags.ref = false } - let foundName = "`found`{at.line}`{at.column}" - let bindName = "`sng`{at.line}`{at.column}" - preludeStmts |> push <| qmacro_expr() { + let foundName = qn("found", at) + let bindName = qn("sng", at) + preludeStmts |> push_from <| qmacro_block_to_array() { var $i(foundName) = false - } - preludeStmts |> push <| qmacro_expr() { var $i(bindName) : $t(retType) } if (opName == "single") { - perMatchStmts |> push <| qmacro_expr() { + perMatchStmts |> push_from <| qmacro_block_to_array() { if ($i(foundName)) { panic("sequence contains more than one element") } - } - perMatchStmts |> push <| qmacro_expr() { $i(foundName) = true - } - perMatchStmts |> push <| qmacro_expr() { $i(bindName) := $i(valueName) } tailStmts <- qmacro_block_to_array() { @@ -942,21 +928,17 @@ def private emit_early_exit_lane( return <- $i(bindName) } } else { - let defaultName = "`dval`{at.line}`{at.column}" + let defaultName = qn("dval", at) var defaultExpr = clone_expression(terminatorCall.arguments[1]) preludeStmts |> push <| qmacro_expr() { let $i(defaultName) = $e(defaultExpr) } // `single_or_default` early-returns default on the SECOND match (subsequent matches don't matter). - perMatchStmts |> push <| qmacro_expr() { + perMatchStmts |> push_from <| qmacro_block_to_array() { if ($i(foundName)) { return $i(defaultName) } - } - perMatchStmts |> push <| qmacro_expr() { $i(foundName) = true - } - perMatchStmts |> push <| qmacro_expr() { $i(bindName) := $i(valueName) } tailStmts <- qmacro_block_to_array() { @@ -978,8 +960,8 @@ def private emit_early_exit_lane( retType.flags.constant = false retType.flags.ref = false } - let idxName = "`idx`{at.line}`{at.column}" - let cntName = "`ec`{at.line}`{at.column}" + let idxName = qn("idx", at) + let cntName = qn("ec", at) var idxExpr = clone_expression(terminatorCall.arguments[1]) preludeStmts |> push <| qmacro_expr() { let $i(idxName) = $e(idxExpr) @@ -1000,12 +982,10 @@ def private emit_early_exit_lane( preludeStmts |> push <| qmacro_expr() { var $i(cntName) = 0 } - perMatchStmts |> push <| qmacro_expr() { + perMatchStmts |> push_from <| qmacro_block_to_array() { if ($i(cntName) == $i(idxName)) { return $i(valueName) } - } - perMatchStmts |> push <| qmacro_expr() { $i(cntName) ++ } if (opName == "element_at") { @@ -1027,7 +1007,7 @@ def private emit_early_exit_lane( accType.flags.constant = false accType.flags.ref = false } - let aggAccName = "`agg`{at.line}`{at.column}" + let aggAccName = qn("agg", at) let aggIsWorkhorse = (seedExpr._type != null && seedExpr._type.isWorkhorseType) if (aggIsWorkhorse) { preludeStmts |> push <| qmacro_expr() { @@ -1114,25 +1094,24 @@ def private strip_const_ref(var t : TypeDeclPtr) : TypeDeclPtr { } [macro_function] -def private finalize_emission(var top : Expression?; srcName : string; at : LineInfo; var body : Expression?) : Expression? { +def private finalize_emission_stmts(var top : Expression?; srcName : string; at : LineInfo; var stmts : array) : Expression? { var srcParamType = invoke_src_param_type(top) var topExpr = clone_expression(top) topExpr.genFlags.alwaysSafe = true var emission : Expression? = qmacro(invoke($($i(srcName) : $t(srcParamType)) { - $e(body) + $b(stmts) }, $e(topExpr))) return finalize_invoke(emission, at) } [macro_function] -def private finalize_emission_stmts(var top : Expression?; srcName : string; at : LineInfo; var stmts : array) : Expression? { - var srcParamType = invoke_src_param_type(top) - var topExpr = clone_expression(top) - topExpr.genFlags.alwaysSafe = true - var emission : Expression? = qmacro(invoke($($i(srcName) : $t(srcParamType)) { - $b(stmts) - }, $e(topExpr))) - return finalize_invoke(emission, at) +def private finalize_decs_emission(var emission : Expression?; at : LineInfo; wrapToIter : bool) : Expression? { + emission.force_at(at) + emission.force_generated(true) + if (wrapToIter) { + emission = qmacro($e(emission).to_sequence_move()) + } + return emission } [macro_function] @@ -1206,7 +1185,7 @@ def private plan_order_family(var expr : Expression?) : Expression? { var firstDefaultExpr : Expression? var hasOrder = false let at = calls[0]._0.at - let itName = "`it`{at.line}`{at.column}" + let itName = qn("it", at) for (i in 0 .. length(calls)) { var cll & = unsafe(calls[i]) let name = cll._1.name @@ -1267,7 +1246,7 @@ def private plan_order_family(var expr : Expression?) : Expression? { // order + first → preserve eager `first()` panic-on-empty. min/max return an uninitialized ref on empty, so wrap in an empty-guard for arrays (zero alloc, O(N) min scan), or use top_n*(_, 1, _) |> first() for iterators (n=1 bounded heap; first() panics on empty). if (top._type.isGoodArrayType) { var srcParamType = invoke_src_param_type(top) - let firstSrcName = "`first_src`{at.line}`{at.column}" + let firstSrcName = qn("first_src", at) var minMaxCall : Expression? if (hasKey) { minMaxCall = qmacro($c(minMaxName)($i(firstSrcName), $e(orderKey))) @@ -1330,8 +1309,8 @@ def private plan_order_family(var expr : Expression?) : Expression? { return emission } // where_* + order_*[+take] — emit a single fused loop that prefilters into a fresh - let srcName = "`source`{at.line}`{at.column}" - let bufName = "`buf`{at.line}`{at.column}" + let srcName = qn("source", at) + let bufName = qn("buf", at) var srcParamType = invoke_src_param_type(top) var topExpr = clone_expression(top) topExpr.genFlags.alwaysSafe = true @@ -1438,12 +1417,12 @@ def private plan_loop_or_count(var expr : Expression?) : Expression? { let hasTerminator = lane != LinqLane.ARRAY let intermediateCount = hasTerminator ? length(calls) - 1 : length(calls) let at = calls[0]._0.at - let srcName = "`source`{at.line}`{at.column}" - let itName = "`it`{at.line}`{at.column}" - let accName = "`acc`{at.line}`{at.column}" - let skipName = "`skip`{at.line}`{at.column}" - let takeCountName = "`tc`{at.line}`{at.column}" - let skippingName = "`skipping`{at.line}`{at.column}" + let srcName = qn("source", at) + let itName = qn("it", at) + let accName = qn("acc", at) + let skipName = qn("skip", at) + let takeCountName = qn("tc", at) + let skippingName = qn("skipping", at) var whereCond : Expression? var projection : Expression? var intermediateBinds : array @@ -1586,7 +1565,7 @@ def private plan_loop_or_count(var expr : Expression?) : Expression? { // Counter lane must evaluate the projection (and any chained intermediates) per var stmts : array if (projection != null && has_sideeffects(projection)) { - let finalBindName = "`vfinal`{at.line}`{at.column}" + let finalBindName = qn("vfinal", at) stmts |> push <| qmacro_expr() { var $i(finalBindName) = $e(projection) } @@ -1649,13 +1628,13 @@ def private plan_reverse(var expr : Expression?) : Expression? { var seenSelect = false var takeExpr : Expression? let at = calls[0]._0.at - let srcName = "`source`{at.line}`{at.column}" - let itName = "`it`{at.line}`{at.column}" - let bufName = "`buf`{at.line}`{at.column}" - let foundName = "`found`{at.line}`{at.column}" - let lastName = "`last`{at.line}`{at.column}" - let cntName = "`cnt`{at.line}`{at.column}" - let dBindName = "`d`{at.line}`{at.column}" + let srcName = qn("source", at) + let itName = qn("it", at) + let bufName = qn("buf", at) + let foundName = qn("found", at) + let lastName = qn("last", at) + let cntName = qn("cnt", at) + let dBindName = qn("d", at) var reverseCall : ExprCall? for (i in 0 .. length(calls)) { var cll & = unsafe(calls[i]) @@ -1686,7 +1665,7 @@ def private plan_reverse(var expr : Expression?) : Expression? { // Reverse is identity for count — counter loop, no buffer. Side-effecting projection still fires per match. var perElement : Expression? if (projection != null && has_sideeffects(projection)) { - let vfinalName = "`vfinal`{at.line}`{at.column}" + let vfinalName = qn("vfinal", at) perElement = qmacro_block() { var $i(vfinalName) = $e(projection) $i(cntName) ++ @@ -1752,9 +1731,9 @@ def private plan_reverse(var expr : Expression?) : Expression? { && (top._type.isGoodArrayType || top._type.isArray)) if (canBackwardIndex) { // R6: visit only the last takeN indices — skips full-source push + O(length) reverse_inplace. - let lenName = "`rlen`{at.line}`{at.column}" - let takeNName = "`rtn`{at.line}`{at.column}" - let kName = "`rk`{at.line}`{at.column}" + let lenName = qn("rlen", at) + let takeNName = qn("rtn", at) + let kName = qn("rk", at) var takeA = clone_expression(takeExpr) var takeB = clone_expression(takeExpr) var takeC = clone_expression(takeExpr) @@ -1810,7 +1789,9 @@ def private plan_reverse(var expr : Expression?) : Expression? { } } } - return finalize_emission(top, srcName, at, body) + var bodyStmts : array + bodyStmts |> push_block_list(body) + return finalize_emission_stmts(top, srcName, at, bodyStmts) } [macro_function] @@ -1837,13 +1818,13 @@ def private plan_distinct(var expr : Expression?) : Expression? { var distinctKeyBlock : Expression? var takeExpr : Expression? let at = calls[0]._0.at - let srcName = "`source`{at.line}`{at.column}" - let itName = "`it`{at.line}`{at.column}" - let bufName = "`buf`{at.line}`{at.column}" - let seenName = "`seen`{at.line}`{at.column}" - let keyName = "`k`{at.line}`{at.column}" - let takenName = "`taken`{at.line}`{at.column}" - let accName = "`acc`{at.line}`{at.column}" + let srcName = qn("source", at) + let itName = qn("it", at) + let bufName = qn("buf", at) + let seenName = qn("seen", at) + let keyName = qn("k", at) + let takenName = qn("taken", at) + let accName = qn("acc", at) for (i in 0 .. length(calls)) { var cll & = unsafe(calls[i]) let name = cll._1.name @@ -1897,18 +1878,16 @@ def private plan_distinct(var expr : Expression?) : Expression? { } } // Bind take(N) limit once at outer scope so a side-effecting arg fires once, not on every fresh-key check. - let takeLimName = "`takeLim`{at.line}`{at.column}" + let takeLimName = qn("takeLim", at) if (takeExpr != null) { var takeBind = clone_expression(takeExpr) - stmts |> push <| qmacro_expr() { + stmts |> push_from <| qmacro_block_to_array() { let $i(takeLimName) = $e(takeBind) - } - stmts |> push <| qmacro_expr() { var $i(takenName) = 0 } } // Bind side-effecting projection once per element; key + buffer/acc share the bind (matches original LINQ's one-eval per source elem). - let pvName = "`pv`{at.line}`{at.column}" + let pvName = qn("pv", at) let bindProjection = projection != null && has_sideeffects(projection) var pushExpr : Expression? if (projection != null) { @@ -2630,15 +2609,15 @@ def private plan_group_by_core(var calls : array>; var adapter : GroupBySourceAdapter) : Expression? { // Shared group_by splice machinery. Caller pops terminator/select/having/group_by_lazy + extracts keyBlock + sets up the source adapter; this function does the rest (upstream chain walk, reducer recognition, state-table hoisting, tab?[uk]??dummy splice, output emission, source-loop wrap, terminator emission, final invoke wrap). let prefix = adapter.namePrefix - let tabName = "`{prefix}tab`{at.line}`{at.column}" - let kName = "`{prefix}k`{at.line}`{at.column}" - let ukName = "`{prefix}uk`{at.line}`{at.column}" - let entryName = "`{prefix}entry`{at.line}`{at.column}" - let kvName = "`{prefix}kv`{at.line}`{at.column}" - let bufName = "`{prefix}buf`{at.line}`{at.column}" - let bindName = "`{prefix}gpb`{at.line}`{at.column}" - let cntName = "`{prefix}cnt`{at.line}`{at.column}" - let dummyName = "`{prefix}dummy`{at.line}`{at.column}" + let tabName = qn("{prefix}tab", at) + let kName = qn("{prefix}k", at) + let ukName = qn("{prefix}uk", at) + let entryName = qn("{prefix}entry", at) + let kvName = qn("{prefix}kv", at) + let bufName = qn("{prefix}buf", at) + let bindName = qn("{prefix}gpb", at) + let cntName = qn("{prefix}cnt", at) + let dummyName = qn("{prefix}dummy", at) // Walk upstream where_/select* into segments. Each where guards everything AFTER it; a select after a where flushes a new segment so the projection bind lives inside the where's guard. var segBinds : array> var segWheres : array @@ -2704,7 +2683,7 @@ def private plan_group_by_core(var calls : array>; // Rewrite optional having predicate against accumulator slots; PR-E scan adds hidden slots for reducers referenced by having but absent from select. var havingPred : Expression? if (havingCall != null) { - let hbName = "`{prefix}hb`{at.line}`{at.column}" + let hbName = qn("{prefix}hb", at) var rawPred = peel_lambda_rename_var(havingCall.arguments[1], hbName) if (rawPred == null || !extend_specs_for_missing_having_reducers(rawPred, hbName, itName, specs, userVisibleSlotCount, elemType, at)) return null havingPred = rewrite_having_pred(rawPred, hbName, kvName, specs) @@ -2747,19 +2726,15 @@ def private plan_group_by_core(var calls : array>; } } var tabValueType2 = clone_type(tabValueType) - stmts |> push <| qmacro_expr() { + stmts |> push_from <| qmacro_block_to_array() { var inscope $i(tabName) : table))); $t(tabValueType)> - } - stmts |> push <| qmacro_expr() { var $i(dummyName) : $t(tabValueType2) } } else { var accType = clone_type(specs[0].accType) var accType2 = clone_type(specs[0].accType) - stmts |> push <| qmacro_expr() { + stmts |> push_from <| qmacro_block_to_array() { var inscope $i(tabName) : table))); tuple)) -const -&; $t(accType)>> - } - stmts |> push <| qmacro_expr() { var $i(dummyName) : tuple)) -const -&; $t(accType2)> } } @@ -2903,10 +2878,8 @@ def private plan_group_by_core(var calls : array>; retType = new TypeDecl(baseType = Type.tArray) retType.firstType = clone_type(bufElemType) } - stmts |> push <| qmacro_expr() { + stmts |> push_from <| qmacro_block_to_array() { var $i(bufName) : array<$t(bufElemType)> - } - stmts |> push <| qmacro_expr() { $i(bufName) |> reserve(length($i(tabName))) } if (havingPred != null) { @@ -2963,12 +2936,12 @@ def private plan_group_by(var expr : Expression?) : Expression? { var keyBlock = clone_expression(groupByCall.arguments[1]) let at = groupByCall.at var adapter = GroupBySourceAdapter( - initialBindName := "`it`{at.line}`{at.column}", + initialBindName := qn("it", at), initialElemType = strip_const_ref(clone_type(top._type.firstType)), namePrefix := "", isDecs = false, arrayTop = top, - arraySrcName := "`source`{at.line}`{at.column}", + arraySrcName := qn("source", at), decsBridge = null ) return plan_group_by_core(calls, keyBlock, groupProjCall, havingCall, terminatorName, expr._type.isIterator, at, adapter) @@ -3326,10 +3299,8 @@ def private decs_range_prelude(rangeInfo : DecsRangeInfo?; skipName, takeCountNa } if (rangeInfo.takeExpr != null) { var takeInit = clone_expression(rangeInfo.takeExpr) - stmts |> push <| qmacro_expr() { + stmts |> push_from <| qmacro_block_to_array() { let $i(takeLimitName) = $e(takeInit) - } - stmts |> push <| qmacro_expr() { var $i(takeCountName) = 0 } } @@ -3345,8 +3316,8 @@ def private decs_range_prelude(rangeInfo : DecsRangeInfo?; skipName, takeCountNa [macro_function] def private emit_decs_count_archsize(bridge : DecsBridgeShape?; at : LineInfo) : Expression? { // Bare count(): no chain ops, sum arch.size per archetype — skips the per-entity walk entirely. - let accName = "`decs_acc`{at.line}`{at.column}" - let archName = "`decs_arch`{at.line}`{at.column}" + let accName = qn("decs_acc", at) + let archName = qn("decs_arch", at) var reqExpr = clone_expression(bridge.reqHashExpr) var erqExpr = clone_expression(bridge.erqExpr) // arch.size is int64; count() returns int and the += site truncates past INT_MAX — chain `long_count()` when an int64-safe total is required. @@ -3373,15 +3344,15 @@ def private emit_decs_accumulator(bridge : DecsBridgeShape?; var accType : TypeDeclPtr; at : LineInfo) : Expression? { // Slice 2/3a/4 accumulator emission: count / long_count / sum / min / max / average with chained _select + interleaved _where + optional _count(pred). Slice 5a adds take/skip ranges via rangeInfo (counters hoisted at invoke scope, take present → for_each_archetype_find). - let accName = "`decs_acc`{at.line}`{at.column}" - let tupName = "`decs_tup`{at.line}`{at.column}" - let cntName = "`decs_cnt`{at.line}`{at.column}" - let firstName = "`decs_first`{at.line}`{at.column}" - let valBindName = "`decs_val`{at.line}`{at.column}" - let skipName = "`decs_skip`{at.line}`{at.column}" - let takeCountName = "`decs_takec`{at.line}`{at.column}" - let takeLimitName = "`decs_takel`{at.line}`{at.column}" - let skippingName = "`decs_skipping`{at.line}`{at.column}" + let accName = qn("decs_acc", at) + let tupName = qn("decs_tup", at) + let cntName = qn("decs_cnt", at) + let firstName = qn("decs_first", at) + let valBindName = qn("decs_val", at) + let skipName = qn("decs_skip", at) + let takeCountName = qn("decs_takec", at) + let takeLimitName = qn("decs_takel", at) + let skippingName = qn("decs_skipping", at) let finalBind = chainInfo.finalBind var perElement : Expression? if (opName == "sum") { @@ -3462,10 +3433,8 @@ def private emit_decs_accumulator(bridge : DecsBridgeShape?; return $i(accName) } } elif (opName == "average") { - preludeStmts |> push <| qmacro_expr() { + preludeStmts |> push_from <| qmacro_block_to_array() { var $i(accName) : double = 0lf - } - preludeStmts |> push <| qmacro_expr() { var $i(cntName) : uint64 = 0ul } tailStmts |> push <| qmacro_expr() { @@ -3473,10 +3442,8 @@ def private emit_decs_accumulator(bridge : DecsBridgeShape?; } } elif (opName == "min" || opName == "max") { // No empty-panic — matches non-decs emit_accumulator_lane (returns default-initialized acc). - preludeStmts |> push <| qmacro_expr() { + preludeStmts |> push_from <| qmacro_block_to_array() { var $i(firstName) = true - } - preludeStmts |> push <| qmacro_expr() { var $i(accName) : $t(accType) } tailStmts |> push <| qmacro_expr() { @@ -3534,25 +3501,23 @@ def private emit_decs_early_exit(bridge : DecsBridgeShape?; terminatorCall : ExprCall?; at : LineInfo) : Expression? { // Slice 3b/4 early-exit: first / first_or_default / any / all / contains via for_each_archetype_find (block returns true to stop archetype walk). Supports chained _select + interleaved _where via wrap_decs_chain. Slice 5a adds take/skip ranges via rangeInfo (counters hoisted at invoke scope). - let tupName = "`decs_tup`{at.line}`{at.column}" - let foundName = "`decs_found`{at.line}`{at.column}" - let resultName = "`decs_result`{at.line}`{at.column}" - let containsValName = "`decs_cv`{at.line}`{at.column}" - let defaultName = "`decs_dv`{at.line}`{at.column}" - let skipName = "`decs_skip`{at.line}`{at.column}" - let takeCountName = "`decs_takec`{at.line}`{at.column}" - let takeLimitName = "`decs_takel`{at.line}`{at.column}" - let skippingName = "`decs_skipping`{at.line}`{at.column}" + let tupName = qn("decs_tup", at) + let foundName = qn("decs_found", at) + let resultName = qn("decs_result", at) + let containsValName = qn("decs_cv", at) + let defaultName = qn("decs_dv", at) + let skipName = qn("decs_skip", at) + let takeCountName = qn("decs_takec", at) + let takeLimitName = qn("decs_takel", at) + let skippingName = qn("decs_skipping", at) let finalBind = chainInfo.finalBind var elemType = clone_type(chainInfo.finalType) var perElement : Expression? var preludeStmts : array var tailStmts : array if (opName == "first" || opName == "first_or_default") { - preludeStmts |> push <| qmacro_expr() { + preludeStmts |> push_from <| qmacro_block_to_array() { var $i(foundName) = false - } - preludeStmts |> push <| qmacro_expr() { var $i(resultName) : $t(elemType) } if (opName == "first_or_default") { @@ -3567,17 +3532,13 @@ def private emit_decs_early_exit(bridge : DecsBridgeShape?; return true } if (opName == "first") { - tailStmts |> push <| qmacro_expr() { + tailStmts |> push_from <| qmacro_block_to_array() { if (!$i(foundName)) panic("sequence contains no elements") - } - tailStmts |> push <| qmacro_expr() { return $i(resultName) } } else { - tailStmts |> push <| qmacro_expr() { + tailStmts |> push_from <| qmacro_block_to_array() { if (!$i(foundName)) return $i(defaultName) - } - tailStmts |> push <| qmacro_expr() { return $i(resultName) } } @@ -3740,12 +3701,12 @@ def private emit_decs_to_array(bridge : DecsBridgeShape?; intermediateEnd : int; at : LineInfo) : Expression? { // Slice 3c/4 to_array: hoist `var buf` above outer for_each_archetype; per-element push_clone of post-chain value at chainInfo.finalBind. Slice 5a adds take/skip ranges via rangeInfo. - let tupName = "`decs_tup`{at.line}`{at.column}" - let bufName = "`decs_buf`{at.line}`{at.column}" - let skipName = "`decs_skip`{at.line}`{at.column}" - let takeCountName = "`decs_takec`{at.line}`{at.column}" - let takeLimitName = "`decs_takel`{at.line}`{at.column}" - let skippingName = "`decs_skipping`{at.line}`{at.column}" + let tupName = qn("decs_tup", at) + let bufName = qn("decs_buf", at) + let skipName = qn("decs_skip", at) + let takeCountName = qn("decs_takec", at) + let takeLimitName = qn("decs_takel", at) + let skippingName = qn("decs_skipping", at) let finalBind = chainInfo.finalBind var elemType = clone_type(chainInfo.finalType) var perElement : Expression? = qmacro_expr() { @@ -3803,15 +3764,15 @@ def private emit_decs_min_max_by(bridge : DecsBridgeShape?; terminatorCall : ExprCall?; at : LineInfo) : Expression? { // Slice 4: min_by / max_by — track best (key, element) pair across archetypes. Element retained via `:=`, matches min_by_impl's empty→default semantics. Slice 5a adds take/skip ranges via rangeInfo. - let tupName = "`decs_tup`{at.line}`{at.column}" - let firstName = "`decs_first`{at.line}`{at.column}" - let bestKeyName = "`decs_bkey`{at.line}`{at.column}" - let bestElemName = "`decs_belem`{at.line}`{at.column}" - let keyBindName = "`decs_key`{at.line}`{at.column}" - let skipName = "`decs_skip`{at.line}`{at.column}" - let takeCountName = "`decs_takec`{at.line}`{at.column}" - let takeLimitName = "`decs_takel`{at.line}`{at.column}" - let skippingName = "`decs_skipping`{at.line}`{at.column}" + let tupName = qn("decs_tup", at) + let firstName = qn("decs_first", at) + let bestKeyName = qn("decs_bkey", at) + let bestElemName = qn("decs_belem", at) + let keyBindName = qn("decs_key", at) + let skipName = qn("decs_skip", at) + let takeCountName = qn("decs_takec", at) + let takeLimitName = qn("decs_takel", at) + let skippingName = qn("decs_skipping", at) let finalBind = chainInfo.finalBind var elemType = clone_type(chainInfo.finalType) var keyExpr = peel_lambda_rename_var(clone_expression(terminatorCall.arguments[1]), finalBind) @@ -3845,13 +3806,9 @@ def private emit_decs_min_max_by(bridge : DecsBridgeShape?; var rangeStmts <- decs_range_prelude(rangeInfo, skipName, takeCountName, takeLimitName, skippingName) var bodyStmts : array bodyStmts |> reserve(length(rangeStmts) + 5) - bodyStmts |> push <| qmacro_expr() { + bodyStmts |> push_from <| qmacro_block_to_array() { var $i(firstName) = true - } - bodyStmts |> push <| qmacro_expr() { var $i(bestKeyName) : $t(keyType) - } - bodyStmts |> push <| qmacro_expr() { var $i(bestElemName) : $t(elemType) } bodyStmts |> push_from(rangeStmts) @@ -3897,7 +3854,7 @@ def private plan_decs_unroll(var expr : Expression?) : Expression? { || lastName == "any" || lastName == "all" || lastName == "contains") let isMinMaxBy = (lastName == "min_by" || lastName == "max_by") let isTerminator = isAccum || isEarlyExit || isMinMaxBy - let tupName = "`decs_tup`{at.line}`{at.column}" + let tupName = qn("decs_tup", at) let intermediateEnd = isTerminator ? length(calls) - 1 : length(calls) // Slice 5a/5b: peel trailing take/skip/take_while/skip_while into rangeInfo. chainEnd is the index past which only range ops live (or == intermediateEnd if none). tupName passed so predicate-driven ranges peel against the source tuple. let rangeInfo = extract_decs_ranges(calls, intermediateEnd, tupName, at) @@ -3956,7 +3913,7 @@ def private plan_decs_group_by(var expr : Expression?) : Expression? { var keyBlock = clone_expression(groupByCall.arguments[1]) let at = groupByCall.at var adapter = GroupBySourceAdapter( - initialBindName := "`decs_tup`{at.line}`{at.column}", + initialBindName := qn("decs_tup", at), initialElemType = strip_const_ref(clone_type(bridge.elementType)), namePrefix := "decs_", isDecs = true, @@ -3976,8 +3933,8 @@ def private plan_decs_order_family(var expr : Expression?) : Expression? { var bridge = extract_decs_bridge(top) if (bridge == null || empty(calls) || expr._type == null) return null let at = calls[0]._0.at - let tupName = "`decs_tup`{at.line}`{at.column}" - let bufName = "`decs_buf`{at.line}`{at.column}" + let tupName = qn("decs_tup", at) + let bufName = qn("decs_buf", at) var whereCond : Expression? var orderName : string = "" var orderKey : Expression? @@ -4048,10 +4005,8 @@ def private plan_decs_order_family(var expr : Expression?) : Expression? { let archName = bridge.archName var bodyStmts : array bodyStmts |> reserve(5) - bodyStmts |> push <| qmacro_expr() { + bodyStmts |> push_from <| qmacro_block_to_array() { var $i(bufName) : array<$t(elemType)> - } - bodyStmts |> push <| qmacro_expr() { for_each_archetype($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) { $e(forExprNode) }) @@ -4077,7 +4032,7 @@ def private plan_decs_order_family(var expr : Expression?) : Expression? { })) } elif (firstName == "first_or_default") { // No min_by_or_default helper exists; route through top_n*(_, 1, _) + first_or_default for the empty-buf case. The default expression is hoisted into a let BEFORE for_each_archetype so its side effects fire ahead of iteration (matches plan_decs_reverse's first_or_default + the eager-arg-eval semantics of the non-spliced chain). - let dBindName = "`order_d`{at.line}`{at.column}" + let dBindName = qn("order_d", at) var foDStmts : array foDStmts |> reserve(4) foDStmts |> push <| qmacro_expr() { @@ -4133,14 +4088,8 @@ def private plan_decs_order_family(var expr : Expression?) : Expression? { $b(bodyStmts) })) } - emission.force_at(at) - emission.force_generated(true) // Bare order + take both return array; wrap to iterator when the user's outer context demands it. first/first_or_default return scalar — no wrap. - let returnsArray = firstName == "" - if (needIterWrap && returnsArray) { - emission = qmacro($e(emission).to_sequence_move()) - } - return emission + return finalize_decs_emission(emission, at, needIterWrap && firstName == "") } // ── decs reverse splice (Slice 5d — buffer + reverse_inplace; or counter/walk-last for terminators) ─────── @@ -4167,7 +4116,7 @@ def private plan_decs_reverse(var expr : Expression?) : Expression? { } if (empty(calls)) return null let at = calls[0]._0.at - let tupName = "`decs_tup`{at.line}`{at.column}" + let tupName = qn("decs_tup", at) var whereCond : Expression? var projection : Expression? var hasReverse = false @@ -4204,10 +4153,10 @@ def private plan_decs_reverse(var expr : Expression?) : Expression? { let archName = bridge.archName if (terminatorName == "count") { // Reverse is identity for count — counter loop, no buffer. Side-effecting projection still fires per match. - let cntName = "`decs_cnt`{at.line}`{at.column}" + let cntName = qn("decs_cnt", at) var perElement : Expression? if (projection != null && has_sideeffects(projection)) { - let vfinalName = "`decs_vfinal`{at.line}`{at.column}" + let vfinalName = qn("decs_vfinal", at) perElement = qmacro_block() { var $i(vfinalName) = $e(projection) $i(cntName) ++ @@ -4239,9 +4188,9 @@ def private plan_decs_reverse(var expr : Expression?) : Expression? { } if (terminatorName == "first" || terminatorName == "first_or_default") { // "first of reversed" = LAST surviving element. Walk all archetypes, overwrite `last` on each match. No buffer. - let foundName = "`decs_found`{at.line}`{at.column}" - let lastName = "`decs_last`{at.line}`{at.column}" - let dBindName = "`decs_d`{at.line}`{at.column}" + let foundName = qn("decs_found", at) + let lastName = qn("decs_last", at) + let dBindName = qn("decs_d", at) var lastType = strip_const_ref(clone_type(projection != null ? projection._type : bridge.elementType)) var valueExpr : Expression? if (projection != null) { @@ -4295,7 +4244,7 @@ def private plan_decs_reverse(var expr : Expression?) : Expression? { return emission } // Buffer + reverse_inplace + optional resize. Implicit to_array (no terminator). - let bufName = "`decs_buf`{at.line}`{at.column}" + let bufName = qn("decs_buf", at) let needIterWrap = expr._type.isIterator var bufElemType = strip_const_ref(clone_type(projection != null ? projection._type : bridge.elementType)) var pushExpr : Expression? @@ -4321,11 +4270,9 @@ def private plan_decs_reverse(var expr : Expression?) : Expression? { var resizeStmts : array if (takeExpr != null) { // take(N) of reversed-buffer = last N of source reversed. Hoist once to a let so a side-effecting take expression evaluates exactly once. - let takeNName = "`take_n`{at.line}`{at.column}" - resizeStmts |> push <| qmacro_expr() { + let takeNName = qn("take_n", at) + resizeStmts |> push_from <| qmacro_block_to_array() { let $i(takeNName) = $e(takeExpr) - } - resizeStmts |> push <| qmacro_expr() { $i(bufName) |> resize($i(takeNName) <= 0 ? 0 : ($i(takeNName) < length($i(bufName)) ? $i(takeNName) : length($i(bufName)))) } } @@ -4338,12 +4285,7 @@ def private plan_decs_reverse(var expr : Expression?) : Expression? { $b(resizeStmts) return <- $i(bufName) })) - emission.force_at(at) - emission.force_generated(true) - if (needIterWrap) { - emission = qmacro($e(emission).to_sequence_move()) - } - return emission + return finalize_decs_emission(emission, at, needIterWrap) } // ── decs distinct splice (Slice 5c — streaming dedup table hoisted above for_each_archetype) ─────── @@ -4366,14 +4308,14 @@ def private plan_decs_distinct(var expr : Expression?) : Expression? { } if (empty(calls)) return null let at = calls[0]._0.at - let tupName = "`decs_tup`{at.line}`{at.column}" - let bufName = "`decs_buf`{at.line}`{at.column}" - let seenName = "`decs_seen`{at.line}`{at.column}" - let keyName = "`decs_k`{at.line}`{at.column}" - let takenName = "`decs_taken`{at.line}`{at.column}" - let takeLimName = "`decs_takeLim`{at.line}`{at.column}" - let accName = "`decs_acc`{at.line}`{at.column}" - let pvName = "`decs_pv`{at.line}`{at.column}" + let tupName = qn("decs_tup", at) + let bufName = qn("decs_buf", at) + let seenName = qn("decs_seen", at) + let keyName = qn("decs_k", at) + let takenName = qn("decs_taken", at) + let takeLimName = qn("decs_takeLim", at) + let accName = qn("decs_acc", at) + let pvName = qn("decs_pv", at) var whereCond : Expression? var projection : Expression? var hasDistinct = false @@ -4441,10 +4383,8 @@ def private plan_decs_distinct(var expr : Expression?) : Expression? { if (takeExpr != null) { // Bind take(N) limit once at outer scope so a side-effecting arg fires once (matches plan_distinct). var takeBind = clone_expression(takeExpr) - preludeStmts |> push <| qmacro_expr() { + preludeStmts |> push_from <| qmacro_block_to_array() { let $i(takeLimName) = $e(takeBind) - } - preludeStmts |> push <| qmacro_expr() { var $i(takenName) = 0 } } @@ -4575,13 +4515,7 @@ def private plan_decs_distinct(var expr : Expression?) : Expression? { $b(bodyStmts) })) } - emission.force_at(at) - emission.force_generated(true) - let needIterWrap = expr._type.isIterator - if (needIterWrap && needBuffer) { - emission = qmacro($e(emission).to_sequence_move()) - } - return emission + return finalize_decs_emission(emission, at, expr._type.isIterator && needBuffer) } [macro_function] @@ -4632,15 +4566,15 @@ def private plan_zip(var expr : Expression?) : Expression? { } } let at = zipCall.at - let srcAName = "`srcA`{at.line}`{at.column}" - let srcBName = "`srcB`{at.line}`{at.column}" - let bufName = "`buf`{at.line}`{at.column}" - let accName = "`acc`{at.line}`{at.column}" - let skipName = "`skip`{at.line}`{at.column}" - let takeCountName = "`tc`{at.line}`{at.column}" - let skippingName = "`skipping`{at.line}`{at.column}" + let srcAName = qn("srcA", at) + let srcBName = qn("srcB", at) + let bufName = qn("buf", at) + let accName = qn("acc", at) + let skipName = qn("skip", at) + let takeCountName = qn("tc", at) + let skippingName = qn("skipping", at) // itName binds the tuple `(itA, itB)` at the top of the for-body; chain ops (where_/select/while-family) reference it by name. - let itName = "`it`{at.line}`{at.column}" + let itName = qn("it", at) var srcAExpr = peel_each(clone_expression(zipCall.arguments[0])) var srcBExpr = peel_each(clone_expression(zipCall.arguments[1])) if (srcAExpr == null || srcAExpr._type == null || srcBExpr == null || srcBExpr._type == null) return null @@ -4778,7 +4712,7 @@ def private plan_zip(var expr : Expression?) : Expression? { if (isCounter) { // Counter lane: if projection has side effects, evaluate it for those effects; then `acc++`. if (seenSelect && !allProjectionsPure) { - let finalBindName = "`vfinal`{at.line}`{at.column}" + let finalBindName = qn("vfinal", at) perStmts |> push <| qmacro_expr() { var $i(finalBindName) = $e(projection) } diff --git a/modules/dasLLVM/daslib/llvm_exe.das b/modules/dasLLVM/daslib/llvm_exe.das index 0f06f0d71a..434ccfbab4 100644 --- a/modules/dasLLVM/daslib/llvm_exe.das +++ b/modules/dasLLVM/daslib/llvm_exe.das @@ -118,9 +118,12 @@ class CollectExternVisitor : AstVisitor { LLVMBuildCall2(init_builder, register_mod_type, reg_strings, array(), "") registered_modules["strings"] = true + // void * get_jit_table_{at,find,erase} ( int32_t baseType, Context * context, LineInfoArg * at ) reg_extern_tab_type = LLVMFunctionType(types.LLVMVoidPtrType(), fixed_array( - types.t_int32, // type + types.t_int32, // baseType + types.LLVMVoidPtrType(), // context + types.LLVMVoidPtrType(), // at ) ) tab_at_accessor = LLVMAddFunctionWithType(g_mod, "get_jit_table_at", reg_extern_tab_type) @@ -420,9 +423,12 @@ class CollectExternVisitor : AstVisitor { } } + // get_jit_table_{at,find,erase} dispatch on baseType only; context+at unused. + // Pass null for both to match the C++ signature without a real Context. def add_table_at_call(index_t : TypeDeclPtr) { let typ = get_table_t(index_t) - var call_args = array(types.ConstI32(uint64(typ))) + let null_ptr = LLVMConstPointerNull(types.LLVMVoidPtrType()) + var call_args = array(types.ConstI32(uint64(typ)), null_ptr, null_ptr) let tab_ptr = LLVMBuildCall2(builder, reg_extern_tab_type, tab_at_accessor, call_args, "") let tab_method = LLVMGetNamedGlobal(g_mod, DllName(FN_JIT_TABLE_AT(typ)).glob()) @@ -431,7 +437,8 @@ class CollectExternVisitor : AstVisitor { def add_table_find_call(index_t : TypeDeclPtr) { let typ = get_table_t(index_t) - var call_args = array(types.ConstI32(uint64(typ))) + let null_ptr = LLVMConstPointerNull(types.LLVMVoidPtrType()) + var call_args = array(types.ConstI32(uint64(typ)), null_ptr, null_ptr) let tab_ptr = LLVMBuildCall2(builder, reg_extern_tab_type, tab_find_accessor, call_args, "") let tab_method = LLVMGetNamedGlobal(g_mod, DllName(FN_JIT_TABLE_FIND(typ)).glob()) @@ -440,7 +447,8 @@ class CollectExternVisitor : AstVisitor { def add_table_erase_call(index_t : TypeDeclPtr) { let typ = get_table_t(index_t) - var call_args = array(types.ConstI32(uint64(typ))) + let null_ptr = LLVMConstPointerNull(types.LLVMVoidPtrType()) + var call_args = array(types.ConstI32(uint64(typ)), null_ptr, null_ptr) let tab_ptr = LLVMBuildCall2(builder, reg_extern_tab_type, tab_erase_accessor, call_args, "") let tab_method = LLVMGetNamedGlobal(g_mod, DllName(FN_JIT_TABLE_ERASE(typ)).glob()) @@ -978,7 +986,8 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; LLVMBuildCall2(builder, reg_fusion_type, reg_fusion_fn, array(), "") } - let init_global_var_type = LLVMFunctionType(types.LLVMVoidPtrType(), + // void jit_register_standalone_variable ( Context * ctx, uint64_t mnh, uint64_t offset ) + let init_global_var_type = LLVMFunctionType(types.t_void, fixed_array( types.LLVMVoidPtrType(), // Context * types.t_int64, // uint64_t mnh diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 5ca163f69d..87a6ad7dd9 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -471,6 +471,9 @@ class public LlvmJitVisitor : AstVisitor { def build_noalias_list(annotations : AnnotationList) : tuple; nocapture : table> { var noalias : table var nocapture : table + // Skip user hints entirely on wasm32 — host-LLVM wasm codegen has had + // miscompiles around them. Tracked alongside the alwaysinline gotcha. + if (g_target_is_wasm) return <- (noalias <- noalias, nocapture <- nocapture) for (ann in annotations) { if (ann.annotation.name == "hint") { for (arg in ann.arguments) { @@ -506,6 +509,10 @@ class public LlvmJitVisitor : AstVisitor { option_no_range_check = false option_no_alias = false option_no_capture = false + // Bypass user hints on wasm — keeps option_no_* flags false (= safe + // defaults: bounds checks on, no alias/capture assumption) and skips + // every LLVMAddAttributeToFunction call below. See g_target_is_wasm. + if (g_target_is_wasm) return for (ann in annotations) { if (ann.annotation.name == "hint") { for (arg in ann.arguments) { @@ -820,7 +827,8 @@ class public LlvmJitVisitor : AstVisitor { get_line_info_ptr(funAt), types.ConstI32(uint64(totalStackSize)), prologue, - get_context_param() + get_context_param(), + get_line_info_ptr(funAt) // at — used for stack-overflow throw ) var typ = g_fn_types[FN_JIT_PROLOGUE] LLVMBuildCall2(g_builder, typ, LLVMGetNamedFunction(g_mod, FN_JIT_PROLOGUE), params, "") @@ -3013,9 +3021,13 @@ class public LlvmJitVisitor : AstVisitor { LLVMBuildStore(g_builder, s0, getV(svar)) let dimSize = ssrc._type.dim[0] var tail = types.ConstI32(uint64(dimSize - 1)) - var sto = build_array_index(sdim, tail, etype, "last_element") - sto = LLVMBuildPtrToInt(g_builder, sto, types.LLVMIntPtrType(), "dim_end") - range2[ssrc] = sto + // Stash end-element pointer as-is; compare directly in next-step. + // Previous code ptrtoint'd to LLVMIntPtrType which is statically i64 + // on a 64-bit host even when codegen target is wasm32 — that zero- + // extends a 32-bit ptr to 64 bits but wasm32 codegen lowers it in a + // way that breaks the i64 equality check. Direct ptr compare avoids + // the width mismatch entirely. + range2[ssrc] = build_array_index(sdim, tail, etype, "last_element") } elif (ssrc._type.isGoodArrayType) {// {}->first() var arr = LLVMBuildLoadData2Aligned(g_builder, type_to_llvm_type(ssrc._type), getE(ssrc), ssrc._type.alignOf, "array") var size = LLVMBuildExtractValue(g_builder, arr, uint(JIT_ARRAY.SIZE), "array.size") @@ -3034,9 +3046,8 @@ class public LlvmJitVisitor : AstVisitor { var data = LLVMBuildExtractValue(g_builder, arr, uint(JIT_ARRAY.DATA), "array.data") LLVMBuildStore(g_builder, data, getV(svar)) var tail = LLVMBuildSub(g_builder, size, i64_one, "") - var sto = build_array_index(data, tail, ssrc._type.firstType, "last_element") - sto = LLVMBuildPtrToInt(g_builder, sto, types.LLVMIntPtrType(), "array_end") - range2[ssrc] = sto + // See dim case above — store end-element ptr directly, compare-by-ptr. + range2[ssrc] = build_array_index(data, tail, ssrc._type.firstType, "last_element") } elif (ssrc._type.isIterator) { if (is_count_or_ucount(ssrc)) { var ccount = ssrc as ExprCall @@ -3132,9 +3143,9 @@ class public LlvmJitVisitor : AstVisitor { var svar_v = LLVMBuildLoad2(g_builder, LLVMPointerType(type_to_llvm_type(svar._type), 0u), getV(svar), "") var svar_i = build_array_index(svar_v, types.ConstI32(1ul), etype, "next_element") LLVMBuildStore(g_builder, svar_i, getV(svar)) + // Direct ptr compare — avoids LLVMIntPtrType host/wasm32 width mismatch. var sto = range2[ssrc] - svar_i = LLVMBuildPtrToInt(g_builder, svar_v, types.LLVMIntPtrType(), "") - var rcond = LLVMBuildICmp(g_builder, LLVMIntPredicate.LLVMIntEQ, svar_i, sto, "") + var rcond = LLVMBuildICmp(g_builder, LLVMIntPredicate.LLVMIntEQ, svar_v, sto, "") var nextOk = append_basic_block("for_{svar.name}_next_ok") LLVMBuildCondBr(g_builder, rcond, lblk.loop_end, nextOk) LLVMPositionBuilderAtEnd(g_builder, nextOk) @@ -3142,9 +3153,9 @@ class public LlvmJitVisitor : AstVisitor { var svar_v = LLVMBuildLoad2(g_builder, LLVMPointerType(type_to_llvm_type(svar._type), 0u), getV(svar), "") var svar_i = build_array_index(svar_v, types.ConstI32(1ul), ssrc._type.firstType, "next_element") LLVMBuildStore(g_builder, svar_i, getV(svar)) + // Direct ptr compare — avoids LLVMIntPtrType host/wasm32 width mismatch. var sto = range2[ssrc] - svar_i = LLVMBuildPtrToInt(g_builder, svar_v, types.LLVMIntPtrType(), "") - var rcond = LLVMBuildICmp(g_builder, LLVMIntPredicate.LLVMIntEQ, svar_i, sto, "") + var rcond = LLVMBuildICmp(g_builder, LLVMIntPredicate.LLVMIntEQ, svar_v, sto, "") var nextOk = append_basic_block("for_{svar.name}_next_ok") LLVMBuildCondBr(g_builder, rcond, lblk.loop_end, nextOk) LLVMPositionBuilderAtEnd(g_builder, nextOk) @@ -4758,10 +4769,25 @@ class public LlvmJitVisitor : AstVisitor { vptr = get_global_offset_by_mnh(mnh, expr.variable.flags.global_shared, "global_var_{expr.variable.name}_mnh_ptr") vptr = LLVMBuildPointerCast(g_builder, vptr, get_type_pointer(expr.variable._type), "global_var_{expr.variable.name}") } else { - var ctx_ptr = LLVMBuildPointerCast(g_builder, get_context_param(), LLVMPointerType(types.t_int8, 0u), "") - let coffset = expr.variable.flags.global_shared ? CONTEXT_OFFSET_OF_SHARED : CONTEXT_OFFSET_OF_GLOBALS - var global_ptr = LLVMBuildGEP2(g_builder, types.t_int8, ctx_ptr, types.ConstI32(uint64(coffset)), "") - global_ptr = LLVMBuildLoad2(g_builder, LLVMPointerType(types.t_int8, 0u), global_ptr, "") + // On wasm32 the host's offsetof(Context, globals) (the + // CONTEXT_OFFSET_OF_GLOBALS constant baked into module_jit.cpp) + // does NOT match the wasm32 runtime's Context layout — + // 4-byte pointers shift everything. Call into the runtime + // archive (which knows its own layout) instead of doing + // GEP+load against a host-derived offset. Native keeps + // the inlined GEP+load — same codegen as before. + var global_ptr : LLVMOpaqueValue? + if (g_target_is_wasm) { + let fname = expr.variable.flags.global_shared ? FN_JIT_GET_SHARED_BASE : FN_JIT_GET_GLOBALS_BASE + let fty = g_fn_types[fname] + var args = fixed_array(get_context_param()) + global_ptr = LLVMBuildCall2(g_builder, fty, LLVMGetNamedFunction(g_mod, fname), args, "") + } else { + var ctx_ptr = LLVMBuildPointerCast(g_builder, get_context_param(), LLVMPointerType(types.t_int8, 0u), "") + let coffset = expr.variable.flags.global_shared ? CONTEXT_OFFSET_OF_SHARED : CONTEXT_OFFSET_OF_GLOBALS + var gp = LLVMBuildGEP2(g_builder, types.t_int8, ctx_ptr, types.ConstI32(uint64(coffset)), "") + global_ptr = LLVMBuildLoad2(g_builder, LLVMPointerType(types.t_int8, 0u), gp, "") + } vptr = LLVMBuildGEP2(g_builder, types.t_int8, global_ptr, types.ConstI32(uint64(expr.variable.stackTop)), "") } tryV = LLVMBuildPointerCast(g_builder, vptr, get_type_pointer(expr.variable._type), "global_var_{expr.variable.name}") @@ -6065,8 +6091,14 @@ class public LlvmJitVisitor : AstVisitor { operands |> push(temp) var mustprog = make_loop_md_flag("llvm.loop.mustprogress") operands |> push(mustprog) - for (a in args) { - append_loop_hint_operand(operands, a) + // Skip user-supplied loop hints on wasm — host LLVM's wasm32 backend + // has had miscompiles around llvm.loop.* metadata in combination with + // certain bodies. mustprogress alone is preserved (safe, required for + // some opt passes). See g_target_is_wasm. + if (!g_target_is_wasm) { + for (a in args) { + append_loop_hint_operand(operands, a) + } } let loop_md = LLVMMDNodeInContext2(g_ctx, array_data_ptr(operands), uint64(length(operands))) LLVMMetadataReplaceAllUsesWith(temp, loop_md) @@ -6598,10 +6630,20 @@ def public generate_globals_initialization_fn(ctx : Context?; prog : Program?; vptr = LLVMBuildCall2(visitor.g_builder, typ, func, params, "global_var_{glob.name}_mnh_ptr") vptr = LLVMBuildPointerCast(visitor.g_builder, vptr, get_type_pointer(glob._type), "global_var_{glob.name}") } else { - var ctx_ptr = LLVMBuildPointerCast(visitor.g_builder, ctx_param, LLVMPointerType(types.t_int8, 0u), "") - let coffset = glob.flags.global_shared ? CONTEXT_OFFSET_OF_SHARED : CONTEXT_OFFSET_OF_GLOBALS - var global_ptr = LLVMBuildGEP2(visitor.g_builder, types.t_int8, ctx_ptr, types.ConstI32(uint64(coffset)), "") - global_ptr = LLVMBuildLoad2(visitor.g_builder, LLVMPointerType(types.t_int8, 0u), global_ptr, "") + // See the matching site in preVisitExprVar for why + // wasm32 goes through a runtime helper here. + var global_ptr : LLVMOpaqueValue? + if (g_target_is_wasm) { + let fname = glob.flags.global_shared ? FN_JIT_GET_SHARED_BASE : FN_JIT_GET_GLOBALS_BASE + let fty = g_fn_types[fname] + var args = fixed_array(ctx_param) + global_ptr = LLVMBuildCall2(visitor.g_builder, fty, LLVMGetNamedFunction(g_mod, fname), args, "") + } else { + var ctx_ptr = LLVMBuildPointerCast(visitor.g_builder, ctx_param, LLVMPointerType(types.t_int8, 0u), "") + let coffset = glob.flags.global_shared ? CONTEXT_OFFSET_OF_SHARED : CONTEXT_OFFSET_OF_GLOBALS + var gp = LLVMBuildGEP2(visitor.g_builder, types.t_int8, ctx_ptr, types.ConstI32(uint64(coffset)), "") + global_ptr = LLVMBuildLoad2(visitor.g_builder, LLVMPointerType(types.t_int8, 0u), gp, "") + } vptr = LLVMBuildGEP2(visitor.g_builder, types.t_int8, global_ptr, types.ConstI32(uint64(glob.stackTop)), "") } // register CMRES destination so init expressions that return by copy/move diff --git a/modules/dasLLVM/daslib/llvm_jit_common.das b/modules/dasLLVM/daslib/llvm_jit_common.das index 013e765276..6798f37484 100644 --- a/modules/dasLLVM/daslib/llvm_jit_common.das +++ b/modules/dasLLVM/daslib/llvm_jit_common.das @@ -49,6 +49,10 @@ let public FN_JIT_STRING_BUILDER = "jit_string_builder" let public FN_JIT_STRING_BUILDER_TEMP = "jit_string_builder_temp" let public FN_JIT_GET_GLOBAL_MNH = "jit_get_global_mnh" let public FN_JIT_GET_SHARED_MNH = "jit_get_shared_mnh" +// Wasm-only path — see g_target_is_wasm in init_jit and the two sites in +// llvm_jit.das that emit a call instead of a baked-host-offset GEP+load. +let public FN_JIT_GET_GLOBALS_BASE = "jit_get_globals_base" +let public FN_JIT_GET_SHARED_BASE = "jit_get_shared_base" let public FN_JIT_ALLOC_HEAP = "jit_alloc_heap" let public FN_JIT_ALLOC_PERSISTENT = "jit_alloc_persistent" let public FN_JIT_FREE_HEAP = "jit_free_heap" @@ -290,6 +294,15 @@ def public get_table_erase(g_builder : LLVMOpaqueBuilder?; llvm_module : LLVMOpa var public g_fn_types : table +// Set by init_jit when target_triple is a wasm target. Cross-compile path's +// host-LLVM wasm32 codegen mishandles several user-provided hints (alwaysinline +// over complex inlined bodies, certain loop-vectorize / unroll metadata). When +// this flag is true, the LlvmJitVisitor skips ALL user hint application — +// function-level [hint(...)] AND for/while-loop hints — and falls back to the +// optimizer's default heuristics. Slight perf hit, but eliminates a class of +// wasm32-only miscompiles. +var public g_target_is_wasm = false + def public LLVMAddFunctionWithType(mod : LLVMModuleRef; name : string; typ : LLVMTypeRef) { var rv = LLVMAddFunction(mod, name, typ) g_fn_types[name] = typ @@ -337,7 +350,7 @@ def public init_jit_clopts() { } [macro_function] -def public init_jit(cg_opt_level : uint) { +def public init_jit(cg_opt_level : uint; target_triple : string = "") { // also prevent double init (g_mod != null) return if (is_in_completion() || is_compiling_macros() || g_mod != null) LLVMLinkInMCJIT() @@ -355,6 +368,32 @@ def public init_jit(cg_opt_level : uint) { g_fn_types |> clear() g_mod = LLVMModuleCreateWithNameInContext("llvm_jit_module", g_ctx) LLVMCreateJITCompilerForModule(g_engine, g_mod, cg_opt_level) + // For cross-compile targets (wasm32, etc.) pin the module's data layout + // to the TARGET's, not the host's, BEFORE any IR is built. LLVM constant- + // folds GEPs eagerly using the module's current layout — if the host has + // 8-byte pointers and the target has 4-byte, every for-loop end-pointer + // GEP bakes in the wrong stride and the loop never terminates. The native + // path (empty triple) keeps the host default. + g_target_is_wasm = target_triple |> starts_with("wasm") + if (!(target_triple |> empty())) { + // wasm32 target isn't always part of LLVMInitializeAllTargets in + // prebuilt LLVM.dll — call the explicit wasm initializer when needed. + if (g_target_is_wasm) { + LLVMInitializeWasmTarget() + } + // +simd128 / +nontrapping-fptoint is the feature set the wasm runtime + // archive is compiled with (see web/CMakeLists.txt). Required on wasm + // so vec4f returns are v128-in-register on both sides; meaningless + // (and potentially a target-machine-creation failure) on other + // cross-compile triples, so keep features empty there. + let features = g_target_is_wasm ? "+simd128,+nontrapping-fptoint" : "" + with_target_machine(target_triple, "", features, cg_opt_level) $(targetMachine : LLVMTargetMachineRef) { + let dl = LLVMCreateTargetDataLayout(targetMachine) + LLVMSetModuleDataLayout(g_mod, dl) + LLVMSetTarget(g_mod, target_triple) + LLVMDisposeTargetData(dl) + } + } static_if (LLVM_DEBUG_EVERYTHING) { let hostcpu = LLVMGetHostCPUName() let features = LLVMGetHostCPUFeatures() @@ -440,6 +479,23 @@ def public init_jit(cg_opt_level : uint) { LLVMAddGlobalMapping(g_engine, jit_get_shared_mnh, get_jit_get_shared_mnh()) LLVMAddAttributesToFunction(jit_get_shared_mnh, fixed_array(nounwind, willreturn)) LLVMAddAttributeToFunctionArgument(jit_get_shared_mnh, 1u, nocapture) + // Wasm-only: void * jit_get_globals_base ( Context * ); same shape for shared. + // Declared unconditionally (zero cost if unused); only called from llvm_jit.das + // when g_target_is_wasm. The native path keeps the inlined GEP+load — these + // helpers exist so the wasm32 runtime archive (which knows its own Context + // layout) resolves the right field offset. + var jit_get_globals_base = LLVMAddFunctionWithType(g_mod, FN_JIT_GET_GLOBALS_BASE, + LLVMFunctionType(g_prim_t.LLVMVoidPtrType(), + fixed_array(g_prim_t.LLVMVoidPtrType()))) + LLVMAddGlobalMapping(g_engine, jit_get_globals_base, get_jit_get_globals_base()) + LLVMAddAttributesToFunction(jit_get_globals_base, fixed_array(nounwind, willreturn)) + LLVMAddAttributeToFunctionArgument(jit_get_globals_base, 0u, nocapture) + var jit_get_shared_base = LLVMAddFunctionWithType(g_mod, FN_JIT_GET_SHARED_BASE, + LLVMFunctionType(g_prim_t.LLVMVoidPtrType(), + fixed_array(g_prim_t.LLVMVoidPtrType()))) + LLVMAddGlobalMapping(g_engine, jit_get_shared_base, get_jit_get_shared_base()) + LLVMAddAttributesToFunction(jit_get_shared_base, fixed_array(nounwind, willreturn)) + LLVMAddAttributeToFunctionArgument(jit_get_shared_base, 0u, nocapture) // jit_alloc_heap ( uint64_t mnh, context * ) var jit_alloc_heap = LLVMAddFunctionWithType(g_mod, FN_JIT_ALLOC_HEAP, LLVMFunctionType(g_prim_t.LLVMVoidPtrType(), @@ -456,7 +512,7 @@ def public init_jit(cg_opt_level : uint) { LLVMAddAttributeToFunctionArgument(jit_alloc_persistent, 1u, nocapture) // void jit_free_heap ( void * bytes, uint32_t size, Context * context ) var jit_free_heap = LLVMAddFunctionWithType(g_mod, FN_JIT_FREE_HEAP, - LLVMFunctionType(g_prim_t.LLVMVoidPtrType(), + LLVMFunctionType(g_prim_t.t_void, fixed_array(g_prim_t.LLVMVoidPtrType(), g_prim_t.t_int32, g_prim_t.LLVMVoidPtrType()))) LLVMAddGlobalMapping(g_engine, jit_free_heap, get_jit_free_heap()) LLVMAddAttributesToFunction(jit_free_heap, fixed_array(nounwind, willreturn, memory_arg_inaccessible_rw)) @@ -464,21 +520,21 @@ def public init_jit(cg_opt_level : uint) { LLVMAddAttributeToFunctionArgument(jit_free_heap, 2u, nocapture) // void jit_free_persistent ( void * bytes, Context * context ) var jit_free_persistent = LLVMAddFunctionWithType(g_mod, FN_JIT_FREE_PERSISTENT, - LLVMFunctionType(g_prim_t.LLVMVoidPtrType(), + LLVMFunctionType(g_prim_t.t_void, fixed_array(g_prim_t.LLVMVoidPtrType(), g_prim_t.LLVMVoidPtrType()))) LLVMAddGlobalMapping(g_engine, jit_free_persistent, get_jit_free_persistent()) LLVMAddAttributesToFunction(jit_free_persistent, fixed_array(nounwind, willreturn, memory_arg_inaccessible_rw)) LLVMAddAttributeToFunctionArgumentRange(jit_free_persistent, urange(0, 2), nocapture) - // void jit_array_lock ( Array & array, Context * context ) + // void jit_array_lock ( Array & array, Context * context, LineInfoArg * at ) var jit_array_lock = LLVMAddFunctionWithType(g_mod, FN_JIT_ARRAY_LOCK, - LLVMFunctionType(g_prim_t.LLVMVoidPtrType(), + LLVMFunctionType(g_prim_t.t_void, fixed_array(g_prim_t.LLVMVoidPtrType(), g_prim_t.LLVMVoidPtrType(), g_prim_t.LLVMVoidPtrType()))) LLVMAddGlobalMapping(g_engine, jit_array_lock, get_jit_array_lock()) LLVMAddAttributesToFunction(jit_array_lock, fixed_array(nounwind, willreturn)) LLVMAddAttributeToFunctionArgumentRange(jit_array_lock, urange(0, 2), nocapture) - // void jit_array_unlock ( Array & array, Context * context ) + // void jit_array_unlock ( Array & array, Context * context, LineInfoArg * at ) var jit_array_unlock = LLVMAddFunctionWithType(g_mod, FN_JIT_ARRAY_UNLOCK, - LLVMFunctionType(g_prim_t.LLVMVoidPtrType(), + LLVMFunctionType(g_prim_t.t_void, fixed_array( g_prim_t.LLVMVoidPtrType(), // arr g_prim_t.LLVMVoidPtrType(), // ctx @@ -509,21 +565,22 @@ def public init_jit(cg_opt_level : uint) { LLVMAddGlobalMapping(g_engine, jit_str_cat, get_jit_str_cat()) LLVMAddAttributesToFunction(jit_str_cat, fixed_array(nounwind, willreturn)) LLVMAddAttributeToFunctionArgumentRange(jit_str_cat, urange(0, 3), nocapture) - // void jit_prologue ( void * line_info, int32_t stackSize, JitStackState * stackState, Context * context ) + // void jit_prologue ( char * funcName, void * funcLineInfo, int32_t stackSize, JitStackState * stackState, Context * context, LineInfoArg * at ) var jit_prologue = LLVMAddFunctionWithType(g_mod, FN_JIT_PROLOGUE, LLVMFunctionType(g_prim_t.t_void, fixed_array( - g_prim_t.get_type_string(), - g_prim_t.LLVMVoidPtrType(), - g_prim_t.t_int32, - LLVMPointerType(g_t_stack_state, 0u), - g_prim_t.LLVMVoidPtrType() + g_prim_t.get_type_string(), // funcName + g_prim_t.LLVMVoidPtrType(), // funcLineInfo + g_prim_t.t_int32, // stackSize + LLVMPointerType(g_t_stack_state, 0u), // stackState + g_prim_t.LLVMVoidPtrType(), // context + g_prim_t.LLVMVoidPtrType() // at ) ) ) LLVMAddGlobalMapping(g_engine, jit_prologue, get_jit_prologue()) LLVMAddAttributesToFunction(jit_prologue, fixed_array(nounwind, willreturn)) - LLVMAddAttributeToFunctionArgumentRange(jit_prologue, urange(3, 5), nocapture) + LLVMAddAttributeToFunctionArgumentRange(jit_prologue, urange(3, 6), nocapture) // void jit_epilogue ( JitStackState * stackState, Context * context ) var jit_epilogue = LLVMAddFunctionWithType(g_mod, FN_JIT_EPILOGUE, LLVMFunctionType(g_prim_t.t_void, @@ -564,7 +621,7 @@ def public init_jit(cg_opt_level : uint) { LLVMAddAttributeToFunctionArgumentRange(jit_iterator_iterate, urange(0, 3), nocapture) // void builtin_iterator_delete ( const Sequence & it, Context * context ) { var jit_iterator_delete = LLVMAddFunctionWithType(g_mod, FN_JIT_ITERATOR_DELETE, - LLVMFunctionType(g_prim_t.t_int1, + LLVMFunctionType(g_prim_t.t_void, fixed_array(LLVMPointerType(g_t_sequence, 0u), g_prim_t.LLVMVoidPtrType()))) LLVMAddGlobalMapping(g_engine, jit_iterator_delete, get_jit_iterator_delete()) LLVMAddAttributesToFunction(jit_iterator_delete, fixed_array(nounwind, willreturn)) @@ -580,11 +637,12 @@ def public init_jit(cg_opt_level : uint) { var jit_iterator_next = LLVMAddFunctionWithType(g_mod, FN_JIT_ITERATOR_NEXT, LLVMFunctionType(g_prim_t.t_int1, fixed_array(LLVMPointerType(g_t_sequence, 0u), g_prim_t.LLVMVoidPtrType(), g_prim_t.LLVMVoidPtrType(), g_prim_t.LLVMVoidPtrType()))) - let simnode_interop_type = LLVMFunctionType(g_prim_t.LLVMVoidPtrType(), + // void jit_simnode_interop ( void * ptr, int argCount, TypeInfo ** types ) + let simnode_interop_type = LLVMFunctionType(g_prim_t.t_void, fixed_array( - g_prim_t.LLVMVoidPtrType(), // context - g_prim_t.t_int32, - g_prim_t.LLVMVoidPtrType() // argv + g_prim_t.LLVMVoidPtrType(), // ptr (placement-new buffer for SimNode_AotInteropBase) + g_prim_t.t_int32, // argCount + g_prim_t.LLVMVoidPtrType() // types ) ) var jit_simnode_interop = LLVMAddFunctionWithType( @@ -827,7 +885,14 @@ def public write_wasm(mod : LLVMOpaqueModule?; out_path : string; triple : strin to_log(LOG_WARNING, "wasm: runtime symbols referenced but libDaScript_runtime.a not found at web/output/lib/ — run the web/ emscripten build to link in the runtime, or pass --jit-runtime-lib=\n") } let real_triple = (!(triple |> empty()) ? triple : "wasm32-unknown-emscripten") - with_target_machine(real_triple, "", "", 2u) $(targetMachine : LLVMTargetMachineRef) { + // Match the feature set web/CMakeLists.txt compiles the runtime archive with + // (-msimd128 -mnontrapping-fptoint). Without +simd128 the host LLVM lowers + // vec4f returns via sret while emcc returns them as native v128, causing + // function-signature mismatches at wasm-ld link time and `unreachable` + // traps at runtime on any helper that returns vec4f (jit_invoke_block_*, + // jit_call_*). + let features = "+simd128,+nontrapping-fptoint" + with_target_machine(real_triple, "", features, 2u) $(targetMachine : LLVMTargetMachineRef) { // Pin the module's data layout + triple so codegen sizes pointers as // 32-bit. Without this the module inherits host layout and produces // wrong-sized loads/stores in the emitted wasm. diff --git a/modules/dasLLVM/daslib/llvm_macro.das b/modules/dasLLVM/daslib/llvm_macro.das index 8a2962621f..7edc922f72 100644 --- a/modules/dasLLVM/daslib/llvm_macro.das +++ b/modules/dasLLVM/daslib/llvm_macro.das @@ -26,7 +26,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // invalidates cached DLLs (e.g. edits to llvm_jit.das, llvm_macro.das, llvm_jit_common.das, // runtime helper ABI, default target triple). Cache filenames fold this in, so a bump // makes every previously written DLL miss the cache on the next run and get GC'd. -let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x04ul +let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x09ul let JIT_FNV_PRIME : uint64 = 1099511628211ul @@ -236,7 +236,10 @@ class JIT_LLVM : AstSimulateMacro { dll_hash_basename = jit_dll_basename(funcs, opt_level, size_level, emit_prologue, debug_info) output_path = ".jitted_scripts/{prog.thisNamespace}/{dll_hash_basename}" } - init_jit(opt_level |> uint) + // Pass target_triple so init_jit can pin the module data layout + // BEFORE IR construction (wasm32 cross-compile needs 4-byte pointer + // sizes baked into GEP folding from the very first IR instruction). + init_jit(opt_level |> uint, target_triple) var jit_flags : LlvmJitFlags jit_flags.emit_prologue = emit_prologue diff --git a/site/tests/playground/dropdowns.spec.js b/site/tests/playground/dropdowns.spec.js index 54d9fce9ab..e52ce49f5d 100644 --- a/site/tests/playground/dropdowns.spec.js +++ b/site/tests/playground/dropdowns.spec.js @@ -1,7 +1,8 @@ // The Examples