nomen-lang 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/audit_runtime.c +157 -19
- package/core/System/Console.nm +26 -0
- package/core/System/Test.nm +18 -0
- package/dist/index.mjs +395 -182
- package/package.json +1 -1
- package/src/index.ts +8 -1
- package/src/test.ts +166 -53
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
import { execFile, execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import { execFile, execFileSync, execSync, spawn } from "node:child_process";
|
|
3
3
|
import fs, { stat, unwatchFile, watch, watchFile } from "node:fs";
|
|
4
4
|
import * as sp from "node:path";
|
|
5
5
|
import path, { join, resolve, sep } from "node:path";
|
|
@@ -2122,8 +2122,40 @@ function try_parse_operands(parts) {
|
|
|
2122
2122
|
* is not an instruction — label, directive, comment, blank, OR an
|
|
2123
2123
|
* instruction outside the contract table). Shared by the validator and the
|
|
2124
2124
|
* phase-2 optimizer so both see identical shapes.
|
|
2125
|
-
|
|
2125
|
+
*
|
|
2126
|
+
* Memoized by raw text: the post-processing pipeline (frame slots, coalesce,
|
|
2127
|
+
* loop promote, if-convert, float forwarding, widen masks, …) plus the
|
|
2128
|
+
* validators each re-parse the SAME instruction texts — prologue/epilogue
|
|
2129
|
+
* lines repeat across every function, so the hit rate is very high. Every
|
|
2130
|
+
* call returns a PRIVATE deep copy (the passes treat parsed instructions as
|
|
2131
|
+
* mutable IR — coalesce renames operands in place — so sharing a parsed
|
|
2132
|
+
* object would corrupt the memo and cross-pass state).
|
|
2133
|
+
*/
|
|
2134
|
+
const parse_memo = /* @__PURE__ */ new Map();
|
|
2135
|
+
const PARSE_MEMO_CAP = 2e5;
|
|
2136
|
+
function clone_parsed(c, line) {
|
|
2137
|
+
return {
|
|
2138
|
+
text: c.text,
|
|
2139
|
+
op: c.op,
|
|
2140
|
+
setsFlags: c.setsFlags,
|
|
2141
|
+
readsFlags: c.readsFlags,
|
|
2142
|
+
operands: c.operands.map((o) => {
|
|
2143
|
+
const copy = { ...o };
|
|
2144
|
+
if (copy.kind === "mem" && copy.offset) copy.offset = { ...copy.offset };
|
|
2145
|
+
return copy;
|
|
2146
|
+
}),
|
|
2147
|
+
line
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2126
2150
|
function parse_asm_instruction(text, line) {
|
|
2151
|
+
const cached = parse_memo.get(text);
|
|
2152
|
+
if (cached !== void 0) return cached === null ? null : clone_parsed(cached, line);
|
|
2153
|
+
const parsed = parse_asm_instruction_uncached(text, line);
|
|
2154
|
+
if (parse_memo.size >= PARSE_MEMO_CAP) parse_memo.clear();
|
|
2155
|
+
parse_memo.set(text, parsed ? clone_parsed(parsed, 0) : null);
|
|
2156
|
+
return parsed;
|
|
2157
|
+
}
|
|
2158
|
+
function parse_asm_instruction_uncached(text, line) {
|
|
2127
2159
|
const t = strip_comment$1(text).trim();
|
|
2128
2160
|
const m = /^([a-z][a-z0-9.]*)\s*(.*)$/.exec(t);
|
|
2129
2161
|
if (!m) return null;
|
|
@@ -2274,6 +2306,13 @@ function parse_mem_inner(inner) {
|
|
|
2274
2306
|
if (tail_shift) mem.scale = 1 << parseInt(tail_shift[1], 10);
|
|
2275
2307
|
continue;
|
|
2276
2308
|
}
|
|
2309
|
+
if (/^[A-Za-z_.$][\w.$]*@[A-Za-z0-9_]+$/.test(comp)) {
|
|
2310
|
+
mem.offset = {
|
|
2311
|
+
kind: "reg",
|
|
2312
|
+
name: comp
|
|
2313
|
+
};
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2277
2316
|
return null;
|
|
2278
2317
|
}
|
|
2279
2318
|
return mem;
|
|
@@ -2407,34 +2446,13 @@ function validate_asm(code) {
|
|
|
2407
2446
|
}
|
|
2408
2447
|
return errors;
|
|
2409
2448
|
}
|
|
2410
|
-
/**
|
|
2411
|
-
*
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
2414
|
-
|
|
2415
|
-
* the lifted function bodies:
|
|
2416
|
-
*
|
|
2417
|
-
* - Block splitting: labels and branch/terminator instructions bound blocks,
|
|
2418
|
-
* exactly like the CFG the emitters produce.
|
|
2419
|
-
* - Per-instruction sp effects: pre-indexed `stp/str [sp, #-N]!` subtracts,
|
|
2420
|
-
* post-indexed `ldp/ldr [sp], #N` adds, `sub/add sp, sp, #imm` adjusts;
|
|
2421
|
-
* any other sp write (`mov sp, xN`, register-scaled adds, `ldr sp, …`)
|
|
2422
|
-
* is UNKNOWN and conservatively poisons downstream deltas.
|
|
2423
|
-
* - Join = require equal deltas or unknown: a label reached by paths with
|
|
2424
|
-
* differing deltas becomes unknown; only a KNOWN non-zero delta at a
|
|
2425
|
-
* `ret` is reported. Unknown never errors — the check is conservative by
|
|
2426
|
-
* construction (no false positives, only silence).
|
|
2427
|
-
* - `bl`/`blr` preserve sp (the callee rebalances under the ABI) and do not
|
|
2428
|
-
* terminate blocks; `br xN` (tail-jump) ends propagation with no check;
|
|
2429
|
-
* `svc` continues (the kernel preserves sp).
|
|
2430
|
-
* - Branch targets resolve within the function, including the numeric local
|
|
2431
|
-
* form raw `#arch` blocks use (`1:` / `b.hs 1f` — forward/backward by
|
|
2432
|
-
* suffix); an unresolvable target makes downstream deltas unknown.
|
|
2433
|
-
*/
|
|
2434
|
-
function validate_stack_balance(code) {
|
|
2435
|
-
const { result } = lift_functions(code);
|
|
2436
|
-
if (!result.ok) return [];
|
|
2449
|
+
/** Stack-balance check over an ALREADY-lifted result — lets the build
|
|
2450
|
+
* pipeline lift+validate once and share the structured functions between
|
|
2451
|
+
* the shape/branch validation and the balance pass (lift_functions runs
|
|
2452
|
+
* validate_asm internally, so re-lifting per check doubled the cost). */
|
|
2453
|
+
function stack_balance_errors_from_lift(result) {
|
|
2437
2454
|
const errors = [];
|
|
2455
|
+
if (!result.ok) return [];
|
|
2438
2456
|
for (const fn of result.functions) check_function_balance(fn, errors);
|
|
2439
2457
|
return errors;
|
|
2440
2458
|
}
|
|
@@ -2562,37 +2580,37 @@ function line_effect(text) {
|
|
|
2562
2580
|
};
|
|
2563
2581
|
}
|
|
2564
2582
|
/** Resolve a branch target token to a body line index, or null when the
|
|
2565
|
-
* target is outside this function
|
|
2566
|
-
*
|
|
2567
|
-
|
|
2583
|
+
* target is outside this function. Backed by the per-function label maps
|
|
2584
|
+
* built once in check_function_balance — a linear rescan of the body per
|
|
2585
|
+
* pending branch used to be the single hottest spot in large builds
|
|
2586
|
+
* (branches × body-length re-stripping + regex). Numeric `Nf`/`Nb` forms
|
|
2587
|
+
* resolve directionally against the (possibly redefined) definitions,
|
|
2588
|
+
* matching the assembler's semantics; a named label resolves to its FIRST
|
|
2589
|
+
* definition (the old body scan returned the first match too). */
|
|
2590
|
+
function resolve_target(token, from, named_labels, numeric_labels) {
|
|
2568
2591
|
const numeric = /^(\d+)([fb])$/.exec(token);
|
|
2569
2592
|
if (numeric) {
|
|
2570
|
-
const
|
|
2593
|
+
const defs = numeric_labels.get(numeric[1]);
|
|
2594
|
+
if (!defs) return null;
|
|
2571
2595
|
if (numeric[2] === "f") {
|
|
2572
|
-
for (let
|
|
2573
|
-
const lm = /^(\d+):/.exec(strip_comment$1(body[i].text).trim());
|
|
2574
|
-
if (lm && lm[1] === n) return i;
|
|
2575
|
-
}
|
|
2596
|
+
for (let k = 0; k < defs.length; k++) if (defs[k] > from) return defs[k];
|
|
2576
2597
|
return null;
|
|
2577
2598
|
}
|
|
2578
|
-
for (let
|
|
2579
|
-
const lm = /^(\d+):/.exec(strip_comment$1(body[i].text).trim());
|
|
2580
|
-
if (lm && lm[1] === n) return i;
|
|
2581
|
-
}
|
|
2599
|
+
for (let k = defs.length - 1; k >= 0; k--) if (defs[k] < from) return defs[k];
|
|
2582
2600
|
return null;
|
|
2583
2601
|
}
|
|
2584
|
-
|
|
2585
|
-
const lm = LABEL_RE.exec(strip_comment$1(body[i].text).trim());
|
|
2586
|
-
if (lm && lm[1] === token) return i;
|
|
2587
|
-
}
|
|
2588
|
-
return null;
|
|
2602
|
+
return named_labels.get(token) ?? null;
|
|
2589
2603
|
}
|
|
2590
2604
|
function check_function_balance(fn, errors) {
|
|
2591
2605
|
const body = fn.body;
|
|
2606
|
+
const stripped_text = [];
|
|
2592
2607
|
const effects = [];
|
|
2593
2608
|
const is_code = [];
|
|
2609
|
+
const named_labels = /* @__PURE__ */ new Map();
|
|
2610
|
+
const numeric_labels = /* @__PURE__ */ new Map();
|
|
2594
2611
|
for (let i = 0; i < body.length; i++) {
|
|
2595
2612
|
const t = strip_comment$1(body[i].text).trim();
|
|
2613
|
+
stripped_text.push(t);
|
|
2596
2614
|
effects.push({
|
|
2597
2615
|
delta: null,
|
|
2598
2616
|
branch: null,
|
|
@@ -2601,8 +2619,15 @@ function check_function_balance(fn, errors) {
|
|
|
2601
2619
|
unknown_jump: false
|
|
2602
2620
|
});
|
|
2603
2621
|
is_code.push(false);
|
|
2604
|
-
if (!t) continue;
|
|
2605
2622
|
const lm = LABEL_RE.exec(t);
|
|
2623
|
+
if (lm) {
|
|
2624
|
+
if (/^\d+$/.test(lm[1])) {
|
|
2625
|
+
const defs = numeric_labels.get(lm[1]);
|
|
2626
|
+
if (defs) defs.push(i);
|
|
2627
|
+
else numeric_labels.set(lm[1], [i]);
|
|
2628
|
+
} else if (!named_labels.has(lm[1])) named_labels.set(lm[1], i);
|
|
2629
|
+
}
|
|
2630
|
+
if (!t) continue;
|
|
2606
2631
|
const rest = lm ? lm[2].trim() : t;
|
|
2607
2632
|
if (rest.startsWith(".") || /^[\w.$]+\s*=\s*[\w.$]+$/.test(rest) || !rest) continue;
|
|
2608
2633
|
is_code[i] = true;
|
|
@@ -2614,7 +2639,7 @@ function check_function_balance(fn, errors) {
|
|
|
2614
2639
|
let cur = -1;
|
|
2615
2640
|
let terminated = true;
|
|
2616
2641
|
for (let i = 0; i < body.length; i++) {
|
|
2617
|
-
const t =
|
|
2642
|
+
const t = stripped_text[i];
|
|
2618
2643
|
if (!t) continue;
|
|
2619
2644
|
if (LABEL_RE.test(t)) {
|
|
2620
2645
|
cur++;
|
|
@@ -2662,7 +2687,7 @@ function check_function_balance(fn, errors) {
|
|
|
2662
2687
|
}
|
|
2663
2688
|
}
|
|
2664
2689
|
for (const p of pending) {
|
|
2665
|
-
const line = resolve_target(p.token, p.from,
|
|
2690
|
+
const line = resolve_target(p.token, p.from, named_labels, numeric_labels);
|
|
2666
2691
|
blocks[p.block].targets.push(line === null ? null : line_block.get(line) ?? null);
|
|
2667
2692
|
}
|
|
2668
2693
|
const preds = Array.from({ length: blocks.length }, () => []);
|
|
@@ -7511,6 +7536,13 @@ var DeclarationNode = class extends BaseNode {
|
|
|
7511
7536
|
* a heap shell for the argument (pass-by-value): the callee frees the
|
|
7512
7537
|
* shell itself at scope exit (`free(p)`) after freeing the fields. */
|
|
7513
7538
|
owned_heap_shell;
|
|
7539
|
+
/** True for a `move` param of an owning VALUE-struct type: the caller
|
|
7540
|
+
* passed its storage by address, so the callee owns the CONTENTS but not
|
|
7541
|
+
* the storage. free_scoped_declarations destroys the fields' owned
|
|
7542
|
+
* resources at `(*param)` at scope exit — never a free. Set at build
|
|
7543
|
+
* time by the C function/method builders; consumed by
|
|
7544
|
+
* free_scoped_declarations. */
|
|
7545
|
+
moved_value_struct_param;
|
|
7514
7546
|
constructor(start, visibility, declaration, name, type, value) {
|
|
7515
7547
|
super("declare", start);
|
|
7516
7548
|
this.visibility = visibility;
|
|
@@ -8972,6 +9004,85 @@ function record_ctor_heap_default_fields(var_name, struct_node, status, is_owned
|
|
|
8972
9004
|
}
|
|
8973
9005
|
}
|
|
8974
9006
|
//#endregion
|
|
9007
|
+
//#region ../src/build_common/destroy_analysis.ts
|
|
9008
|
+
/**
|
|
9009
|
+
* Whether any field of the struct is an enum-with-data value owning heap
|
|
9010
|
+
* payloads (a string payload — always heap, case construction strdups it —
|
|
9011
|
+
* or a class/trait reference payload — construction transfers ownership).
|
|
9012
|
+
* Such a field must be reclaimed when the struct dies.
|
|
9013
|
+
*/
|
|
9014
|
+
function struct_has_owning_enum_field(struct, table) {
|
|
9015
|
+
for (const field of struct.fields) {
|
|
9016
|
+
if (field.type.is_ref) continue;
|
|
9017
|
+
const en = table.enums?.find((e) => e.name === field.type.name);
|
|
9018
|
+
if (!en?.has_associated_data) continue;
|
|
9019
|
+
for (const c of en.cases) for (const p of c.params) {
|
|
9020
|
+
if (p.type.name === "string" && !p.type.is_array) return true;
|
|
9021
|
+
if (table.traits?.some((t) => t.name === p.type.name)) return true;
|
|
9022
|
+
if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
|
|
9023
|
+
}
|
|
9024
|
+
}
|
|
9025
|
+
return false;
|
|
9026
|
+
}
|
|
9027
|
+
/**
|
|
9028
|
+
* Whether a struct declares its own `#destroy` (user side effects /
|
|
9029
|
+
* resource release).
|
|
9030
|
+
*/
|
|
9031
|
+
function has_destroy(struct) {
|
|
9032
|
+
return !!struct.functions.find((f) => f.name === "#destroy");
|
|
9033
|
+
}
|
|
9034
|
+
/**
|
|
9035
|
+
* Whether any field (recursively) requires destroy handling — a class
|
|
9036
|
+
* field (heap instance to reclaim) or a nested struct with its own
|
|
9037
|
+
* `#destroy` (or nested fields that do). Deliberately excludes `string`
|
|
9038
|
+
* fields: a struct local whose only owning fields are strings is NOT
|
|
9039
|
+
* auto-destroyed at scope exit (the strings may be raw rodata arg
|
|
9040
|
+
* pointers, not heap) — see `struct_needs_auto_destroy` for the broader
|
|
9041
|
+
* form used to decide whether a `<Struct>_destroy` function is generated.
|
|
9042
|
+
*/
|
|
9043
|
+
function has_struct_fields_with_destroy(struct, table) {
|
|
9044
|
+
for (const field of struct.fields) {
|
|
9045
|
+
if (field.type.is_ref) continue;
|
|
9046
|
+
const field_struct = resolve_struct_type(field.type, table);
|
|
9047
|
+
if (!field_struct) continue;
|
|
9048
|
+
if (field_struct.is_class) return true;
|
|
9049
|
+
if (has_destroy(field_struct)) return true;
|
|
9050
|
+
if (has_struct_fields_with_destroy(field_struct, table)) return true;
|
|
9051
|
+
}
|
|
9052
|
+
return false;
|
|
9053
|
+
}
|
|
9054
|
+
/**
|
|
9055
|
+
* Whether a struct (or any embedded struct field, recursively) needs a
|
|
9056
|
+
* destroy call at scope exit — it has a `#destroy`, a class-typed field,
|
|
9057
|
+
* or a nested struct field that itself needs destroying. This does NOT
|
|
9058
|
+
* count `string` fields (see `has_struct_fields_with_destroy`).
|
|
9059
|
+
*/
|
|
9060
|
+
function struct_needs_destroy(struct, table) {
|
|
9061
|
+
return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
|
|
9062
|
+
}
|
|
9063
|
+
/**
|
|
9064
|
+
* Whether a struct needs an auto-generated `<Struct>_destroy` function.
|
|
9065
|
+
* Broader than `struct_needs_destroy`: also returns true for structs with
|
|
9066
|
+
* `string` fields, because a Buffer<T> for such a struct deep-copies the
|
|
9067
|
+
* strings into slots (strdup on store) and the per-element destroy must
|
|
9068
|
+
* free them. The generated destroy frees each string field; it is called
|
|
9069
|
+
* from Buffer's specialized #destroy / replace_T, NOT from struct local
|
|
9070
|
+
* scope exit (`struct_needs_destroy` governs that path and excludes
|
|
9071
|
+
* strings).
|
|
9072
|
+
*/
|
|
9073
|
+
function struct_needs_auto_destroy(struct, table) {
|
|
9074
|
+
if (struct_needs_destroy(struct, table)) return true;
|
|
9075
|
+
if (struct_has_owning_enum_field(struct, table)) return true;
|
|
9076
|
+
for (const field of struct.fields) {
|
|
9077
|
+
if (field.type.is_ref) continue;
|
|
9078
|
+
if (field.type.is_view) continue;
|
|
9079
|
+
if (field.type.name === "string" && !field.type.is_array) return true;
|
|
9080
|
+
const field_struct = resolve_struct_type(field.type, table);
|
|
9081
|
+
if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
|
|
9082
|
+
}
|
|
9083
|
+
return false;
|
|
9084
|
+
}
|
|
9085
|
+
//#endregion
|
|
8975
9086
|
//#region ../src/nodes/child_nodes.ts
|
|
8976
9087
|
/** Whether `value` is an AST node (carries a string `node_type`). */
|
|
8977
9088
|
function is_ast_node(value) {
|
|
@@ -11648,85 +11759,6 @@ function array_struct_name(type, status) {
|
|
|
11648
11759
|
return status.structs.find((s) => s.name === mono && !s.is_generic) ? mono : void 0;
|
|
11649
11760
|
}
|
|
11650
11761
|
//#endregion
|
|
11651
|
-
//#region ../src/build_common/destroy_analysis.ts
|
|
11652
|
-
/**
|
|
11653
|
-
* Whether any field of the struct is an enum-with-data value owning heap
|
|
11654
|
-
* payloads (a string payload — always heap, case construction strdups it —
|
|
11655
|
-
* or a class/trait reference payload — construction transfers ownership).
|
|
11656
|
-
* Such a field must be reclaimed when the struct dies.
|
|
11657
|
-
*/
|
|
11658
|
-
function struct_has_owning_enum_field(struct, table) {
|
|
11659
|
-
for (const field of struct.fields) {
|
|
11660
|
-
if (field.type.is_ref) continue;
|
|
11661
|
-
const en = table.enums?.find((e) => e.name === field.type.name);
|
|
11662
|
-
if (!en?.has_associated_data) continue;
|
|
11663
|
-
for (const c of en.cases) for (const p of c.params) {
|
|
11664
|
-
if (p.type.name === "string" && !p.type.is_array) return true;
|
|
11665
|
-
if (table.traits?.some((t) => t.name === p.type.name)) return true;
|
|
11666
|
-
if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
|
|
11667
|
-
}
|
|
11668
|
-
}
|
|
11669
|
-
return false;
|
|
11670
|
-
}
|
|
11671
|
-
/**
|
|
11672
|
-
* Whether a struct declares its own `#destroy` (user side effects /
|
|
11673
|
-
* resource release).
|
|
11674
|
-
*/
|
|
11675
|
-
function has_destroy(struct) {
|
|
11676
|
-
return !!struct.functions.find((f) => f.name === "#destroy");
|
|
11677
|
-
}
|
|
11678
|
-
/**
|
|
11679
|
-
* Whether any field (recursively) requires destroy handling — a class
|
|
11680
|
-
* field (heap instance to reclaim) or a nested struct with its own
|
|
11681
|
-
* `#destroy` (or nested fields that do). Deliberately excludes `string`
|
|
11682
|
-
* fields: a struct local whose only owning fields are strings is NOT
|
|
11683
|
-
* auto-destroyed at scope exit (the strings may be raw rodata arg
|
|
11684
|
-
* pointers, not heap) — see `struct_needs_auto_destroy` for the broader
|
|
11685
|
-
* form used to decide whether a `<Struct>_destroy` function is generated.
|
|
11686
|
-
*/
|
|
11687
|
-
function has_struct_fields_with_destroy(struct, table) {
|
|
11688
|
-
for (const field of struct.fields) {
|
|
11689
|
-
if (field.type.is_ref) continue;
|
|
11690
|
-
const field_struct = resolve_struct_type(field.type, table);
|
|
11691
|
-
if (!field_struct) continue;
|
|
11692
|
-
if (field_struct.is_class) return true;
|
|
11693
|
-
if (has_destroy(field_struct)) return true;
|
|
11694
|
-
if (has_struct_fields_with_destroy(field_struct, table)) return true;
|
|
11695
|
-
}
|
|
11696
|
-
return false;
|
|
11697
|
-
}
|
|
11698
|
-
/**
|
|
11699
|
-
* Whether a struct (or any embedded struct field, recursively) needs a
|
|
11700
|
-
* destroy call at scope exit — it has a `#destroy`, a class-typed field,
|
|
11701
|
-
* or a nested struct field that itself needs destroying. This does NOT
|
|
11702
|
-
* count `string` fields (see `has_struct_fields_with_destroy`).
|
|
11703
|
-
*/
|
|
11704
|
-
function struct_needs_destroy(struct, table) {
|
|
11705
|
-
return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
|
|
11706
|
-
}
|
|
11707
|
-
/**
|
|
11708
|
-
* Whether a struct needs an auto-generated `<Struct>_destroy` function.
|
|
11709
|
-
* Broader than `struct_needs_destroy`: also returns true for structs with
|
|
11710
|
-
* `string` fields, because a Buffer<T> for such a struct deep-copies the
|
|
11711
|
-
* strings into slots (strdup on store) and the per-element destroy must
|
|
11712
|
-
* free them. The generated destroy frees each string field; it is called
|
|
11713
|
-
* from Buffer's specialized #destroy / replace_T, NOT from struct local
|
|
11714
|
-
* scope exit (`struct_needs_destroy` governs that path and excludes
|
|
11715
|
-
* strings).
|
|
11716
|
-
*/
|
|
11717
|
-
function struct_needs_auto_destroy(struct, table) {
|
|
11718
|
-
if (struct_needs_destroy(struct, table)) return true;
|
|
11719
|
-
if (struct_has_owning_enum_field(struct, table)) return true;
|
|
11720
|
-
for (const field of struct.fields) {
|
|
11721
|
-
if (field.type.is_ref) continue;
|
|
11722
|
-
if (field.type.is_view) continue;
|
|
11723
|
-
if (field.type.name === "string" && !field.type.is_array) return true;
|
|
11724
|
-
const field_struct = resolve_struct_type(field.type, table);
|
|
11725
|
-
if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
|
|
11726
|
-
}
|
|
11727
|
-
return false;
|
|
11728
|
-
}
|
|
11729
|
-
//#endregion
|
|
11730
11762
|
//#region ../src/build_common/scan_force_heap_strings.ts
|
|
11731
11763
|
function scan_force_heap_strings(statements, structs) {
|
|
11732
11764
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -14909,7 +14941,33 @@ function emit_base_ptr(status, decl_name, is_class_parent) {
|
|
|
14909
14941
|
if (is_class_parent) emit_var_load(status, "x0", decl_name, 8);
|
|
14910
14942
|
else emit_var_address(status, "x0", decl_name);
|
|
14911
14943
|
}
|
|
14912
|
-
|
|
14944
|
+
/**
|
|
14945
|
+
* Reclaim a struct's owning fields in place (base + every nested owning field).
|
|
14946
|
+
*
|
|
14947
|
+
* `base_offset` (added to each field offset) and `is_class_parent` (a class's
|
|
14948
|
+
* base is a pointer LOADED from the slot; a value struct is inline AT it)
|
|
14949
|
+
* locate `decl_name`'s storage.
|
|
14950
|
+
*
|
|
14951
|
+
* `free_strings` is REQUIRED — it is a per-call-site OWNOWNITY fact, not a
|
|
14952
|
+
* per-type one, so it deliberately has no default:
|
|
14953
|
+
* - `true` from a DESTROY function (`<T>_destroy`), which only ever runs on
|
|
14954
|
+
* heap-owned storage: a class's plain string fields are always heap-owned
|
|
14955
|
+
* (`_init` strdup's defaults, assignments strdup a non-heap RHS), and a
|
|
14956
|
+
* VALUE struct arrives with the per-slot copies `store_T`/`_copy` strdup'd
|
|
14957
|
+
* (Buffer per-element teardown, the displaced-field reclaim, the
|
|
14958
|
+
* anchor-slot walk). Matches C's build_auto_destroy, which frees string
|
|
14959
|
+
* fields unconditionally.
|
|
14960
|
+
* - `false` from struct-LOCAL scope exit, where a string field may still hold
|
|
14961
|
+
* a rodata literal (aarch64 constructors don't strdup) and freeing it would
|
|
14962
|
+
* `free` rodata (SIGABRT).
|
|
14963
|
+
*
|
|
14964
|
+
* A default (`= true`) previously stood in for the `true` arm while
|
|
14965
|
+
* `node.is_class` (undefined on plain parsed structs) was passed as if it
|
|
14966
|
+
* selected between the arms — so the day `is_class` becomes explicitly false,
|
|
14967
|
+
* value-struct slot strings would silently stop being reclaimed. Passing the
|
|
14968
|
+
* boolean at every call site makes both arms visible.
|
|
14969
|
+
*/
|
|
14970
|
+
function emit_field_destroys(status, struct_type, decl_name, base_offset, is_class_parent, free_strings) {
|
|
14913
14971
|
const destroy_fields = struct_type.is_class ? layout_fields(struct_type, status) : struct_type.fields;
|
|
14914
14972
|
for (const field of destroy_fields) {
|
|
14915
14973
|
const offset = get_field_offset_of_fields(destroy_fields, field.name, status);
|
|
@@ -20720,16 +20778,19 @@ function build_function_node$1(node, status) {
|
|
|
20720
20778
|
if (param.is_moved) {
|
|
20721
20779
|
const is_class = !!status.structs.find((s) => s.name === param.type.name && s.is_class);
|
|
20722
20780
|
const is_trait = !!status.traits.find((t) => t.name === param.type.name);
|
|
20723
|
-
|
|
20781
|
+
const vstruct = !is_class && !is_trait ? resolve_struct_type(param.type, status) : void 0;
|
|
20782
|
+
const is_owning_value_struct = !!vstruct && !vstruct.is_class && !param.type.is_nullable && struct_needs_destroy(vstruct, status);
|
|
20783
|
+
if (is_class || is_trait || is_owning_value_struct) {
|
|
20724
20784
|
const reg = callee_map.get(param.name) ?? param_regs[pidx];
|
|
20725
|
-
status.moved_class_params.set(param.name, reg);
|
|
20785
|
+
if (is_class || is_trait) status.moved_class_params.set(param.name, reg);
|
|
20726
20786
|
const save_offset = allocate_stack_space(status, 8);
|
|
20727
20787
|
emit_asm(status, `str ${reg}, [x29, #${save_offset}]\n`);
|
|
20728
20788
|
moved_param_save_slots.set(param.name, {
|
|
20729
20789
|
offset: save_offset,
|
|
20730
20790
|
type_name: param.type.name,
|
|
20731
20791
|
type_args: param.type.type_args,
|
|
20732
|
-
is_nullable: param.type.is_nullable
|
|
20792
|
+
is_nullable: param.type.is_nullable,
|
|
20793
|
+
value_struct_destroy_name: is_owning_value_struct ? vstruct.name : void 0
|
|
20733
20794
|
});
|
|
20734
20795
|
}
|
|
20735
20796
|
}
|
|
@@ -20837,6 +20898,16 @@ function build_function_node$1(node, status) {
|
|
|
20837
20898
|
emit_asm(status, `cmp x0, x1\n`);
|
|
20838
20899
|
emit_asm(status, `beq ${keep_prefix}_${name}\n`);
|
|
20839
20900
|
}
|
|
20901
|
+
if (info.value_struct_destroy_name) {
|
|
20902
|
+
const vstruct = status.structs.find((s) => s.name === info.value_struct_destroy_name);
|
|
20903
|
+
release_heap_string_fields(status, name, info.value_struct_destroy_name);
|
|
20904
|
+
if (vstruct && has_destroy(vstruct)) {
|
|
20905
|
+
emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
|
|
20906
|
+
emit_asm(status, `bl ${info.value_struct_destroy_name}_destroy\n`);
|
|
20907
|
+
} else if (vstruct) emit_field_destroys(status, vstruct, name, void 0, void 0, false);
|
|
20908
|
+
if (need_guard) emit_asm(status, `${keep_prefix}_${name}:\n`);
|
|
20909
|
+
continue;
|
|
20910
|
+
}
|
|
20840
20911
|
emit_destroy_for_anchor_slot(status, info.offset, info.type_name, info.type_args, info.is_nullable);
|
|
20841
20912
|
emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
|
|
20842
20913
|
emit_free(status);
|
|
@@ -21047,6 +21118,7 @@ function init_nullable_field_default(node, field, base_reg, status) {
|
|
|
21047
21118
|
* sites record it (record_ctor_heap_default_fields) so scope exit frees it.
|
|
21048
21119
|
*/
|
|
21049
21120
|
function init_expr_field_default(node, field, offset, base_reg, status) {
|
|
21121
|
+
status.last_result_is_heap = false;
|
|
21050
21122
|
build_node(field.value, status);
|
|
21051
21123
|
ensure_newline(status);
|
|
21052
21124
|
const t = field.type;
|
|
@@ -21215,7 +21287,7 @@ function build_destroy_function(node, func, status) {
|
|
|
21215
21287
|
status.code = status.code.slice(0, after_prologue) + saves + status.code.slice(after_prologue);
|
|
21216
21288
|
}
|
|
21217
21289
|
}
|
|
21218
|
-
if (node.is_class) emit_field_destroys(status, node, "self", void 0, false);
|
|
21290
|
+
if (node.is_class) emit_field_destroys(status, node, "self", void 0, false, true);
|
|
21219
21291
|
emit_asm(status, `${return_label}:\n`);
|
|
21220
21292
|
const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
|
|
21221
21293
|
status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
|
|
@@ -21272,7 +21344,7 @@ function build_auto_destroy_function(node, status) {
|
|
|
21272
21344
|
status.function_param_regs.set("self", "x19");
|
|
21273
21345
|
emit_asm(status, `sub sp, sp, #${stack_placeholder}\n`);
|
|
21274
21346
|
emit_asm(status, `mov x29, sp\n`);
|
|
21275
|
-
emit_field_destroys(status, node, "self", void 0, false,
|
|
21347
|
+
emit_field_destroys(status, node, "self", void 0, false, true);
|
|
21276
21348
|
emit_asm(status, `${return_label}:\n`);
|
|
21277
21349
|
const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
|
|
21278
21350
|
status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
|
|
@@ -22172,7 +22244,10 @@ function build_struct_functions$1(node, status) {
|
|
|
22172
22244
|
const moved_param_save_slots = /* @__PURE__ */ new Map();
|
|
22173
22245
|
for (const param of func.params) {
|
|
22174
22246
|
if (!param.is_moved || param.is_self_param) continue;
|
|
22175
|
-
|
|
22247
|
+
const is_class = !!status.structs.find((s) => s.name === param.type.name && s.is_class);
|
|
22248
|
+
const vstruct = !is_class ? resolve_struct_type(param.type, status) : void 0;
|
|
22249
|
+
const is_owning_value_struct = !!vstruct && !vstruct.is_class && !param.type.is_nullable && struct_needs_destroy(vstruct, status);
|
|
22250
|
+
if (!is_class && !is_owning_value_struct) continue;
|
|
22176
22251
|
const reg = status.function_param_regs.get(param.name);
|
|
22177
22252
|
if (reg) {
|
|
22178
22253
|
const save_offset = allocate_stack_space(status, 8);
|
|
@@ -22181,7 +22256,8 @@ function build_struct_functions$1(node, status) {
|
|
|
22181
22256
|
offset: save_offset,
|
|
22182
22257
|
type_name: param.type.name,
|
|
22183
22258
|
type_args: param.type.type_args,
|
|
22184
|
-
is_nullable: param.type.is_nullable
|
|
22259
|
+
is_nullable: param.type.is_nullable,
|
|
22260
|
+
value_struct_destroy_name: is_owning_value_struct ? vstruct.name : void 0
|
|
22185
22261
|
});
|
|
22186
22262
|
} else {
|
|
22187
22263
|
const offset = status.stack_offsets.get(param.name);
|
|
@@ -22189,7 +22265,8 @@ function build_struct_functions$1(node, status) {
|
|
|
22189
22265
|
offset,
|
|
22190
22266
|
type_name: param.type.name,
|
|
22191
22267
|
type_args: param.type.type_args,
|
|
22192
|
-
is_nullable: param.type.is_nullable
|
|
22268
|
+
is_nullable: param.type.is_nullable,
|
|
22269
|
+
value_struct_destroy_name: is_owning_value_struct ? vstruct.name : void 0
|
|
22193
22270
|
});
|
|
22194
22271
|
}
|
|
22195
22272
|
}
|
|
@@ -22244,6 +22321,16 @@ function build_struct_functions$1(node, status) {
|
|
|
22244
22321
|
emit_asm(status, `cmp x0, x1\n`);
|
|
22245
22322
|
emit_asm(status, `beq ${keep_prefix}_${name}\n`);
|
|
22246
22323
|
}
|
|
22324
|
+
if (info.value_struct_destroy_name) {
|
|
22325
|
+
const vstruct = status.structs.find((s) => s.name === info.value_struct_destroy_name);
|
|
22326
|
+
release_heap_string_fields(status, name, info.value_struct_destroy_name);
|
|
22327
|
+
if (vstruct && has_destroy(vstruct)) {
|
|
22328
|
+
emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
|
|
22329
|
+
emit_asm(status, `bl ${info.value_struct_destroy_name}_destroy\n`);
|
|
22330
|
+
} else if (vstruct) emit_field_destroys(status, vstruct, name, void 0, void 0, false);
|
|
22331
|
+
if (ret_is_class) emit_asm(status, `${keep_prefix}_${name}:\n`);
|
|
22332
|
+
continue;
|
|
22333
|
+
}
|
|
22247
22334
|
emit_destroy_for_anchor_slot(status, info.offset, info.type_name, info.type_args, info.is_nullable);
|
|
22248
22335
|
emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
|
|
22249
22336
|
emit_free(status);
|
|
@@ -22689,6 +22776,7 @@ function build_block_node$1(node, status) {
|
|
|
22689
22776
|
const prologue = status.module_init_statements;
|
|
22690
22777
|
status.module_init_statements = void 0;
|
|
22691
22778
|
for (const stmt of prologue) {
|
|
22779
|
+
status.last_result_is_heap = false;
|
|
22692
22780
|
emit_allocations$1(stmt, status);
|
|
22693
22781
|
emit_stmt_from_nir$1(stmt, prologue.indexOf(stmt), prologue, status);
|
|
22694
22782
|
}
|
|
@@ -22698,6 +22786,7 @@ function build_block_node$1(node, status) {
|
|
|
22698
22786
|
if (module_init_set.has(child)) continue;
|
|
22699
22787
|
if (!is_trait_node(child) && !is_struct_node(child) && !is_function_node(child) && child.node_type !== "enum" && child.node_type !== "bitset" && !(child.node_type === "declare" && inlined_const_names.has(child.name))) {
|
|
22700
22788
|
if (child.node_type !== "while") emit_allocations$1(child, status);
|
|
22789
|
+
status.last_result_is_heap = false;
|
|
22701
22790
|
const prev_forwarded = status.forwarded_param_inits;
|
|
22702
22791
|
const consumed = emit_stmt_from_nir$1(child, index, node.statements, status);
|
|
22703
22792
|
index += consumed - 1;
|
|
@@ -23874,7 +23963,6 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
23874
23963
|
}
|
|
23875
23964
|
hoist_field_overrides(node.value, build_node, status, "", node.name);
|
|
23876
23965
|
status.last_result_is_heap = false;
|
|
23877
|
-
const prev_heap = status.last_result_is_heap;
|
|
23878
23966
|
if (node.value?.node_type === "value") {
|
|
23879
23967
|
const inlined = status.top_level_consts?.get(node.value.value);
|
|
23880
23968
|
if (inlined?.value) node.value = fold_string_const(inlined, status.top_level_consts) ?? inlined.value;
|
|
@@ -23896,7 +23984,6 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
23896
23984
|
consolidate_temp_anchors$1(status, node.value, node.type.name);
|
|
23897
23985
|
}
|
|
23898
23986
|
}
|
|
23899
|
-
status.last_result_is_heap = prev_heap;
|
|
23900
23987
|
}
|
|
23901
23988
|
if (node.func_params) {
|
|
23902
23989
|
if (status.function_return_label) {
|
|
@@ -24784,8 +24871,12 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
24784
24871
|
emit_init_value$1(node.value, nir_init, status);
|
|
24785
24872
|
ensure_newline(status);
|
|
24786
24873
|
status.int_dest_hint = void 0;
|
|
24787
|
-
if (size === 16 && node.type.name === "string")
|
|
24788
|
-
|
|
24874
|
+
if (size === 16 && node.type.name === "string") {
|
|
24875
|
+
const force_heap_init = (status.force_heap_strings?.has(node.name) ?? false) && raw !== "null";
|
|
24876
|
+
if (force_heap_init) emit_strdup_string(status);
|
|
24877
|
+
emit_pair_store_x29(status, offset);
|
|
24878
|
+
if (force_heap_init) mark_heap_string(status, node.name);
|
|
24879
|
+
} else emit_var_store(status, "x0", node.name, size);
|
|
24789
24880
|
} else if (is_float_type$1(node.type.name)) {
|
|
24790
24881
|
const label = `_float_const_${decl_const_counter++}`;
|
|
24791
24882
|
emit_data(status, `${label}: .double ${raw}\n.p2align 2\n`);
|
|
@@ -24988,6 +25079,17 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
24988
25079
|
check_heap();
|
|
24989
25080
|
return;
|
|
24990
25081
|
}
|
|
25082
|
+
const decl_string_field_read = node.type.name === "string" && !node.type.is_view && !node.type.is_array && size === 16 && node.value.node_type === "access" && node.value.access.node_type === "access_field";
|
|
25083
|
+
if (decl_string_field_read && !!status.force_heap_strings?.has(node.name)) {
|
|
25084
|
+
emit_init_value$1(node.value, nir_init, status);
|
|
25085
|
+
ensure_newline(status);
|
|
25086
|
+
emit_strdup_string(status);
|
|
25087
|
+
emit_var_store(status, "x0", node.name, size);
|
|
25088
|
+
mark_heap_string(status, node.name);
|
|
25089
|
+
status.last_result_is_heap = false;
|
|
25090
|
+
return;
|
|
25091
|
+
}
|
|
25092
|
+
if (decl_string_field_read) status.last_result_is_heap = false;
|
|
24991
25093
|
emit_init_value$1(node.value, nir_init, status);
|
|
24992
25094
|
emit_var_store(status, "x0", node.name, size);
|
|
24993
25095
|
check_heap();
|
|
@@ -31044,6 +31146,10 @@ function build_auto_free(status) {
|
|
|
31044
31146
|
function free_scoped_declarations(status, decls, persist_string_field_records = false) {
|
|
31045
31147
|
let commented = false;
|
|
31046
31148
|
if (status.heap_string_fields?.size) for (const dec of decls) {
|
|
31149
|
+
if (dec.moved_value_struct_param) {
|
|
31150
|
+
const dec_struct = status.structs.find((s) => s.name === mono_type_name(dec.type) && !s.is_simple_type && !s.is_generic);
|
|
31151
|
+
if (dec_struct && has_destroy(dec_struct)) continue;
|
|
31152
|
+
}
|
|
31047
31153
|
const prefix = `${dec.name}.`;
|
|
31048
31154
|
const keys = Array.from(status.heap_string_fields).filter((key) => key.startsWith(prefix));
|
|
31049
31155
|
if (!keys.length) continue;
|
|
@@ -31075,6 +31181,18 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
|
|
|
31075
31181
|
}
|
|
31076
31182
|
if (persist_string_field_records && dec.force_owned_string === true) continue;
|
|
31077
31183
|
if (status.moved?.has(dec.name)) continue;
|
|
31184
|
+
if (dec.moved_value_struct_param) {
|
|
31185
|
+
const mono_name = mono_type_name(dec.type);
|
|
31186
|
+
const param_struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type && !s.is_generic);
|
|
31187
|
+
if (param_struct && struct_needs_destroy(param_struct, status)) {
|
|
31188
|
+
if (!commented) {
|
|
31189
|
+
status.code += "\n// Auto-free\n";
|
|
31190
|
+
commented = true;
|
|
31191
|
+
}
|
|
31192
|
+
emit_struct_destroys(status, param_struct, `(*${c_function_name(dec.name)})`);
|
|
31193
|
+
}
|
|
31194
|
+
continue;
|
|
31195
|
+
}
|
|
31078
31196
|
const cname = c_function_name(dec.name);
|
|
31079
31197
|
if (dec.type.name === "func") {
|
|
31080
31198
|
if (!commented) {
|
|
@@ -33565,13 +33683,15 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
|
|
|
33565
33683
|
}
|
|
33566
33684
|
}
|
|
33567
33685
|
}
|
|
33568
|
-
|
|
33686
|
+
const assign_rhs_is_moved_value = node.right_value.node_type === "value" && node.right_value.is_moved;
|
|
33687
|
+
const assign_rhs_is_fresh_call = !node.swap && node.right_value.node_type === "func_call";
|
|
33688
|
+
if (!node.operator && node.left_value.node_type === "access" && node.left_value.access.node_type === "access_field" && (assign_rhs_is_moved_value || assign_rhs_is_fresh_call)) {
|
|
33569
33689
|
const access_lhs = node.left_value;
|
|
33570
33690
|
const field_type = access_lhs.access.type;
|
|
33571
33691
|
const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
|
|
33572
33692
|
const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
|
|
33573
33693
|
if (field_struct) {
|
|
33574
|
-
splice_decl_from_c_scopes(status, node.right_value.value);
|
|
33694
|
+
if (assign_rhs_is_moved_value) splice_decl_from_c_scopes(status, node.right_value.value);
|
|
33575
33695
|
const saved_len = begin_code_scratch(status);
|
|
33576
33696
|
build_node$1(node.left_value, status);
|
|
33577
33697
|
const field_access = end_code_scratch(status, saved_len);
|
|
@@ -36790,9 +36910,16 @@ function build_struct_functions(node, status, skip_init = false) {
|
|
|
36790
36910
|
for (const param of func.params) {
|
|
36791
36911
|
if (param.is_self_param) continue;
|
|
36792
36912
|
const param_struct = status.structs.find((s) => s.name === param.type.name);
|
|
36793
|
-
if (param.is_moved
|
|
36913
|
+
if (!param.is_moved) continue;
|
|
36914
|
+
if (param_struct?.is_class && !moved_param_is_consumed(func, param.name, param_struct.name, status.structs)) {
|
|
36794
36915
|
const pname = c_function_name(param.name);
|
|
36795
36916
|
status.scoped_declarations.push(new DeclarationNode(param.start, "private", "move", pname, param.type));
|
|
36917
|
+
} else if (param_struct && !param_struct.is_class && !param_struct.is_simple_type && !param_struct.is_generic && !param.type.is_nullable && struct_needs_destroy(param_struct, status) && !moved_param_is_consumed(func, param.name, param_struct.name, status.structs)) {
|
|
36918
|
+
const pname = c_function_name(param.name);
|
|
36919
|
+
const decl = new DeclarationNode(param.start, "private", "move", pname, param.type);
|
|
36920
|
+
decl.moved_value_struct_param = true;
|
|
36921
|
+
decl.string_fields_via_pointer = true;
|
|
36922
|
+
status.scoped_declarations.push(decl);
|
|
36796
36923
|
}
|
|
36797
36924
|
}
|
|
36798
36925
|
const saved_sig = begin_code_scratch(status);
|
|
@@ -37679,6 +37806,12 @@ function build_function_node(node, status) {
|
|
|
37679
37806
|
if (param_trait && !param_struct?.is_class) decl.trait_class_trait = param.type.name;
|
|
37680
37807
|
status.scoped_declarations.push(decl);
|
|
37681
37808
|
}
|
|
37809
|
+
if (param.is_moved && !moved_owning_param && param_struct && !param_struct.is_class && !param_struct.is_generic && !param.type.is_nullable && node.name !== "main" && struct_needs_destroy(param_struct, status) && !moved_param_is_consumed(node, param.name, param_struct.name, status.structs)) {
|
|
37810
|
+
const decl = new DeclarationNode(param.start, "private", "move", pname, param.type);
|
|
37811
|
+
decl.moved_value_struct_param = true;
|
|
37812
|
+
decl.string_fields_via_pointer = true;
|
|
37813
|
+
status.scoped_declarations.push(decl);
|
|
37814
|
+
}
|
|
37682
37815
|
const is_free_function = (!node.scope || node.scope.node_type === "root" || node.scope.node_type === "func") && !node.params.some((p) => p.is_self_param);
|
|
37683
37816
|
const fn_name = node.name;
|
|
37684
37817
|
const fn_label = emission_label(node);
|
|
@@ -41684,7 +41817,11 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
|
|
|
41684
41817
|
const rhs_is_string_var = !node.swap && !!rhs_value && typeof rhs_value.value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rhs_value.value) && rhs_value.value !== "true" && rhs_value.value !== "false" && rhs_value.value !== "null" && !rhs_value.is_moved && size === 16 && rhs_value.type?.name === "string" && !rhs_value.type?.is_view;
|
|
41685
41818
|
const move_source = rhs_is_string_var && node.last_use_move && move_on_last_use_enabled() ? rhs_value.value : void 0;
|
|
41686
41819
|
const explicit_move_source = !node.swap && !!rhs_value && rhs_value.is_moved && size === 16 && rhs_value.type?.name === "string" && !rhs_value.type?.is_view ? rhs_value.value : void 0;
|
|
41687
|
-
const
|
|
41820
|
+
const rhs_is_borrow_reception = !node.swap && size === 16 && is_string_borrow(node.right_value);
|
|
41821
|
+
const rhs_is_field_read = !node.swap && size === 16 && node.right_value.node_type === "access" && node.right_value.access.node_type === "access_field";
|
|
41822
|
+
const rhs_var_name = !node.swap && size === 16 && node.right_value.node_type === "value" && !node.right_value.type?.is_view ? node.right_value.value : void 0;
|
|
41823
|
+
const rhs_is_unrecorded_var_read = rhs_var_name !== void 0 && !status.heap_strings?.has(rhs_var_name);
|
|
41824
|
+
const borrow_needs_dup = (rhs_is_borrow_reception || rhs_is_field_read || rhs_is_unrecorded_var_read) && !!status.force_heap_strings?.has(name);
|
|
41688
41825
|
const move_source_owns_heap = move_source !== void 0 && !!status.heap_strings?.has(move_source);
|
|
41689
41826
|
if (rhs_is_view_into_owned) {
|
|
41690
41827
|
if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
|
|
@@ -41694,7 +41831,8 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
|
|
|
41694
41831
|
if (move_source !== void 0 && move_source_owns_heap) status.heap_strings?.delete(move_source);
|
|
41695
41832
|
if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
|
|
41696
41833
|
status.heap_strings.add(name);
|
|
41697
|
-
} else if (status.last_result_is_heap
|
|
41834
|
+
} else if (rhs_is_field_read || rhs_is_unrecorded_var_read) status.last_result_is_heap = false;
|
|
41835
|
+
else if (status.last_result_is_heap) {
|
|
41698
41836
|
if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
|
|
41699
41837
|
status.heap_strings.add(name);
|
|
41700
41838
|
} else if (explicit_move_source !== void 0 && status.heap_strings?.delete(explicit_move_source)) status.heap_strings.add(name);
|
|
@@ -41911,6 +42049,24 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
|
|
|
41911
42049
|
get_base_address(access, status, "x0");
|
|
41912
42050
|
emit_asm(status, `str x0, [sp, #-16]!\n`);
|
|
41913
42051
|
}
|
|
42052
|
+
const field_rhs_is_moved_value = node.right_value.node_type === "value" && node.right_value.is_moved;
|
|
42053
|
+
const field_rhs_is_fresh_call = !node.swap && node.right_value.node_type === "func_call";
|
|
42054
|
+
if (!node.operator && (field_rhs_is_moved_value || field_rhs_is_fresh_call)) {
|
|
42055
|
+
const field_resolved = resolve_struct_type(field_type, status);
|
|
42056
|
+
const field_target_var = access.target.node_type === "value" ? access.target.value : "";
|
|
42057
|
+
const first_init_write = is_first_init_field_write(field_target_var, field_name, status);
|
|
42058
|
+
mark_init_field_write(field_target_var, field_name, status);
|
|
42059
|
+
if (field_resolved && !field_resolved.is_class && !first_init_write && struct_needs_auto_destroy(field_resolved, status)) {
|
|
42060
|
+
if (base_reg !== void 0) {
|
|
42061
|
+
if (offset !== 0) emit_asm(status, `add x0, ${base_reg}, #${offset}\n`);
|
|
42062
|
+
else emit_asm(status, `mov x0, ${base_reg}\n`);
|
|
42063
|
+
} else {
|
|
42064
|
+
emit_asm(status, `ldr x0, [sp]\n`);
|
|
42065
|
+
if (offset !== 0) emit_asm(status, `add x0, x0, #${offset}\n`);
|
|
42066
|
+
}
|
|
42067
|
+
emit_asm(status, `bl ${field_resolved.name}_destroy\n`);
|
|
42068
|
+
}
|
|
42069
|
+
}
|
|
41914
42070
|
get_source_address(node.right_value, status, nir_rhs);
|
|
41915
42071
|
ensure_newline(status);
|
|
41916
42072
|
emit_asm(status, `mov x1, x0\n`);
|
|
@@ -47312,7 +47468,8 @@ function build(root, options = {}) {
|
|
|
47312
47468
|
status.code = status.code.replaceAll("bl _strdup\n", "bl _nomen_strdup_wrap\n");
|
|
47313
47469
|
status.code = status.code.replaceAll("bl _free\n", "bl _nomen_free_wrap\n");
|
|
47314
47470
|
}
|
|
47315
|
-
const
|
|
47471
|
+
const { result: lifted } = lift_functions(status.code);
|
|
47472
|
+
const lift_errors = lifted.errors;
|
|
47316
47473
|
if (lift_errors.length > 0) {
|
|
47317
47474
|
if (!status.build_errors) status.build_errors = [];
|
|
47318
47475
|
for (const e of lift_errors.slice(0, 20)) status.build_errors.push({
|
|
@@ -47320,7 +47477,7 @@ function build(root, options = {}) {
|
|
|
47320
47477
|
start: 0
|
|
47321
47478
|
});
|
|
47322
47479
|
}
|
|
47323
|
-
const balance_errors =
|
|
47480
|
+
const balance_errors = stack_balance_errors_from_lift(lifted);
|
|
47324
47481
|
if (balance_errors.length > 0) {
|
|
47325
47482
|
if (!status.build_errors) status.build_errors = [];
|
|
47326
47483
|
for (const e of balance_errors.slice(0, 20)) status.build_errors.push({
|
|
@@ -63125,11 +63282,20 @@ function generate_harness(tests, benches) {
|
|
|
63125
63282
|
const n = Math.min(raw, 4096);
|
|
63126
63283
|
return template.replace(/__NAME__/g, b.name).replace(/__TARGET__/g, b.target).replace(/__N__/g, String(n));
|
|
63127
63284
|
}).join("\n");
|
|
63128
|
-
let main = "\nimport System\nimport System::Test\n
|
|
63285
|
+
let main = "\nimport System\nimport System::Test\n";
|
|
63286
|
+
main += "\npub func main = () {\n";
|
|
63287
|
+
main += `\tConsole.unbuffered()\n`;
|
|
63129
63288
|
main += " var Tester t = Tester()\n";
|
|
63289
|
+
main += " var int64 audit_mark = 0\n";
|
|
63290
|
+
main += " var int64 audit_leaked = 0\n";
|
|
63130
63291
|
for (const test of tests) {
|
|
63292
|
+
main += `\taudit_mark = Tester.audit_count()\n`;
|
|
63131
63293
|
main += `\tt.begin_test("${escape_nm_string(test.name)}")\n`;
|
|
63132
63294
|
main += `\t${test.name}(ref t)\n`;
|
|
63295
|
+
main += `\taudit_leaked = Tester.audit_count() - audit_mark\n`;
|
|
63296
|
+
main += `\tif audit_leaked > 0 {\n`;
|
|
63297
|
+
main += `\t\tConsole.write_line("\\\\nomen|leaks|" + "${escape_nm_string(test.name)}" + "|\\{audit_leaked}")\n`;
|
|
63298
|
+
main += `\t}\n`;
|
|
63133
63299
|
main += `\tt.end_test()\n`;
|
|
63134
63300
|
}
|
|
63135
63301
|
for (const bench of benches) {
|
|
@@ -63148,6 +63314,7 @@ function parse_records(stdout) {
|
|
|
63148
63314
|
const tests = [];
|
|
63149
63315
|
const fails = [];
|
|
63150
63316
|
const benches = [];
|
|
63317
|
+
const leaks = [];
|
|
63151
63318
|
const other = [];
|
|
63152
63319
|
for (const line of stdout.split("\n")) {
|
|
63153
63320
|
if (!line.startsWith(RECORD_PREFIX)) {
|
|
@@ -63166,6 +63333,10 @@ function parse_records(stdout) {
|
|
|
63166
63333
|
test: parts[1],
|
|
63167
63334
|
message: parts.slice(2).join("|")
|
|
63168
63335
|
});
|
|
63336
|
+
else if (kind === "leaks") leaks.push({
|
|
63337
|
+
test: parts[1],
|
|
63338
|
+
count: parseInt(parts[2] || "0", 10)
|
|
63339
|
+
});
|
|
63169
63340
|
else if (kind === "bench") benches.push({
|
|
63170
63341
|
label: parts[1],
|
|
63171
63342
|
n: parseInt(parts[2] || "0", 10),
|
|
@@ -63180,12 +63351,19 @@ function parse_records(stdout) {
|
|
|
63180
63351
|
tests,
|
|
63181
63352
|
fails,
|
|
63182
63353
|
benches,
|
|
63354
|
+
leaks,
|
|
63183
63355
|
other
|
|
63184
63356
|
};
|
|
63185
63357
|
}
|
|
63186
|
-
/**
|
|
63358
|
+
/**
|
|
63359
|
+
* Pull the exit-time audit lines out of stdout: "LEAK: N allocation(s)" (a
|
|
63360
|
+
* nonzero malloc/free balance) and "AUDIT-STALE-FREE: N pointer(s) freed that
|
|
63361
|
+
* were not live audit allocations" (a free that matched no live block — a
|
|
63362
|
+
* foreign pointer or a double free; the runtime no longer passes those to
|
|
63363
|
+
* libc free, so this is the only record they leave). Both fail the file.
|
|
63364
|
+
*/
|
|
63187
63365
|
function extract_leaks(stdout) {
|
|
63188
|
-
return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:"));
|
|
63366
|
+
return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:") || l.startsWith("AUDIT-STALE-FREE:"));
|
|
63189
63367
|
}
|
|
63190
63368
|
/**
|
|
63191
63369
|
* Compile one `*.test.nm` file (with the generated harness) and run it,
|
|
@@ -63201,8 +63379,9 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
|
|
|
63201
63379
|
tests: [],
|
|
63202
63380
|
fails: [],
|
|
63203
63381
|
benches: [],
|
|
63204
|
-
other: [],
|
|
63205
63382
|
leaks: [],
|
|
63383
|
+
leaking_tests: [],
|
|
63384
|
+
other: [],
|
|
63206
63385
|
ms: 0
|
|
63207
63386
|
};
|
|
63208
63387
|
const resolved = path.resolve(entry_path);
|
|
@@ -63230,30 +63409,30 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
|
|
|
63230
63409
|
}
|
|
63231
63410
|
const buildDir = build_dir_for(resolved, true);
|
|
63232
63411
|
if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
|
|
63233
|
-
|
|
63234
|
-
if (
|
|
63235
|
-
|
|
63236
|
-
|
|
63237
|
-
|
|
63238
|
-
|
|
63239
|
-
|
|
63240
|
-
result.ms = performance.now() - start;
|
|
63241
|
-
return result;
|
|
63242
|
-
}
|
|
63243
|
-
audit_obj = path.join(buildDir, "audit_runtime.o");
|
|
63244
|
-
await execFile_async("clang", [
|
|
63245
|
-
"-c",
|
|
63246
|
-
runtime_src,
|
|
63247
|
-
"-o",
|
|
63248
|
-
audit_obj
|
|
63249
|
-
]);
|
|
63412
|
+
const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime();
|
|
63413
|
+
if (!runtime_src || !fs.existsSync(runtime_src)) {
|
|
63414
|
+
result.ok = false;
|
|
63415
|
+
result.phase = "setup";
|
|
63416
|
+
result.crashed = "audit_runtime.c was not found. Pass --audit-runtime <path>.";
|
|
63417
|
+
result.ms = performance.now() - start;
|
|
63418
|
+
return result;
|
|
63250
63419
|
}
|
|
63420
|
+
const audit_obj = path.join(buildDir, path.basename(entry_path, ".nm") + "_audit.o");
|
|
63421
|
+
await execFile_async("clang", [
|
|
63422
|
+
"-c",
|
|
63423
|
+
runtime_src,
|
|
63424
|
+
"-o",
|
|
63425
|
+
audit_obj
|
|
63426
|
+
]);
|
|
63251
63427
|
const platform = default_platform();
|
|
63252
63428
|
const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
|
|
63253
63429
|
const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
|
|
63254
63430
|
const outfile = path.join(buildDir, path.basename(entry_path, ".nm"));
|
|
63255
|
-
|
|
63256
|
-
fs.writeFileSync(
|
|
63431
|
+
const header_name = path.basename(entry_path, ".nm") + ".main.h";
|
|
63432
|
+
fs.writeFileSync(path.join(buildDir, header_name), buildResult.headers ?? "");
|
|
63433
|
+
let code_text = buildResult.code;
|
|
63434
|
+
if (arch !== "aarch64") code_text = code_text.replace("#include \"main.h\"", `#include "${header_name}"`);
|
|
63435
|
+
fs.writeFileSync(codefile, code_text);
|
|
63257
63436
|
let companionfile;
|
|
63258
63437
|
if (buildResult.companion) {
|
|
63259
63438
|
const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
|
|
@@ -63279,27 +63458,56 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
|
|
|
63279
63458
|
result.ms = performance.now() - start;
|
|
63280
63459
|
return result;
|
|
63281
63460
|
}
|
|
63461
|
+
const run_out = outfile + ".out";
|
|
63462
|
+
const run_err = outfile + ".err";
|
|
63282
63463
|
let runStdout = "";
|
|
63464
|
+
let runStderr = "";
|
|
63283
63465
|
let crashed;
|
|
63466
|
+
const out_fd = fs.openSync(run_out, "w");
|
|
63467
|
+
const err_fd = fs.openSync(run_err, "w");
|
|
63284
63468
|
try {
|
|
63285
|
-
|
|
63286
|
-
encoding: "utf8",
|
|
63469
|
+
const child = spawn(outfile, [], {
|
|
63287
63470
|
timeout: 3e4,
|
|
63288
|
-
|
|
63471
|
+
stdio: [
|
|
63472
|
+
"ignore",
|
|
63473
|
+
out_fd,
|
|
63474
|
+
err_fd
|
|
63475
|
+
],
|
|
63289
63476
|
cwd: cwd ?? process.cwd()
|
|
63290
|
-
})
|
|
63291
|
-
|
|
63292
|
-
|
|
63293
|
-
|
|
63294
|
-
|
|
63477
|
+
});
|
|
63478
|
+
const run = await new Promise((resolve) => {
|
|
63479
|
+
child.on("error", () => resolve({
|
|
63480
|
+
code: null,
|
|
63481
|
+
signal: null
|
|
63482
|
+
}));
|
|
63483
|
+
child.on("close", (code, signal) => resolve({
|
|
63484
|
+
code,
|
|
63485
|
+
signal
|
|
63486
|
+
}));
|
|
63487
|
+
});
|
|
63488
|
+
if (run.signal === "SIGTERM") crashed = "test binary timed out after 30s";
|
|
63489
|
+
else if (run.signal !== null) crashed = `test binary exited abnormally (signal ${run.signal})`;
|
|
63490
|
+
else if (run.code === null) crashed = "test binary could not be run";
|
|
63491
|
+
else if (run.code !== 0) crashed = `test binary exited abnormally (signal ${run.code})`;
|
|
63492
|
+
} finally {
|
|
63493
|
+
fs.closeSync(out_fd);
|
|
63494
|
+
fs.closeSync(err_fd);
|
|
63295
63495
|
}
|
|
63496
|
+
runStdout = fs.readFileSync(run_out, "utf8");
|
|
63497
|
+
try {
|
|
63498
|
+
runStderr = fs.readFileSync(run_err, "utf8");
|
|
63499
|
+
} catch {
|
|
63500
|
+
runStderr = "";
|
|
63501
|
+
}
|
|
63502
|
+
if (crashed && runStderr.trim()) crashed += `\n${runStderr.trim()}`;
|
|
63296
63503
|
const records = parse_records(runStdout);
|
|
63297
63504
|
result.tests = records.tests;
|
|
63298
63505
|
result.fails = records.fails;
|
|
63299
63506
|
result.benches = records.benches;
|
|
63507
|
+
result.leaking_tests = records.leaks;
|
|
63300
63508
|
result.other = records.other;
|
|
63301
63509
|
result.leaks = audit ? extract_leaks(runStdout) : [];
|
|
63302
|
-
if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:"));
|
|
63510
|
+
if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:") && !l.trim().startsWith("AUDIT-STALE-FREE:"));
|
|
63303
63511
|
result.crashed = crashed;
|
|
63304
63512
|
if (crashed) result.phase = "run";
|
|
63305
63513
|
result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0 && result.leaks.length === 0;
|
|
@@ -63340,6 +63548,10 @@ function report_file(result) {
|
|
|
63340
63548
|
if (result.crashed) console.log(C.red(` ${result.crashed}`));
|
|
63341
63549
|
for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
|
|
63342
63550
|
for (const leak of result.leaks) console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
|
|
63551
|
+
if (result.leaks.length) {
|
|
63552
|
+
for (const lt of result.leaking_tests) console.log(` ${C.yellow("⚠")} ${lt.test} ${C.dim(`leaked ${lt.count} allocation(s)`)}`);
|
|
63553
|
+
if (!result.leaking_tests.length) console.log(` ${C.yellow("⚠")} ${C.yellow("LEAK: no per-test attribution — leaked outside a test (harness/setup/teardown) or crashed mid-test")}`);
|
|
63554
|
+
}
|
|
63343
63555
|
for (const b of result.benches) console.log(` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`);
|
|
63344
63556
|
if (result.other.length) {
|
|
63345
63557
|
console.log(C.dim(" --- test stdout ---"));
|
|
@@ -63395,11 +63607,12 @@ async function runTests(root, options = {}) {
|
|
|
63395
63607
|
const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
|
|
63396
63608
|
const totalPassed = totalTests - totalFailed;
|
|
63397
63609
|
const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
|
|
63610
|
+
const totalLeakingTests = results.reduce((a, r) => a + r.leaking_tests.length, 0);
|
|
63398
63611
|
const anyFailed = results.some((r) => !r.ok);
|
|
63399
63612
|
console.log("");
|
|
63400
63613
|
console.log(` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` + (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") + C.dim(` (${totalFiles})`));
|
|
63401
63614
|
console.log(` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` + (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") + C.dim(` (${totalTests})`));
|
|
63402
|
-
if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${
|
|
63615
|
+
if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${totalLeakingTests} leaking test(s)`)}` + (totalLeakingTests !== totalLeaks ? C.dim(` (${totalLeaks} leaked file(s))`) : "") + C.dim(" (--audit; leaking tests fail their file)"));
|
|
63403
63616
|
console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
|
|
63404
63617
|
console.log("");
|
|
63405
63618
|
return !anyFailed;
|
|
@@ -63681,7 +63894,7 @@ function processFile(filename, config, mode, program_args) {
|
|
|
63681
63894
|
console.log(`Created ${codefile} in ${(compileTime - startTime).toFixed(2)}ms`);
|
|
63682
63895
|
console.log("");
|
|
63683
63896
|
startTime = performance.now();
|
|
63684
|
-
const audit_obj = config.audit ? compile_audit_runtime(config, resolved, buildDir) : void 0;
|
|
63897
|
+
const audit_obj = config.audit || result.code.includes("nomen_audit_count") ? compile_audit_runtime(config, resolved, buildDir) : void 0;
|
|
63685
63898
|
let link_inputs = codefile;
|
|
63686
63899
|
if (companionfile) link_inputs += ` ${companionfile}`;
|
|
63687
63900
|
if (audit_obj) link_inputs += ` ${audit_obj}`;
|