nomen-lang 0.6.8 → 0.6.10
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 +188 -0
- package/core/System/StringBuilder.nm +10 -1
- package/core/System/Test.nm +18 -0
- package/dist/index.mjs +354 -191
- package/package.json +3 -2
- package/src/args.ts +7 -2
- package/src/index.ts +15 -4
- package/src/init.ts +10 -0
- package/src/test.ts +155 -55
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import { execFile, execFileSync, execSync } 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";
|
|
6
6
|
import { EventEmitter } from "node:events";
|
|
7
7
|
import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises";
|
|
8
8
|
import { Readable } from "node:stream";
|
|
9
|
-
import { type } from "node:os";
|
|
9
|
+
import os, { type } from "node:os";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { promisify } from "node:util";
|
|
11
12
|
//#region ../node_modules/.pnpm/readdirp@5.1.1/node_modules/readdirp/index.js
|
|
12
13
|
const EntryTypes = {
|
|
13
14
|
FILE_TYPE: "files",
|
|
@@ -2121,8 +2122,40 @@ function try_parse_operands(parts) {
|
|
|
2121
2122
|
* is not an instruction — label, directive, comment, blank, OR an
|
|
2122
2123
|
* instruction outside the contract table). Shared by the validator and the
|
|
2123
2124
|
* phase-2 optimizer so both see identical shapes.
|
|
2124
|
-
|
|
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
|
+
}
|
|
2125
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) {
|
|
2126
2159
|
const t = strip_comment$1(text).trim();
|
|
2127
2160
|
const m = /^([a-z][a-z0-9.]*)\s*(.*)$/.exec(t);
|
|
2128
2161
|
if (!m) return null;
|
|
@@ -2406,34 +2439,13 @@ function validate_asm(code) {
|
|
|
2406
2439
|
}
|
|
2407
2440
|
return errors;
|
|
2408
2441
|
}
|
|
2409
|
-
/**
|
|
2410
|
-
*
|
|
2411
|
-
*
|
|
2412
|
-
*
|
|
2413
|
-
|
|
2414
|
-
* the lifted function bodies:
|
|
2415
|
-
*
|
|
2416
|
-
* - Block splitting: labels and branch/terminator instructions bound blocks,
|
|
2417
|
-
* exactly like the CFG the emitters produce.
|
|
2418
|
-
* - Per-instruction sp effects: pre-indexed `stp/str [sp, #-N]!` subtracts,
|
|
2419
|
-
* post-indexed `ldp/ldr [sp], #N` adds, `sub/add sp, sp, #imm` adjusts;
|
|
2420
|
-
* any other sp write (`mov sp, xN`, register-scaled adds, `ldr sp, …`)
|
|
2421
|
-
* is UNKNOWN and conservatively poisons downstream deltas.
|
|
2422
|
-
* - Join = require equal deltas or unknown: a label reached by paths with
|
|
2423
|
-
* differing deltas becomes unknown; only a KNOWN non-zero delta at a
|
|
2424
|
-
* `ret` is reported. Unknown never errors — the check is conservative by
|
|
2425
|
-
* construction (no false positives, only silence).
|
|
2426
|
-
* - `bl`/`blr` preserve sp (the callee rebalances under the ABI) and do not
|
|
2427
|
-
* terminate blocks; `br xN` (tail-jump) ends propagation with no check;
|
|
2428
|
-
* `svc` continues (the kernel preserves sp).
|
|
2429
|
-
* - Branch targets resolve within the function, including the numeric local
|
|
2430
|
-
* form raw `#arch` blocks use (`1:` / `b.hs 1f` — forward/backward by
|
|
2431
|
-
* suffix); an unresolvable target makes downstream deltas unknown.
|
|
2432
|
-
*/
|
|
2433
|
-
function validate_stack_balance(code) {
|
|
2434
|
-
const { result } = lift_functions(code);
|
|
2435
|
-
if (!result.ok) return [];
|
|
2442
|
+
/** Stack-balance check over an ALREADY-lifted result — lets the build
|
|
2443
|
+
* pipeline lift+validate once and share the structured functions between
|
|
2444
|
+
* the shape/branch validation and the balance pass (lift_functions runs
|
|
2445
|
+
* validate_asm internally, so re-lifting per check doubled the cost). */
|
|
2446
|
+
function stack_balance_errors_from_lift(result) {
|
|
2436
2447
|
const errors = [];
|
|
2448
|
+
if (!result.ok) return [];
|
|
2437
2449
|
for (const fn of result.functions) check_function_balance(fn, errors);
|
|
2438
2450
|
return errors;
|
|
2439
2451
|
}
|
|
@@ -2561,37 +2573,37 @@ function line_effect(text) {
|
|
|
2561
2573
|
};
|
|
2562
2574
|
}
|
|
2563
2575
|
/** Resolve a branch target token to a body line index, or null when the
|
|
2564
|
-
* target is outside this function
|
|
2565
|
-
*
|
|
2566
|
-
|
|
2576
|
+
* target is outside this function. Backed by the per-function label maps
|
|
2577
|
+
* built once in check_function_balance — a linear rescan of the body per
|
|
2578
|
+
* pending branch used to be the single hottest spot in large builds
|
|
2579
|
+
* (branches × body-length re-stripping + regex). Numeric `Nf`/`Nb` forms
|
|
2580
|
+
* resolve directionally against the (possibly redefined) definitions,
|
|
2581
|
+
* matching the assembler's semantics; a named label resolves to its FIRST
|
|
2582
|
+
* definition (the old body scan returned the first match too). */
|
|
2583
|
+
function resolve_target(token, from, named_labels, numeric_labels) {
|
|
2567
2584
|
const numeric = /^(\d+)([fb])$/.exec(token);
|
|
2568
2585
|
if (numeric) {
|
|
2569
|
-
const
|
|
2586
|
+
const defs = numeric_labels.get(numeric[1]);
|
|
2587
|
+
if (!defs) return null;
|
|
2570
2588
|
if (numeric[2] === "f") {
|
|
2571
|
-
for (let
|
|
2572
|
-
const lm = /^(\d+):/.exec(strip_comment$1(body[i].text).trim());
|
|
2573
|
-
if (lm && lm[1] === n) return i;
|
|
2574
|
-
}
|
|
2589
|
+
for (let k = 0; k < defs.length; k++) if (defs[k] > from) return defs[k];
|
|
2575
2590
|
return null;
|
|
2576
2591
|
}
|
|
2577
|
-
for (let
|
|
2578
|
-
const lm = /^(\d+):/.exec(strip_comment$1(body[i].text).trim());
|
|
2579
|
-
if (lm && lm[1] === n) return i;
|
|
2580
|
-
}
|
|
2592
|
+
for (let k = defs.length - 1; k >= 0; k--) if (defs[k] < from) return defs[k];
|
|
2581
2593
|
return null;
|
|
2582
2594
|
}
|
|
2583
|
-
|
|
2584
|
-
const lm = LABEL_RE.exec(strip_comment$1(body[i].text).trim());
|
|
2585
|
-
if (lm && lm[1] === token) return i;
|
|
2586
|
-
}
|
|
2587
|
-
return null;
|
|
2595
|
+
return named_labels.get(token) ?? null;
|
|
2588
2596
|
}
|
|
2589
2597
|
function check_function_balance(fn, errors) {
|
|
2590
2598
|
const body = fn.body;
|
|
2599
|
+
const stripped_text = [];
|
|
2591
2600
|
const effects = [];
|
|
2592
2601
|
const is_code = [];
|
|
2602
|
+
const named_labels = /* @__PURE__ */ new Map();
|
|
2603
|
+
const numeric_labels = /* @__PURE__ */ new Map();
|
|
2593
2604
|
for (let i = 0; i < body.length; i++) {
|
|
2594
2605
|
const t = strip_comment$1(body[i].text).trim();
|
|
2606
|
+
stripped_text.push(t);
|
|
2595
2607
|
effects.push({
|
|
2596
2608
|
delta: null,
|
|
2597
2609
|
branch: null,
|
|
@@ -2600,8 +2612,15 @@ function check_function_balance(fn, errors) {
|
|
|
2600
2612
|
unknown_jump: false
|
|
2601
2613
|
});
|
|
2602
2614
|
is_code.push(false);
|
|
2603
|
-
if (!t) continue;
|
|
2604
2615
|
const lm = LABEL_RE.exec(t);
|
|
2616
|
+
if (lm) {
|
|
2617
|
+
if (/^\d+$/.test(lm[1])) {
|
|
2618
|
+
const defs = numeric_labels.get(lm[1]);
|
|
2619
|
+
if (defs) defs.push(i);
|
|
2620
|
+
else numeric_labels.set(lm[1], [i]);
|
|
2621
|
+
} else if (!named_labels.has(lm[1])) named_labels.set(lm[1], i);
|
|
2622
|
+
}
|
|
2623
|
+
if (!t) continue;
|
|
2605
2624
|
const rest = lm ? lm[2].trim() : t;
|
|
2606
2625
|
if (rest.startsWith(".") || /^[\w.$]+\s*=\s*[\w.$]+$/.test(rest) || !rest) continue;
|
|
2607
2626
|
is_code[i] = true;
|
|
@@ -2613,7 +2632,7 @@ function check_function_balance(fn, errors) {
|
|
|
2613
2632
|
let cur = -1;
|
|
2614
2633
|
let terminated = true;
|
|
2615
2634
|
for (let i = 0; i < body.length; i++) {
|
|
2616
|
-
const t =
|
|
2635
|
+
const t = stripped_text[i];
|
|
2617
2636
|
if (!t) continue;
|
|
2618
2637
|
if (LABEL_RE.test(t)) {
|
|
2619
2638
|
cur++;
|
|
@@ -2661,7 +2680,7 @@ function check_function_balance(fn, errors) {
|
|
|
2661
2680
|
}
|
|
2662
2681
|
}
|
|
2663
2682
|
for (const p of pending) {
|
|
2664
|
-
const line = resolve_target(p.token, p.from,
|
|
2683
|
+
const line = resolve_target(p.token, p.from, named_labels, numeric_labels);
|
|
2665
2684
|
blocks[p.block].targets.push(line === null ? null : line_block.get(line) ?? null);
|
|
2666
2685
|
}
|
|
2667
2686
|
const preds = Array.from({ length: blocks.length }, () => []);
|
|
@@ -8971,6 +8990,85 @@ function record_ctor_heap_default_fields(var_name, struct_node, status, is_owned
|
|
|
8971
8990
|
}
|
|
8972
8991
|
}
|
|
8973
8992
|
//#endregion
|
|
8993
|
+
//#region ../src/build_common/destroy_analysis.ts
|
|
8994
|
+
/**
|
|
8995
|
+
* Whether any field of the struct is an enum-with-data value owning heap
|
|
8996
|
+
* payloads (a string payload — always heap, case construction strdups it —
|
|
8997
|
+
* or a class/trait reference payload — construction transfers ownership).
|
|
8998
|
+
* Such a field must be reclaimed when the struct dies.
|
|
8999
|
+
*/
|
|
9000
|
+
function struct_has_owning_enum_field(struct, table) {
|
|
9001
|
+
for (const field of struct.fields) {
|
|
9002
|
+
if (field.type.is_ref) continue;
|
|
9003
|
+
const en = table.enums?.find((e) => e.name === field.type.name);
|
|
9004
|
+
if (!en?.has_associated_data) continue;
|
|
9005
|
+
for (const c of en.cases) for (const p of c.params) {
|
|
9006
|
+
if (p.type.name === "string" && !p.type.is_array) return true;
|
|
9007
|
+
if (table.traits?.some((t) => t.name === p.type.name)) return true;
|
|
9008
|
+
if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
|
|
9009
|
+
}
|
|
9010
|
+
}
|
|
9011
|
+
return false;
|
|
9012
|
+
}
|
|
9013
|
+
/**
|
|
9014
|
+
* Whether a struct declares its own `#destroy` (user side effects /
|
|
9015
|
+
* resource release).
|
|
9016
|
+
*/
|
|
9017
|
+
function has_destroy(struct) {
|
|
9018
|
+
return !!struct.functions.find((f) => f.name === "#destroy");
|
|
9019
|
+
}
|
|
9020
|
+
/**
|
|
9021
|
+
* Whether any field (recursively) requires destroy handling — a class
|
|
9022
|
+
* field (heap instance to reclaim) or a nested struct with its own
|
|
9023
|
+
* `#destroy` (or nested fields that do). Deliberately excludes `string`
|
|
9024
|
+
* fields: a struct local whose only owning fields are strings is NOT
|
|
9025
|
+
* auto-destroyed at scope exit (the strings may be raw rodata arg
|
|
9026
|
+
* pointers, not heap) — see `struct_needs_auto_destroy` for the broader
|
|
9027
|
+
* form used to decide whether a `<Struct>_destroy` function is generated.
|
|
9028
|
+
*/
|
|
9029
|
+
function has_struct_fields_with_destroy(struct, table) {
|
|
9030
|
+
for (const field of struct.fields) {
|
|
9031
|
+
if (field.type.is_ref) continue;
|
|
9032
|
+
const field_struct = resolve_struct_type(field.type, table);
|
|
9033
|
+
if (!field_struct) continue;
|
|
9034
|
+
if (field_struct.is_class) return true;
|
|
9035
|
+
if (has_destroy(field_struct)) return true;
|
|
9036
|
+
if (has_struct_fields_with_destroy(field_struct, table)) return true;
|
|
9037
|
+
}
|
|
9038
|
+
return false;
|
|
9039
|
+
}
|
|
9040
|
+
/**
|
|
9041
|
+
* Whether a struct (or any embedded struct field, recursively) needs a
|
|
9042
|
+
* destroy call at scope exit — it has a `#destroy`, a class-typed field,
|
|
9043
|
+
* or a nested struct field that itself needs destroying. This does NOT
|
|
9044
|
+
* count `string` fields (see `has_struct_fields_with_destroy`).
|
|
9045
|
+
*/
|
|
9046
|
+
function struct_needs_destroy(struct, table) {
|
|
9047
|
+
return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
|
|
9048
|
+
}
|
|
9049
|
+
/**
|
|
9050
|
+
* Whether a struct needs an auto-generated `<Struct>_destroy` function.
|
|
9051
|
+
* Broader than `struct_needs_destroy`: also returns true for structs with
|
|
9052
|
+
* `string` fields, because a Buffer<T> for such a struct deep-copies the
|
|
9053
|
+
* strings into slots (strdup on store) and the per-element destroy must
|
|
9054
|
+
* free them. The generated destroy frees each string field; it is called
|
|
9055
|
+
* from Buffer's specialized #destroy / replace_T, NOT from struct local
|
|
9056
|
+
* scope exit (`struct_needs_destroy` governs that path and excludes
|
|
9057
|
+
* strings).
|
|
9058
|
+
*/
|
|
9059
|
+
function struct_needs_auto_destroy(struct, table) {
|
|
9060
|
+
if (struct_needs_destroy(struct, table)) return true;
|
|
9061
|
+
if (struct_has_owning_enum_field(struct, table)) return true;
|
|
9062
|
+
for (const field of struct.fields) {
|
|
9063
|
+
if (field.type.is_ref) continue;
|
|
9064
|
+
if (field.type.is_view) continue;
|
|
9065
|
+
if (field.type.name === "string" && !field.type.is_array) return true;
|
|
9066
|
+
const field_struct = resolve_struct_type(field.type, table);
|
|
9067
|
+
if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
|
|
9068
|
+
}
|
|
9069
|
+
return false;
|
|
9070
|
+
}
|
|
9071
|
+
//#endregion
|
|
8974
9072
|
//#region ../src/nodes/child_nodes.ts
|
|
8975
9073
|
/** Whether `value` is an AST node (carries a string `node_type`). */
|
|
8976
9074
|
function is_ast_node(value) {
|
|
@@ -11647,85 +11745,6 @@ function array_struct_name(type, status) {
|
|
|
11647
11745
|
return status.structs.find((s) => s.name === mono && !s.is_generic) ? mono : void 0;
|
|
11648
11746
|
}
|
|
11649
11747
|
//#endregion
|
|
11650
|
-
//#region ../src/build_common/destroy_analysis.ts
|
|
11651
|
-
/**
|
|
11652
|
-
* Whether any field of the struct is an enum-with-data value owning heap
|
|
11653
|
-
* payloads (a string payload — always heap, case construction strdups it —
|
|
11654
|
-
* or a class/trait reference payload — construction transfers ownership).
|
|
11655
|
-
* Such a field must be reclaimed when the struct dies.
|
|
11656
|
-
*/
|
|
11657
|
-
function struct_has_owning_enum_field(struct, table) {
|
|
11658
|
-
for (const field of struct.fields) {
|
|
11659
|
-
if (field.type.is_ref) continue;
|
|
11660
|
-
const en = table.enums?.find((e) => e.name === field.type.name);
|
|
11661
|
-
if (!en?.has_associated_data) continue;
|
|
11662
|
-
for (const c of en.cases) for (const p of c.params) {
|
|
11663
|
-
if (p.type.name === "string" && !p.type.is_array) return true;
|
|
11664
|
-
if (table.traits?.some((t) => t.name === p.type.name)) return true;
|
|
11665
|
-
if (table.structs.find((s) => s.name === p.type.name)?.is_class) return true;
|
|
11666
|
-
}
|
|
11667
|
-
}
|
|
11668
|
-
return false;
|
|
11669
|
-
}
|
|
11670
|
-
/**
|
|
11671
|
-
* Whether a struct declares its own `#destroy` (user side effects /
|
|
11672
|
-
* resource release).
|
|
11673
|
-
*/
|
|
11674
|
-
function has_destroy(struct) {
|
|
11675
|
-
return !!struct.functions.find((f) => f.name === "#destroy");
|
|
11676
|
-
}
|
|
11677
|
-
/**
|
|
11678
|
-
* Whether any field (recursively) requires destroy handling — a class
|
|
11679
|
-
* field (heap instance to reclaim) or a nested struct with its own
|
|
11680
|
-
* `#destroy` (or nested fields that do). Deliberately excludes `string`
|
|
11681
|
-
* fields: a struct local whose only owning fields are strings is NOT
|
|
11682
|
-
* auto-destroyed at scope exit (the strings may be raw rodata arg
|
|
11683
|
-
* pointers, not heap) — see `struct_needs_auto_destroy` for the broader
|
|
11684
|
-
* form used to decide whether a `<Struct>_destroy` function is generated.
|
|
11685
|
-
*/
|
|
11686
|
-
function has_struct_fields_with_destroy(struct, table) {
|
|
11687
|
-
for (const field of struct.fields) {
|
|
11688
|
-
if (field.type.is_ref) continue;
|
|
11689
|
-
const field_struct = resolve_struct_type(field.type, table);
|
|
11690
|
-
if (!field_struct) continue;
|
|
11691
|
-
if (field_struct.is_class) return true;
|
|
11692
|
-
if (has_destroy(field_struct)) return true;
|
|
11693
|
-
if (has_struct_fields_with_destroy(field_struct, table)) return true;
|
|
11694
|
-
}
|
|
11695
|
-
return false;
|
|
11696
|
-
}
|
|
11697
|
-
/**
|
|
11698
|
-
* Whether a struct (or any embedded struct field, recursively) needs a
|
|
11699
|
-
* destroy call at scope exit — it has a `#destroy`, a class-typed field,
|
|
11700
|
-
* or a nested struct field that itself needs destroying. This does NOT
|
|
11701
|
-
* count `string` fields (see `has_struct_fields_with_destroy`).
|
|
11702
|
-
*/
|
|
11703
|
-
function struct_needs_destroy(struct, table) {
|
|
11704
|
-
return has_destroy(struct) || has_struct_fields_with_destroy(struct, table) || struct_has_owning_enum_field(struct, table);
|
|
11705
|
-
}
|
|
11706
|
-
/**
|
|
11707
|
-
* Whether a struct needs an auto-generated `<Struct>_destroy` function.
|
|
11708
|
-
* Broader than `struct_needs_destroy`: also returns true for structs with
|
|
11709
|
-
* `string` fields, because a Buffer<T> for such a struct deep-copies the
|
|
11710
|
-
* strings into slots (strdup on store) and the per-element destroy must
|
|
11711
|
-
* free them. The generated destroy frees each string field; it is called
|
|
11712
|
-
* from Buffer's specialized #destroy / replace_T, NOT from struct local
|
|
11713
|
-
* scope exit (`struct_needs_destroy` governs that path and excludes
|
|
11714
|
-
* strings).
|
|
11715
|
-
*/
|
|
11716
|
-
function struct_needs_auto_destroy(struct, table) {
|
|
11717
|
-
if (struct_needs_destroy(struct, table)) return true;
|
|
11718
|
-
if (struct_has_owning_enum_field(struct, table)) return true;
|
|
11719
|
-
for (const field of struct.fields) {
|
|
11720
|
-
if (field.type.is_ref) continue;
|
|
11721
|
-
if (field.type.is_view) continue;
|
|
11722
|
-
if (field.type.name === "string" && !field.type.is_array) return true;
|
|
11723
|
-
const field_struct = resolve_struct_type(field.type, table);
|
|
11724
|
-
if (field_struct && !field_struct.is_class && struct_needs_auto_destroy(field_struct, table)) return true;
|
|
11725
|
-
}
|
|
11726
|
-
return false;
|
|
11727
|
-
}
|
|
11728
|
-
//#endregion
|
|
11729
11748
|
//#region ../src/build_common/scan_force_heap_strings.ts
|
|
11730
11749
|
function scan_force_heap_strings(statements, structs) {
|
|
11731
11750
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -14908,7 +14927,33 @@ function emit_base_ptr(status, decl_name, is_class_parent) {
|
|
|
14908
14927
|
if (is_class_parent) emit_var_load(status, "x0", decl_name, 8);
|
|
14909
14928
|
else emit_var_address(status, "x0", decl_name);
|
|
14910
14929
|
}
|
|
14911
|
-
|
|
14930
|
+
/**
|
|
14931
|
+
* Reclaim a struct's owning fields in place (base + every nested owning field).
|
|
14932
|
+
*
|
|
14933
|
+
* `base_offset` (added to each field offset) and `is_class_parent` (a class's
|
|
14934
|
+
* base is a pointer LOADED from the slot; a value struct is inline AT it)
|
|
14935
|
+
* locate `decl_name`'s storage.
|
|
14936
|
+
*
|
|
14937
|
+
* `free_strings` is REQUIRED — it is a per-call-site OWNOWNITY fact, not a
|
|
14938
|
+
* per-type one, so it deliberately has no default:
|
|
14939
|
+
* - `true` from a DESTROY function (`<T>_destroy`), which only ever runs on
|
|
14940
|
+
* heap-owned storage: a class's plain string fields are always heap-owned
|
|
14941
|
+
* (`_init` strdup's defaults, assignments strdup a non-heap RHS), and a
|
|
14942
|
+
* VALUE struct arrives with the per-slot copies `store_T`/`_copy` strdup'd
|
|
14943
|
+
* (Buffer per-element teardown, the displaced-field reclaim, the
|
|
14944
|
+
* anchor-slot walk). Matches C's build_auto_destroy, which frees string
|
|
14945
|
+
* fields unconditionally.
|
|
14946
|
+
* - `false` from struct-LOCAL scope exit, where a string field may still hold
|
|
14947
|
+
* a rodata literal (aarch64 constructors don't strdup) and freeing it would
|
|
14948
|
+
* `free` rodata (SIGABRT).
|
|
14949
|
+
*
|
|
14950
|
+
* A default (`= true`) previously stood in for the `true` arm while
|
|
14951
|
+
* `node.is_class` (undefined on plain parsed structs) was passed as if it
|
|
14952
|
+
* selected between the arms — so the day `is_class` becomes explicitly false,
|
|
14953
|
+
* value-struct slot strings would silently stop being reclaimed. Passing the
|
|
14954
|
+
* boolean at every call site makes both arms visible.
|
|
14955
|
+
*/
|
|
14956
|
+
function emit_field_destroys(status, struct_type, decl_name, base_offset, is_class_parent, free_strings) {
|
|
14912
14957
|
const destroy_fields = struct_type.is_class ? layout_fields(struct_type, status) : struct_type.fields;
|
|
14913
14958
|
for (const field of destroy_fields) {
|
|
14914
14959
|
const offset = get_field_offset_of_fields(destroy_fields, field.name, status);
|
|
@@ -15134,6 +15179,33 @@ function emit_destroy_for_scope(status, declarations_before) {
|
|
|
15134
15179
|
emit_asm(status, `ldr x19, [sp], #16\n`);
|
|
15135
15180
|
continue;
|
|
15136
15181
|
}
|
|
15182
|
+
if (status.heap_array_vars?.has(decl.name)) {
|
|
15183
|
+
if (status.heap_owned_string_arrays?.has(decl.name)) {
|
|
15184
|
+
emit_asm(status, `str x19, [sp, #-16]!\n`);
|
|
15185
|
+
emit_asm(status, `str x20, [sp, #-16]!\n`);
|
|
15186
|
+
emit_var_load(status, "x0", decl.name, 8);
|
|
15187
|
+
emit_asm(status, `mov x19, x0\n`);
|
|
15188
|
+
emit_asm(status, `ldr x20, [x19]\n`);
|
|
15189
|
+
emit_asm(status, `add x19, x19, #8\n`);
|
|
15190
|
+
const loop = `.Lhosa_${decl.name}`;
|
|
15191
|
+
emit_asm(status, `${loop}:\n`);
|
|
15192
|
+
emit_asm(status, `cbz x20, .Lhosa_done_${decl.name}\n`);
|
|
15193
|
+
emit_asm(status, `ldr x0, [x19]\n`);
|
|
15194
|
+
emit_free(status);
|
|
15195
|
+
emit_asm(status, `add x19, x19, #16\n`);
|
|
15196
|
+
emit_asm(status, `sub x20, x20, #1\n`);
|
|
15197
|
+
emit_asm(status, `b ${loop}\n`);
|
|
15198
|
+
emit_asm(status, `.Lhosa_done_${decl.name}:\n`);
|
|
15199
|
+
emit_var_load(status, "x0", decl.name, 8);
|
|
15200
|
+
emit_free(status);
|
|
15201
|
+
emit_asm(status, `ldr x20, [sp], #16\n`);
|
|
15202
|
+
emit_asm(status, `ldr x19, [sp], #16\n`);
|
|
15203
|
+
continue;
|
|
15204
|
+
}
|
|
15205
|
+
emit_var_load(status, "x0", decl.name, 8);
|
|
15206
|
+
emit_free(status);
|
|
15207
|
+
continue;
|
|
15208
|
+
}
|
|
15137
15209
|
if (status.heap_strings?.has(decl.name)) {
|
|
15138
15210
|
emit_var_load(status, "x0", decl.name, 8);
|
|
15139
15211
|
emit_free(status);
|
|
@@ -21187,7 +21259,7 @@ function build_destroy_function(node, func, status) {
|
|
|
21187
21259
|
status.code = status.code.slice(0, after_prologue) + saves + status.code.slice(after_prologue);
|
|
21188
21260
|
}
|
|
21189
21261
|
}
|
|
21190
|
-
if (node.is_class) emit_field_destroys(status, node, "self", void 0, false);
|
|
21262
|
+
if (node.is_class) emit_field_destroys(status, node, "self", void 0, false, true);
|
|
21191
21263
|
emit_asm(status, `${return_label}:\n`);
|
|
21192
21264
|
const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
|
|
21193
21265
|
status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
|
|
@@ -21244,7 +21316,7 @@ function build_auto_destroy_function(node, status) {
|
|
|
21244
21316
|
status.function_param_regs.set("self", "x19");
|
|
21245
21317
|
emit_asm(status, `sub sp, sp, #${stack_placeholder}\n`);
|
|
21246
21318
|
emit_asm(status, `mov x29, sp\n`);
|
|
21247
|
-
emit_field_destroys(status, node, "self", void 0, false,
|
|
21319
|
+
emit_field_destroys(status, node, "self", void 0, false, true);
|
|
21248
21320
|
emit_asm(status, `${return_label}:\n`);
|
|
21249
21321
|
const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
|
|
21250
21322
|
status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
|
|
@@ -24960,6 +25032,17 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
24960
25032
|
check_heap();
|
|
24961
25033
|
return;
|
|
24962
25034
|
}
|
|
25035
|
+
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";
|
|
25036
|
+
if (decl_string_field_read && !!status.force_heap_strings?.has(node.name)) {
|
|
25037
|
+
emit_init_value$1(node.value, nir_init, status);
|
|
25038
|
+
ensure_newline(status);
|
|
25039
|
+
emit_strdup_string(status);
|
|
25040
|
+
emit_var_store(status, "x0", node.name, size);
|
|
25041
|
+
mark_heap_string(status, node.name);
|
|
25042
|
+
status.last_result_is_heap = false;
|
|
25043
|
+
return;
|
|
25044
|
+
}
|
|
25045
|
+
if (decl_string_field_read) status.last_result_is_heap = false;
|
|
24963
25046
|
emit_init_value$1(node.value, nir_init, status);
|
|
24964
25047
|
emit_var_store(status, "x0", node.name, size);
|
|
24965
25048
|
check_heap();
|
|
@@ -33540,7 +33623,8 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
|
|
|
33540
33623
|
if (!node.operator && node.left_value.node_type === "access" && node.left_value.access.node_type === "access_field" && node.right_value.node_type === "value" && node.right_value.is_moved) {
|
|
33541
33624
|
const access_lhs = node.left_value;
|
|
33542
33625
|
const field_type = access_lhs.access.type;
|
|
33543
|
-
const
|
|
33626
|
+
const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
|
|
33627
|
+
const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
|
|
33544
33628
|
if (field_struct) {
|
|
33545
33629
|
splice_decl_from_c_scopes(status, node.right_value.value);
|
|
33546
33630
|
const saved_len = begin_code_scratch(status);
|
|
@@ -39017,10 +39101,31 @@ function build_access_node$1(node, status) {
|
|
|
39017
39101
|
status.code += `${ret_c} ${ctor_temp_val} = `;
|
|
39018
39102
|
}
|
|
39019
39103
|
}
|
|
39104
|
+
const owned_string_recv = !ctor_temp_receiver && (node.target.node_type === "func_call" || node.target.node_type === "access" && node.target.access.node_type === "access_func") && method_type?.name === "string" && !method_type.is_view && !is_string_borrow(node.target);
|
|
39105
|
+
let owned_recv_temp;
|
|
39106
|
+
let owned_recv_val;
|
|
39107
|
+
if (owned_string_recv) {
|
|
39108
|
+
const id = status.label_counter = (status.label_counter ?? 0) + 1;
|
|
39109
|
+
owned_recv_temp = `_osrecv_${id}`;
|
|
39110
|
+
status.code += `({ nomen_string ${owned_recv_temp} = `;
|
|
39111
|
+
{
|
|
39112
|
+
const saved_suppress = status.suppress_dereference;
|
|
39113
|
+
status.suppress_dereference = true;
|
|
39114
|
+
build_node$1(node.target, status);
|
|
39115
|
+
status.suppress_dereference = saved_suppress;
|
|
39116
|
+
}
|
|
39117
|
+
status.code += `; `;
|
|
39118
|
+
const recv_ret_c = c_return_type((target_method ?? trait_default_func)?.return_type ?? access_func.type, status);
|
|
39119
|
+
if (recv_ret_c !== "void") {
|
|
39120
|
+
owned_recv_val = `_osrecvv_${id}`;
|
|
39121
|
+
status.code += `${recv_ret_c} ${owned_recv_val} = `;
|
|
39122
|
+
}
|
|
39123
|
+
}
|
|
39020
39124
|
if (lambda_ret_tmp) status.code += `${lambda_ret_c} ${lambda_ret_tmp} = `;
|
|
39021
39125
|
status.code += `${label}(`;
|
|
39022
39126
|
if (!access_func.is_static) {
|
|
39023
39127
|
if (ctor_temp_free) status.code += ctor_temp_free;
|
|
39128
|
+
else if (owned_recv_temp) status.code += owned_recv_temp;
|
|
39024
39129
|
else if (array_wrap) status.code += array_wrap.self_expr;
|
|
39025
39130
|
else {
|
|
39026
39131
|
const method_self_is_ref = !!target_method?.params?.some((p) => p.is_self_param && (p.is_ref || p.type?.is_ref));
|
|
@@ -39105,6 +39210,11 @@ function build_access_node$1(node, status) {
|
|
|
39105
39210
|
if (ctor_temp_val) status.code += ` ${ctor_temp_val};`;
|
|
39106
39211
|
status.code += ` })`;
|
|
39107
39212
|
}
|
|
39213
|
+
if (owned_recv_temp) {
|
|
39214
|
+
status.code += `; ${status.audit ? "nomen_free_wrap" : "free"}(${owned_recv_temp}.ptr);`;
|
|
39215
|
+
if (owned_recv_val) status.code += ` ${owned_recv_val};`;
|
|
39216
|
+
status.code += ` })`;
|
|
39217
|
+
}
|
|
39108
39218
|
if (lambda_arg_temps.size > 0) {
|
|
39109
39219
|
for (const tmp of lambda_arg_temps.values()) status.code += `; ${closure_dispose_arm(tmp)}`;
|
|
39110
39220
|
if (lambda_ret_tmp) status.code += `; ${lambda_ret_tmp}`;
|
|
@@ -41629,7 +41739,9 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
|
|
|
41629
41739
|
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;
|
|
41630
41740
|
const move_source = rhs_is_string_var && node.last_use_move && move_on_last_use_enabled() ? rhs_value.value : void 0;
|
|
41631
41741
|
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;
|
|
41632
|
-
const
|
|
41742
|
+
const rhs_is_borrow_reception = !node.swap && size === 16 && is_string_borrow(node.right_value);
|
|
41743
|
+
const rhs_is_field_read = !node.swap && size === 16 && node.right_value.node_type === "access" && node.right_value.access.node_type === "access_field";
|
|
41744
|
+
const borrow_needs_dup = (rhs_is_borrow_reception || rhs_is_field_read) && !!status.force_heap_strings?.has(name);
|
|
41633
41745
|
const move_source_owns_heap = move_source !== void 0 && !!status.heap_strings?.has(move_source);
|
|
41634
41746
|
if (rhs_is_view_into_owned) {
|
|
41635
41747
|
if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
|
|
@@ -41639,7 +41751,8 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
|
|
|
41639
41751
|
if (move_source !== void 0 && move_source_owns_heap) status.heap_strings?.delete(move_source);
|
|
41640
41752
|
if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
|
|
41641
41753
|
status.heap_strings.add(name);
|
|
41642
|
-
} else if (status.last_result_is_heap
|
|
41754
|
+
} else if (rhs_is_field_read) status.last_result_is_heap = false;
|
|
41755
|
+
else if (status.last_result_is_heap) {
|
|
41643
41756
|
if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
|
|
41644
41757
|
status.heap_strings.add(name);
|
|
41645
41758
|
} else if (explicit_move_source !== void 0 && status.heap_strings?.delete(explicit_move_source)) status.heap_strings.add(name);
|
|
@@ -41856,6 +41969,22 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
|
|
|
41856
41969
|
get_base_address(access, status, "x0");
|
|
41857
41970
|
emit_asm(status, `str x0, [sp, #-16]!\n`);
|
|
41858
41971
|
}
|
|
41972
|
+
if (!node.operator && node.right_value.node_type === "value" && node.right_value.is_moved) {
|
|
41973
|
+
const field_resolved = resolve_struct_type(field_type, status);
|
|
41974
|
+
const field_target_var = access.target.node_type === "value" ? access.target.value : "";
|
|
41975
|
+
const first_init_write = is_first_init_field_write(field_target_var, field_name, status);
|
|
41976
|
+
mark_init_field_write(field_target_var, field_name, status);
|
|
41977
|
+
if (field_resolved && !field_resolved.is_class && !first_init_write && struct_needs_auto_destroy(field_resolved, status)) {
|
|
41978
|
+
if (base_reg !== void 0) {
|
|
41979
|
+
if (offset !== 0) emit_asm(status, `add x0, ${base_reg}, #${offset}\n`);
|
|
41980
|
+
else emit_asm(status, `mov x0, ${base_reg}\n`);
|
|
41981
|
+
} else {
|
|
41982
|
+
emit_asm(status, `ldr x0, [sp]\n`);
|
|
41983
|
+
if (offset !== 0) emit_asm(status, `add x0, x0, #${offset}\n`);
|
|
41984
|
+
}
|
|
41985
|
+
emit_asm(status, `bl ${field_resolved.name}_destroy\n`);
|
|
41986
|
+
}
|
|
41987
|
+
}
|
|
41859
41988
|
get_source_address(node.right_value, status, nir_rhs);
|
|
41860
41989
|
ensure_newline(status);
|
|
41861
41990
|
emit_asm(status, `mov x1, x0\n`);
|
|
@@ -45635,7 +45764,7 @@ function build_access_method(node, access_func, status) {
|
|
|
45635
45764
|
status.last_result_is_heap = false;
|
|
45636
45765
|
build_node(node.target, status);
|
|
45637
45766
|
ensure_newline(status);
|
|
45638
|
-
frees_string_receiver =
|
|
45767
|
+
frees_string_receiver = status.last_result_is_heap;
|
|
45639
45768
|
status.last_result_is_heap = false;
|
|
45640
45769
|
emit_asm(status, `stp x0, x1, [sp, #-16]!\n`);
|
|
45641
45770
|
}
|
|
@@ -47257,7 +47386,8 @@ function build(root, options = {}) {
|
|
|
47257
47386
|
status.code = status.code.replaceAll("bl _strdup\n", "bl _nomen_strdup_wrap\n");
|
|
47258
47387
|
status.code = status.code.replaceAll("bl _free\n", "bl _nomen_free_wrap\n");
|
|
47259
47388
|
}
|
|
47260
|
-
const
|
|
47389
|
+
const { result: lifted } = lift_functions(status.code);
|
|
47390
|
+
const lift_errors = lifted.errors;
|
|
47261
47391
|
if (lift_errors.length > 0) {
|
|
47262
47392
|
if (!status.build_errors) status.build_errors = [];
|
|
47263
47393
|
for (const e of lift_errors.slice(0, 20)) status.build_errors.push({
|
|
@@ -47265,7 +47395,7 @@ function build(root, options = {}) {
|
|
|
47265
47395
|
start: 0
|
|
47266
47396
|
});
|
|
47267
47397
|
}
|
|
47268
|
-
const balance_errors =
|
|
47398
|
+
const balance_errors = stack_balance_errors_from_lift(lifted);
|
|
47269
47399
|
if (balance_errors.length > 0) {
|
|
47270
47400
|
if (!status.build_errors) status.build_errors = [];
|
|
47271
47401
|
for (const e of balance_errors.slice(0, 20)) status.build_errors.push({
|
|
@@ -62443,7 +62573,9 @@ const STRING_OPTIONS = /* @__PURE__ */ new Map([
|
|
|
62443
62573
|
["p", "platform"],
|
|
62444
62574
|
["lib", "lib"],
|
|
62445
62575
|
["l", "lib"],
|
|
62446
|
-
["audit-runtime", "audit_runtime"]
|
|
62576
|
+
["audit-runtime", "audit_runtime"],
|
|
62577
|
+
["jobs", "jobs"],
|
|
62578
|
+
["j", "jobs"]
|
|
62447
62579
|
]);
|
|
62448
62580
|
const BOOLEAN_OPTIONS = /* @__PURE__ */ new Map([
|
|
62449
62581
|
["watch", "watch"],
|
|
@@ -62553,8 +62685,9 @@ function print_help() {
|
|
|
62553
62685
|
" --platform, -p <name> Target platform: macos, ios, linux, android, windows, web",
|
|
62554
62686
|
" --lib, -l <path> Path to System library directory (containing package.jsonc)",
|
|
62555
62687
|
" --audit Audit the generated program for memory issues",
|
|
62556
|
-
" --audit-runtime <path> Path to audit_runtime.c
|
|
62557
|
-
" --
|
|
62688
|
+
" --audit-runtime <path> Path to audit_runtime.c (default: auto-discovered — walk-up or the CLI-bundled copy)",
|
|
62689
|
+
" --jobs, -j <n> For `nomen test`: max test files run in parallel (default: CPU count)",
|
|
62690
|
+
" --release, -r Build with optimizations (clang -O2; aarch64 asm cleanup passes)",
|
|
62558
62691
|
" --fast-math Allow FP reassociation (float loop reductions vectorize on aarch64)",
|
|
62559
62692
|
" --check For `nomen format`: report files that would change",
|
|
62560
62693
|
" -h, --help Show this help"
|
|
@@ -62902,6 +63035,9 @@ function find_bundled(filename) {
|
|
|
62902
63035
|
const candidates = !cli_root.includes("node_modules") ? [path.join(repo_root, filename), path.join(cli_root, filename)] : [path.join(cli_root, filename), path.join(repo_root, filename)];
|
|
62903
63036
|
for (const c of candidates) if (fs.existsSync(c)) return path.resolve(c);
|
|
62904
63037
|
}
|
|
63038
|
+
function find_bundled_audit_runtime() {
|
|
63039
|
+
return find_bundled("src/audit_runtime.c") ?? find_bundled("audit_runtime.c");
|
|
63040
|
+
}
|
|
62905
63041
|
/**
|
|
62906
63042
|
* `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
|
|
62907
63043
|
* starting layout (package.jsonc, src/main.nm, a starter test, README and an
|
|
@@ -62980,6 +63116,7 @@ function outfile_for(input_path, is_test, out) {
|
|
|
62980
63116
|
//#endregion
|
|
62981
63117
|
//#region src/test.ts
|
|
62982
63118
|
const RECORD_PREFIX = "\\nomen|";
|
|
63119
|
+
const execFile_async = promisify(execFile);
|
|
62983
63120
|
/** Recursively collect `*.test.nm` files under `root`, skipping build output. */
|
|
62984
63121
|
function collect_test_files(root) {
|
|
62985
63122
|
const out = [];
|
|
@@ -63063,11 +63200,19 @@ function generate_harness(tests, benches) {
|
|
|
63063
63200
|
const n = Math.min(raw, 4096);
|
|
63064
63201
|
return template.replace(/__NAME__/g, b.name).replace(/__TARGET__/g, b.target).replace(/__N__/g, String(n));
|
|
63065
63202
|
}).join("\n");
|
|
63066
|
-
let main = "\nimport System\nimport System::Test\n
|
|
63203
|
+
let main = "\nimport System\nimport System::Test\n";
|
|
63204
|
+
main += "\npub func main = () {\n";
|
|
63067
63205
|
main += " var Tester t = Tester()\n";
|
|
63206
|
+
main += " var int64 audit_mark = 0\n";
|
|
63207
|
+
main += " var int64 audit_leaked = 0\n";
|
|
63068
63208
|
for (const test of tests) {
|
|
63209
|
+
main += `\taudit_mark = Tester.audit_count()\n`;
|
|
63069
63210
|
main += `\tt.begin_test("${escape_nm_string(test.name)}")\n`;
|
|
63070
63211
|
main += `\t${test.name}(ref t)\n`;
|
|
63212
|
+
main += `\taudit_leaked = Tester.audit_count() - audit_mark\n`;
|
|
63213
|
+
main += `\tif audit_leaked > 0 {\n`;
|
|
63214
|
+
main += `\t\tConsole.write_line("\\\\nomen|leaks|" + "${escape_nm_string(test.name)}" + "|\\{audit_leaked}")\n`;
|
|
63215
|
+
main += `\t}\n`;
|
|
63071
63216
|
main += `\tt.end_test()\n`;
|
|
63072
63217
|
}
|
|
63073
63218
|
for (const bench of benches) {
|
|
@@ -63086,6 +63231,7 @@ function parse_records(stdout) {
|
|
|
63086
63231
|
const tests = [];
|
|
63087
63232
|
const fails = [];
|
|
63088
63233
|
const benches = [];
|
|
63234
|
+
const leaks = [];
|
|
63089
63235
|
const other = [];
|
|
63090
63236
|
for (const line of stdout.split("\n")) {
|
|
63091
63237
|
if (!line.startsWith(RECORD_PREFIX)) {
|
|
@@ -63104,6 +63250,10 @@ function parse_records(stdout) {
|
|
|
63104
63250
|
test: parts[1],
|
|
63105
63251
|
message: parts.slice(2).join("|")
|
|
63106
63252
|
});
|
|
63253
|
+
else if (kind === "leaks") leaks.push({
|
|
63254
|
+
test: parts[1],
|
|
63255
|
+
count: parseInt(parts[2] || "0", 10)
|
|
63256
|
+
});
|
|
63107
63257
|
else if (kind === "bench") benches.push({
|
|
63108
63258
|
label: parts[1],
|
|
63109
63259
|
n: parseInt(parts[2] || "0", 10),
|
|
@@ -63118,18 +63268,25 @@ function parse_records(stdout) {
|
|
|
63118
63268
|
tests,
|
|
63119
63269
|
fails,
|
|
63120
63270
|
benches,
|
|
63271
|
+
leaks,
|
|
63121
63272
|
other
|
|
63122
63273
|
};
|
|
63123
63274
|
}
|
|
63124
|
-
/**
|
|
63275
|
+
/**
|
|
63276
|
+
* Pull the exit-time audit lines out of stdout: "LEAK: N allocation(s)" (a
|
|
63277
|
+
* nonzero malloc/free balance) and "AUDIT-STALE-FREE: N pointer(s) freed that
|
|
63278
|
+
* were not live audit allocations" (a free that matched no live block — a
|
|
63279
|
+
* foreign pointer or a double free; the runtime no longer passes those to
|
|
63280
|
+
* libc free, so this is the only record they leave). Both fail the file.
|
|
63281
|
+
*/
|
|
63125
63282
|
function extract_leaks(stdout) {
|
|
63126
|
-
return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:"));
|
|
63283
|
+
return stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("LEAK:") || l.startsWith("AUDIT-STALE-FREE:"));
|
|
63127
63284
|
}
|
|
63128
63285
|
/**
|
|
63129
63286
|
* Compile one `*.test.nm` file (with the generated harness) and run it,
|
|
63130
63287
|
* returning its parsed records and any crash info.
|
|
63131
63288
|
*/
|
|
63132
|
-
function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
|
|
63289
|
+
async function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
|
|
63133
63290
|
const start = performance.now();
|
|
63134
63291
|
const source_text = fs.readFileSync(entry_path, "utf8");
|
|
63135
63292
|
const harness = generate_harness(extract_test_functions(source_text), extract_bench_functions(source_text));
|
|
@@ -63139,8 +63296,9 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63139
63296
|
tests: [],
|
|
63140
63297
|
fails: [],
|
|
63141
63298
|
benches: [],
|
|
63142
|
-
other: [],
|
|
63143
63299
|
leaks: [],
|
|
63300
|
+
leaking_tests: [],
|
|
63301
|
+
other: [],
|
|
63144
63302
|
ms: 0
|
|
63145
63303
|
};
|
|
63146
63304
|
const resolved = path.resolve(entry_path);
|
|
@@ -63168,24 +63326,21 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63168
63326
|
}
|
|
63169
63327
|
const buildDir = build_dir_for(resolved, true);
|
|
63170
63328
|
if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
|
|
63171
|
-
|
|
63172
|
-
if (
|
|
63173
|
-
|
|
63174
|
-
|
|
63175
|
-
|
|
63176
|
-
|
|
63177
|
-
|
|
63178
|
-
result.ms = performance.now() - start;
|
|
63179
|
-
return result;
|
|
63180
|
-
}
|
|
63181
|
-
audit_obj = path.join(buildDir, "audit_runtime.o");
|
|
63182
|
-
execFileSync("clang", [
|
|
63183
|
-
"-c",
|
|
63184
|
-
runtime_src,
|
|
63185
|
-
"-o",
|
|
63186
|
-
audit_obj
|
|
63187
|
-
]);
|
|
63329
|
+
const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime();
|
|
63330
|
+
if (!runtime_src || !fs.existsSync(runtime_src)) {
|
|
63331
|
+
result.ok = false;
|
|
63332
|
+
result.phase = "setup";
|
|
63333
|
+
result.crashed = "audit_runtime.c was not found. Pass --audit-runtime <path>.";
|
|
63334
|
+
result.ms = performance.now() - start;
|
|
63335
|
+
return result;
|
|
63188
63336
|
}
|
|
63337
|
+
const audit_obj = path.join(buildDir, "audit_runtime.o");
|
|
63338
|
+
await execFile_async("clang", [
|
|
63339
|
+
"-c",
|
|
63340
|
+
runtime_src,
|
|
63341
|
+
"-o",
|
|
63342
|
+
audit_obj
|
|
63343
|
+
]);
|
|
63189
63344
|
const platform = default_platform();
|
|
63190
63345
|
const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
|
|
63191
63346
|
const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
|
|
@@ -63208,14 +63363,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63208
63363
|
if (audit_obj) link_args.push(audit_obj);
|
|
63209
63364
|
if (platform === "macos" || platform === "ios") link_args.push("-framework", "CoreGraphics", "-framework", "Foundation", "-framework", "AppKit", "-lobjc");
|
|
63210
63365
|
try {
|
|
63211
|
-
|
|
63212
|
-
encoding: "utf8",
|
|
63213
|
-
stdio: [
|
|
63214
|
-
"ignore",
|
|
63215
|
-
"pipe",
|
|
63216
|
-
"pipe"
|
|
63217
|
-
]
|
|
63218
|
-
});
|
|
63366
|
+
await execFile_async("clang", link_args, { encoding: "utf8" });
|
|
63219
63367
|
} catch (err) {
|
|
63220
63368
|
const stderr = err.stderr ? err.stderr.toString() : err.message ?? "";
|
|
63221
63369
|
result.ok = false;
|
|
@@ -63227,17 +63375,12 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63227
63375
|
let runStdout = "";
|
|
63228
63376
|
let crashed;
|
|
63229
63377
|
try {
|
|
63230
|
-
runStdout =
|
|
63378
|
+
runStdout = (await execFile_async(outfile, [], {
|
|
63231
63379
|
encoding: "utf8",
|
|
63232
|
-
stdio: [
|
|
63233
|
-
"ignore",
|
|
63234
|
-
"pipe",
|
|
63235
|
-
"pipe"
|
|
63236
|
-
],
|
|
63237
63380
|
timeout: 3e4,
|
|
63238
63381
|
maxBuffer: 16777216,
|
|
63239
63382
|
cwd: cwd ?? process.cwd()
|
|
63240
|
-
});
|
|
63383
|
+
})).stdout;
|
|
63241
63384
|
} catch (err) {
|
|
63242
63385
|
runStdout = err.stdout ? err.stdout.toString() : "";
|
|
63243
63386
|
if (err.signal === "SIGTERM") crashed = "test binary timed out after 30s";
|
|
@@ -63247,9 +63390,10 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63247
63390
|
result.tests = records.tests;
|
|
63248
63391
|
result.fails = records.fails;
|
|
63249
63392
|
result.benches = records.benches;
|
|
63393
|
+
result.leaking_tests = records.leaks;
|
|
63250
63394
|
result.other = records.other;
|
|
63251
63395
|
result.leaks = audit ? extract_leaks(runStdout) : [];
|
|
63252
|
-
if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:"));
|
|
63396
|
+
if (result.leaks.length) result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:") && !l.trim().startsWith("AUDIT-STALE-FREE:"));
|
|
63253
63397
|
result.crashed = crashed;
|
|
63254
63398
|
if (crashed) result.phase = "run";
|
|
63255
63399
|
result.ok = !(result.fails.length > 0 || result.tests.some((t) => t.failed > 0)) && crashed === void 0 && result.leaks.length === 0;
|
|
@@ -63290,16 +63434,26 @@ function report_file(result) {
|
|
|
63290
63434
|
if (result.crashed) console.log(C.red(` ${result.crashed}`));
|
|
63291
63435
|
for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
|
|
63292
63436
|
for (const leak of result.leaks) console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
|
|
63437
|
+
if (result.leaks.length) {
|
|
63438
|
+
for (const lt of result.leaking_tests) console.log(` ${C.yellow("⚠")} ${lt.test} ${C.dim(`leaked ${lt.count} allocation(s)`)}`);
|
|
63439
|
+
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")}`);
|
|
63440
|
+
}
|
|
63293
63441
|
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))}`);
|
|
63294
63442
|
if (result.other.length) {
|
|
63295
63443
|
console.log(C.dim(" --- test stdout ---"));
|
|
63296
63444
|
for (const line of result.other) console.log(C.dim(` ${line}`));
|
|
63297
63445
|
}
|
|
63298
63446
|
}
|
|
63447
|
+
/** The default concurrency for `runTests` when no `jobs` is given. */
|
|
63448
|
+
function default_jobs() {
|
|
63449
|
+
const n = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
|
|
63450
|
+
return Math.max(1, n);
|
|
63451
|
+
}
|
|
63299
63452
|
/**
|
|
63300
63453
|
* Locate audit_runtime.c for an audited test build: an explicit path wins,
|
|
63301
63454
|
* otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
|
|
63302
|
-
* convention the run/build commands use).
|
|
63455
|
+
* convention the run/build commands use). When both miss, the caller falls
|
|
63456
|
+
* back to the CLI-bundled runtime (`find_bundled_audit_runtime`).
|
|
63303
63457
|
*/
|
|
63304
63458
|
function find_audit_runtime(dir) {
|
|
63305
63459
|
let cur = dir;
|
|
@@ -63312,19 +63466,25 @@ function find_audit_runtime(dir) {
|
|
|
63312
63466
|
}
|
|
63313
63467
|
}
|
|
63314
63468
|
/** Discover, run, and report every `*.test.nm` under `root`. */
|
|
63315
|
-
function runTests(root, options = {}) {
|
|
63469
|
+
async function runTests(root, options = {}) {
|
|
63316
63470
|
const arch = options.arch ?? "aarch64";
|
|
63317
63471
|
const lib = resolve_lib_for(root);
|
|
63318
63472
|
const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
|
|
63319
63473
|
console.log(`Found ${files.length} test file(s)\n`);
|
|
63320
63474
|
const startTime = performance.now();
|
|
63321
63475
|
const binary_cwd = path.resolve(root);
|
|
63322
|
-
const
|
|
63323
|
-
|
|
63324
|
-
|
|
63325
|
-
|
|
63326
|
-
|
|
63327
|
-
|
|
63476
|
+
const jobs = Math.max(1, Math.min(options.jobs ?? default_jobs(), files.length));
|
|
63477
|
+
const results = new Array(files.length);
|
|
63478
|
+
let next = 0;
|
|
63479
|
+
const worker = async () => {
|
|
63480
|
+
for (;;) {
|
|
63481
|
+
const i = next++;
|
|
63482
|
+
if (i >= files.length) return;
|
|
63483
|
+
results[i] = await run_test_file(files[i], lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
|
|
63484
|
+
}
|
|
63485
|
+
};
|
|
63486
|
+
await Promise.all(Array.from({ length: jobs }, worker));
|
|
63487
|
+
for (const result of results) report_file(result);
|
|
63328
63488
|
const elapsed = performance.now() - startTime;
|
|
63329
63489
|
const totalFiles = results.length;
|
|
63330
63490
|
const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
|
|
@@ -63333,11 +63493,12 @@ function runTests(root, options = {}) {
|
|
|
63333
63493
|
const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
|
|
63334
63494
|
const totalPassed = totalTests - totalFailed;
|
|
63335
63495
|
const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
|
|
63496
|
+
const totalLeakingTests = results.reduce((a, r) => a + r.leaking_tests.length, 0);
|
|
63336
63497
|
const anyFailed = results.some((r) => !r.ok);
|
|
63337
63498
|
console.log("");
|
|
63338
63499
|
console.log(` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` + (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") + C.dim(` (${totalFiles})`));
|
|
63339
63500
|
console.log(` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` + (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") + C.dim(` (${totalTests})`));
|
|
63340
|
-
if (totalLeaks) console.log(` ${C.dim("Leaks ")} ${C.yellow(`${
|
|
63501
|
+
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)"));
|
|
63341
63502
|
console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
|
|
63342
63503
|
console.log("");
|
|
63343
63504
|
return !anyFailed;
|
|
@@ -63425,12 +63586,13 @@ try {
|
|
|
63425
63586
|
if (command === "test") {
|
|
63426
63587
|
const root = args.in ?? process.cwd();
|
|
63427
63588
|
const filter = args.filter ? new RegExp(args.filter) : void 0;
|
|
63428
|
-
const ok = runTests(root, {
|
|
63589
|
+
const ok = await runTests(root, {
|
|
63429
63590
|
arch: args.arch ?? "aarch64",
|
|
63430
63591
|
filter,
|
|
63431
63592
|
audit: args.audit,
|
|
63432
63593
|
audit_runtime: args.audit_runtime,
|
|
63433
|
-
release: args.release
|
|
63594
|
+
release: args.release,
|
|
63595
|
+
jobs: args.jobs !== void 0 ? parseInt(args.jobs, 10) : void 0
|
|
63434
63596
|
});
|
|
63435
63597
|
process.exit(ok ? 0 : 1);
|
|
63436
63598
|
}
|
|
@@ -63541,6 +63703,7 @@ function resolve_audit_runtime(config, input_path) {
|
|
|
63541
63703
|
if (parent === dir) break;
|
|
63542
63704
|
dir = parent;
|
|
63543
63705
|
}
|
|
63706
|
+
return find_bundled_audit_runtime();
|
|
63544
63707
|
}
|
|
63545
63708
|
function compile_audit_runtime(config, input_path, buildDir) {
|
|
63546
63709
|
const runtime_src = resolve_audit_runtime(config, input_path);
|
|
@@ -63617,7 +63780,7 @@ function processFile(filename, config, mode, program_args) {
|
|
|
63617
63780
|
console.log(`Created ${codefile} in ${(compileTime - startTime).toFixed(2)}ms`);
|
|
63618
63781
|
console.log("");
|
|
63619
63782
|
startTime = performance.now();
|
|
63620
|
-
const audit_obj = config.audit ? compile_audit_runtime(config, resolved, buildDir) : void 0;
|
|
63783
|
+
const audit_obj = config.audit || result.code.includes("nomen_audit_count") ? compile_audit_runtime(config, resolved, buildDir) : void 0;
|
|
63621
63784
|
let link_inputs = codefile;
|
|
63622
63785
|
if (companionfile) link_inputs += ` ${companionfile}`;
|
|
63623
63786
|
if (audit_obj) link_inputs += ` ${audit_obj}`;
|