nomen-lang 0.6.10 → 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.
@@ -43,6 +43,32 @@ pub struct Console {
43
43
  ```
44
44
  }
45
45
 
46
+ // Disable stdout buffering: every write goes straight to the file
47
+ // descriptor. The `nomen test` harness calls this at startup so a
48
+ // crashing binary still leaves every record it printed before the
49
+ // crash — a buffered stdout loses its pending bytes on an abnormal
50
+ // exit, which used to turn a mid-suite abort into "no records at all".
51
+ pub func unbuffered = () {
52
+ ```
53
+ #arch: c
54
+ setbuf(stdout, NULL);
55
+ ```
56
+ ```
57
+ #arch: aarch64
58
+ // setbuf(stdout, NULL): Apple's stdio names the FILE* global
59
+ // __stdoutp (stdout itself is a macro), so the Mach-O symbol is
60
+ // ___stdoutp — and it must load through the GOT (@GOTPAGE), the
61
+ // only relocation the linker accepts for it. The aarch64 backend
62
+ // always emits Mach-O symbol names (see `bl _printf` above), so
63
+ // this is unconditional like every other core raw body.
64
+ adrp x0, ___stdoutp@GOTPAGE
65
+ ldr x0, [x0, ___stdoutp@GOTPAGEOFF]
66
+ ldr x0, [x0]
67
+ mov x1, #0
68
+ bl _setbuf
69
+ ```
70
+ }
71
+
46
72
  // Read one line from stdin (newline stripped). The raw body mallocs the
47
73
  // buffer — `move out string` transfers ownership to the caller.
48
74
  pub func read_line = (move out string) {
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";
@@ -2306,6 +2306,13 @@ function parse_mem_inner(inner) {
2306
2306
  if (tail_shift) mem.scale = 1 << parseInt(tail_shift[1], 10);
2307
2307
  continue;
2308
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
+ }
2309
2316
  return null;
2310
2317
  }
2311
2318
  return mem;
@@ -7529,6 +7536,13 @@ var DeclarationNode = class extends BaseNode {
7529
7536
  * a heap shell for the argument (pass-by-value): the callee frees the
7530
7537
  * shell itself at scope exit (`free(p)`) after freeing the fields. */
7531
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;
7532
7546
  constructor(start, visibility, declaration, name, type, value) {
7533
7547
  super("declare", start);
7534
7548
  this.visibility = visibility;
@@ -20764,16 +20778,19 @@ function build_function_node$1(node, status) {
20764
20778
  if (param.is_moved) {
20765
20779
  const is_class = !!status.structs.find((s) => s.name === param.type.name && s.is_class);
20766
20780
  const is_trait = !!status.traits.find((t) => t.name === param.type.name);
20767
- if (is_class || is_trait) {
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) {
20768
20784
  const reg = callee_map.get(param.name) ?? param_regs[pidx];
20769
- status.moved_class_params.set(param.name, reg);
20785
+ if (is_class || is_trait) status.moved_class_params.set(param.name, reg);
20770
20786
  const save_offset = allocate_stack_space(status, 8);
20771
20787
  emit_asm(status, `str ${reg}, [x29, #${save_offset}]\n`);
20772
20788
  moved_param_save_slots.set(param.name, {
20773
20789
  offset: save_offset,
20774
20790
  type_name: param.type.name,
20775
20791
  type_args: param.type.type_args,
20776
- 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
20777
20794
  });
20778
20795
  }
20779
20796
  }
@@ -20881,6 +20898,16 @@ function build_function_node$1(node, status) {
20881
20898
  emit_asm(status, `cmp x0, x1\n`);
20882
20899
  emit_asm(status, `beq ${keep_prefix}_${name}\n`);
20883
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
+ }
20884
20911
  emit_destroy_for_anchor_slot(status, info.offset, info.type_name, info.type_args, info.is_nullable);
20885
20912
  emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
20886
20913
  emit_free(status);
@@ -21091,6 +21118,7 @@ function init_nullable_field_default(node, field, base_reg, status) {
21091
21118
  * sites record it (record_ctor_heap_default_fields) so scope exit frees it.
21092
21119
  */
21093
21120
  function init_expr_field_default(node, field, offset, base_reg, status) {
21121
+ status.last_result_is_heap = false;
21094
21122
  build_node(field.value, status);
21095
21123
  ensure_newline(status);
21096
21124
  const t = field.type;
@@ -22216,7 +22244,10 @@ function build_struct_functions$1(node, status) {
22216
22244
  const moved_param_save_slots = /* @__PURE__ */ new Map();
22217
22245
  for (const param of func.params) {
22218
22246
  if (!param.is_moved || param.is_self_param) continue;
22219
- if (!status.structs.find((s) => s.name === param.type.name && s.is_class)) continue;
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;
22220
22251
  const reg = status.function_param_regs.get(param.name);
22221
22252
  if (reg) {
22222
22253
  const save_offset = allocate_stack_space(status, 8);
@@ -22225,7 +22256,8 @@ function build_struct_functions$1(node, status) {
22225
22256
  offset: save_offset,
22226
22257
  type_name: param.type.name,
22227
22258
  type_args: param.type.type_args,
22228
- 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
22229
22261
  });
22230
22262
  } else {
22231
22263
  const offset = status.stack_offsets.get(param.name);
@@ -22233,7 +22265,8 @@ function build_struct_functions$1(node, status) {
22233
22265
  offset,
22234
22266
  type_name: param.type.name,
22235
22267
  type_args: param.type.type_args,
22236
- 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
22237
22270
  });
22238
22271
  }
22239
22272
  }
@@ -22288,6 +22321,16 @@ function build_struct_functions$1(node, status) {
22288
22321
  emit_asm(status, `cmp x0, x1\n`);
22289
22322
  emit_asm(status, `beq ${keep_prefix}_${name}\n`);
22290
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
+ }
22291
22334
  emit_destroy_for_anchor_slot(status, info.offset, info.type_name, info.type_args, info.is_nullable);
22292
22335
  emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
22293
22336
  emit_free(status);
@@ -22733,6 +22776,7 @@ function build_block_node$1(node, status) {
22733
22776
  const prologue = status.module_init_statements;
22734
22777
  status.module_init_statements = void 0;
22735
22778
  for (const stmt of prologue) {
22779
+ status.last_result_is_heap = false;
22736
22780
  emit_allocations$1(stmt, status);
22737
22781
  emit_stmt_from_nir$1(stmt, prologue.indexOf(stmt), prologue, status);
22738
22782
  }
@@ -22742,6 +22786,7 @@ function build_block_node$1(node, status) {
22742
22786
  if (module_init_set.has(child)) continue;
22743
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))) {
22744
22788
  if (child.node_type !== "while") emit_allocations$1(child, status);
22789
+ status.last_result_is_heap = false;
22745
22790
  const prev_forwarded = status.forwarded_param_inits;
22746
22791
  const consumed = emit_stmt_from_nir$1(child, index, node.statements, status);
22747
22792
  index += consumed - 1;
@@ -23918,7 +23963,6 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23918
23963
  }
23919
23964
  hoist_field_overrides(node.value, build_node, status, "", node.name);
23920
23965
  status.last_result_is_heap = false;
23921
- const prev_heap = status.last_result_is_heap;
23922
23966
  if (node.value?.node_type === "value") {
23923
23967
  const inlined = status.top_level_consts?.get(node.value.value);
23924
23968
  if (inlined?.value) node.value = fold_string_const(inlined, status.top_level_consts) ?? inlined.value;
@@ -23940,7 +23984,6 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23940
23984
  consolidate_temp_anchors$1(status, node.value, node.type.name);
23941
23985
  }
23942
23986
  }
23943
- status.last_result_is_heap = prev_heap;
23944
23987
  }
23945
23988
  if (node.func_params) {
23946
23989
  if (status.function_return_label) {
@@ -24828,8 +24871,12 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24828
24871
  emit_init_value$1(node.value, nir_init, status);
24829
24872
  ensure_newline(status);
24830
24873
  status.int_dest_hint = void 0;
24831
- if (size === 16 && node.type.name === "string") emit_pair_store_x29(status, offset);
24832
- else emit_var_store(status, "x0", node.name, size);
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);
24833
24880
  } else if (is_float_type$1(node.type.name)) {
24834
24881
  const label = `_float_const_${decl_const_counter++}`;
24835
24882
  emit_data(status, `${label}: .double ${raw}\n.p2align 2\n`);
@@ -31099,6 +31146,10 @@ function build_auto_free(status) {
31099
31146
  function free_scoped_declarations(status, decls, persist_string_field_records = false) {
31100
31147
  let commented = false;
31101
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
+ }
31102
31153
  const prefix = `${dec.name}.`;
31103
31154
  const keys = Array.from(status.heap_string_fields).filter((key) => key.startsWith(prefix));
31104
31155
  if (!keys.length) continue;
@@ -31130,6 +31181,18 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
31130
31181
  }
31131
31182
  if (persist_string_field_records && dec.force_owned_string === true) continue;
31132
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
+ }
31133
31196
  const cname = c_function_name(dec.name);
31134
31197
  if (dec.type.name === "func") {
31135
31198
  if (!commented) {
@@ -33620,13 +33683,15 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
33620
33683
  }
33621
33684
  }
33622
33685
  }
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) {
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)) {
33624
33689
  const access_lhs = node.left_value;
33625
33690
  const field_type = access_lhs.access.type;
33626
33691
  const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
33627
33692
  const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
33628
33693
  if (field_struct) {
33629
- 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);
33630
33695
  const saved_len = begin_code_scratch(status);
33631
33696
  build_node$1(node.left_value, status);
33632
33697
  const field_access = end_code_scratch(status, saved_len);
@@ -36845,9 +36910,16 @@ function build_struct_functions(node, status, skip_init = false) {
36845
36910
  for (const param of func.params) {
36846
36911
  if (param.is_self_param) continue;
36847
36912
  const param_struct = status.structs.find((s) => s.name === param.type.name);
36848
- if (param.is_moved && param_struct?.is_class && !moved_param_is_consumed(func, param.name, param_struct.name, status.structs)) {
36913
+ if (!param.is_moved) continue;
36914
+ if (param_struct?.is_class && !moved_param_is_consumed(func, param.name, param_struct.name, status.structs)) {
36849
36915
  const pname = c_function_name(param.name);
36850
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);
36851
36923
  }
36852
36924
  }
36853
36925
  const saved_sig = begin_code_scratch(status);
@@ -37734,6 +37806,12 @@ function build_function_node(node, status) {
37734
37806
  if (param_trait && !param_struct?.is_class) decl.trait_class_trait = param.type.name;
37735
37807
  status.scoped_declarations.push(decl);
37736
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
+ }
37737
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);
37738
37816
  const fn_name = node.name;
37739
37817
  const fn_label = emission_label(node);
@@ -41741,7 +41819,9 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41741
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;
41742
41820
  const rhs_is_borrow_reception = !node.swap && size === 16 && is_string_borrow(node.right_value);
41743
41821
  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);
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);
41745
41825
  const move_source_owns_heap = move_source !== void 0 && !!status.heap_strings?.has(move_source);
41746
41826
  if (rhs_is_view_into_owned) {
41747
41827
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
@@ -41751,7 +41831,7 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41751
41831
  if (move_source !== void 0 && move_source_owns_heap) status.heap_strings?.delete(move_source);
41752
41832
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
41753
41833
  status.heap_strings.add(name);
41754
- } else if (rhs_is_field_read) status.last_result_is_heap = false;
41834
+ } else if (rhs_is_field_read || rhs_is_unrecorded_var_read) status.last_result_is_heap = false;
41755
41835
  else if (status.last_result_is_heap) {
41756
41836
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
41757
41837
  status.heap_strings.add(name);
@@ -41969,7 +42049,9 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41969
42049
  get_base_address(access, status, "x0");
41970
42050
  emit_asm(status, `str x0, [sp, #-16]!\n`);
41971
42051
  }
41972
- if (!node.operator && node.right_value.node_type === "value" && node.right_value.is_moved) {
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)) {
41973
42055
  const field_resolved = resolve_struct_type(field_type, status);
41974
42056
  const field_target_var = access.target.node_type === "value" ? access.target.value : "";
41975
42057
  const first_init_write = is_first_init_field_write(field_target_var, field_name, status);
@@ -63202,6 +63284,7 @@ function generate_harness(tests, benches) {
63202
63284
  }).join("\n");
63203
63285
  let main = "\nimport System\nimport System::Test\n";
63204
63286
  main += "\npub func main = () {\n";
63287
+ main += `\tConsole.unbuffered()\n`;
63205
63288
  main += " var Tester t = Tester()\n";
63206
63289
  main += " var int64 audit_mark = 0\n";
63207
63290
  main += " var int64 audit_leaked = 0\n";
@@ -63334,7 +63417,7 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63334
63417
  result.ms = performance.now() - start;
63335
63418
  return result;
63336
63419
  }
63337
- const audit_obj = path.join(buildDir, "audit_runtime.o");
63420
+ const audit_obj = path.join(buildDir, path.basename(entry_path, ".nm") + "_audit.o");
63338
63421
  await execFile_async("clang", [
63339
63422
  "-c",
63340
63423
  runtime_src,
@@ -63345,8 +63428,11 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63345
63428
  const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
63346
63429
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
63347
63430
  const outfile = path.join(buildDir, path.basename(entry_path, ".nm"));
63348
- fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
63349
- fs.writeFileSync(codefile, buildResult.code);
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);
63350
63436
  let companionfile;
63351
63437
  if (buildResult.companion) {
63352
63438
  const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
@@ -63372,20 +63458,48 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63372
63458
  result.ms = performance.now() - start;
63373
63459
  return result;
63374
63460
  }
63461
+ const run_out = outfile + ".out";
63462
+ const run_err = outfile + ".err";
63375
63463
  let runStdout = "";
63464
+ let runStderr = "";
63376
63465
  let crashed;
63466
+ const out_fd = fs.openSync(run_out, "w");
63467
+ const err_fd = fs.openSync(run_err, "w");
63377
63468
  try {
63378
- runStdout = (await execFile_async(outfile, [], {
63379
- encoding: "utf8",
63469
+ const child = spawn(outfile, [], {
63380
63470
  timeout: 3e4,
63381
- maxBuffer: 16777216,
63471
+ stdio: [
63472
+ "ignore",
63473
+ out_fd,
63474
+ err_fd
63475
+ ],
63382
63476
  cwd: cwd ?? process.cwd()
63383
- })).stdout;
63384
- } catch (err) {
63385
- runStdout = err.stdout ? err.stdout.toString() : "";
63386
- if (err.signal === "SIGTERM") crashed = "test binary timed out after 30s";
63387
- else crashed = `test binary exited abnormally (signal ${err.signal ?? err.code})`;
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);
63495
+ }
63496
+ runStdout = fs.readFileSync(run_out, "utf8");
63497
+ try {
63498
+ runStderr = fs.readFileSync(run_err, "utf8");
63499
+ } catch {
63500
+ runStderr = "";
63388
63501
  }
63502
+ if (crashed && runStderr.trim()) crashed += `\n${runStderr.trim()}`;
63389
63503
  const records = parse_records(runStdout);
63390
63504
  result.tests = records.tests;
63391
63505
  result.fails = records.fails;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.6.10",
3
+ "version": "0.6.11",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",
package/src/test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -220,6 +220,11 @@ export function generate_harness(tests: TestFunction[], benches: BenchFunction[]
220
220
  // reports as a leaking test instead of leaving the file's exit-time
221
221
  // total unattributed.
222
222
  main += "\npub func main = () {\n";
223
+ // Unbuffered stdout: a binary that crashes mid-suite still leaves every
224
+ // record it printed (the CLI reads them from the run's .out file), and a
225
+ // crash is diagnosed with the tests that PASSED before it instead of
226
+ // "(crashed before records)".
227
+ main += `\tConsole.unbuffered()\n`;
223
228
  main += "\tvar Tester t = Tester()\n";
224
229
  main += "\tvar int64 audit_mark = 0\n";
225
230
  main += "\tvar int64 audit_leaked = 0\n";
@@ -398,7 +403,11 @@ export async function run_test_file(
398
403
  result.ms = performance.now() - start;
399
404
  return result;
400
405
  }
401
- const audit_obj = path.join(buildDir, "audit_runtime.o");
406
+ // Per-file audit object: two concurrent workers compiling to the SHARED
407
+ // `audit_runtime.o` raced — a link could read the object mid-rewrite
408
+ // (concurrent `clang -c` open the output with O_TRUNC) and fail, or link
409
+ // a stale slab. The runtime is tiny; recompiling per file costs ~0.1s.
410
+ const audit_obj = path.join(buildDir, path.basename(entry_path, ".nm") + "_audit.o");
402
411
  await execFile_async("clang", ["-c", runtime_src, "-o", audit_obj]);
403
412
  const platform = default_platform();
404
413
  // Match the run/build command's emission: ObjC-bearing sources (GUI code
@@ -408,10 +417,19 @@ export async function run_test_file(
408
417
  const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
409
418
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
410
419
  const outfile = path.join(buildDir, path.basename(entry_path, ".nm"));
411
- // The C backend's generated source does `#include "main.h"`, so write the
412
- // header next to the code. (aarch64 inlines everything into the .s file.)
413
- fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
414
- fs.writeFileSync(codefile, buildResult.code);
420
+ // The C backend's generated source does `#include "main.h"`, so the
421
+ // header lands next to the code. (aarch64 inlines everything into the
422
+ // .s file and ignores it.) Per-file header name: every test file shares
423
+ // one build folder, and two concurrent C builds writing the SAME
424
+ // `main.h` raced — a compile could read another file's header between
425
+ // the write and the clang read and fail (or mis-declare) spuriously.
426
+ const header_name = path.basename(entry_path, ".nm") + ".main.h";
427
+ fs.writeFileSync(path.join(buildDir, header_name), buildResult.headers ?? "");
428
+ let code_text: string = buildResult.code;
429
+ if (arch !== "aarch64") {
430
+ code_text = code_text.replace('#include "main.h"', `#include "${header_name}"`);
431
+ }
432
+ fs.writeFileSync(codefile, code_text);
415
433
 
416
434
  // Companion C (raw-block functions + the pool/fiber/file-scope C infra):
417
435
  // `run`/`build` compile and link it next to the main TU. The aarch64 .s
@@ -458,30 +476,63 @@ export async function run_test_file(
458
476
  return result;
459
477
  }
460
478
 
461
- // Run the binary and collect the records it streams over stdout. A
479
+ // Run the binary with its stdout/stderr redirected to per-file FILES
480
+ // (next to the binary, doubling as debugging artifacts) instead of node
481
+ // pipes, then read them. (Why not pipes: this runner's parse/build
482
+ // phases block the event loop for tens of seconds at a time. A binary
483
+ // that finished inside such a block exited 0 long before its records
484
+ // were read, but the 30s execFile timeout that became due during the
485
+ // block still fired at loop-resume — and the kill path destroyed the
486
+ // child's unread pipe, silently dropping EVERY record: the file
487
+ // reported "(0 tests)" and passed. Records on disk are immune to
488
+ // anything the parent does after the fact.) The documented CWD
489
+ // contract: the binary runs in the `--in` root (the folder `nomen test`
490
+ // scanned for *.test.nm files), so fixture paths in tests resolve
491
+ // against that folder regardless of where the CLI was invoked from. A
462
492
  // non-zero exit (crash, abort) still surfaces whatever records were
463
- // emitted before the crash; a timeout is treated as a crash. The
464
- // documented CWD contract: the binary runs in the `--in` root (the
465
- // folder `nomen test` scanned for *.test.nm files), so fixture paths in
466
- // tests resolve against that folder regardless of where the CLI was
467
- // invoked from.
493
+ // emitted before the crash (the harness runs its stdout unbuffered, so
494
+ // a crashing binary keeps the records it printed); a timeout is treated
495
+ // as a crash.
496
+ const run_out = outfile + ".out";
497
+ const run_err = outfile + ".err";
468
498
  let runStdout = "";
499
+ let runStderr = "";
469
500
  let crashed: string | undefined;
501
+ const out_fd = fs.openSync(run_out, "w");
502
+ const err_fd = fs.openSync(run_err, "w");
470
503
  try {
471
- const run = await execFile_async(outfile, [], {
472
- encoding: "utf8",
504
+ const child = spawn(outfile, [], {
473
505
  timeout: 30_000,
474
- maxBuffer: 16 * 1024 * 1024,
506
+ stdio: ["ignore", out_fd, err_fd],
475
507
  cwd: cwd ?? process.cwd(),
476
508
  });
477
- runStdout = run.stdout;
478
- } catch (err: any) {
479
- runStdout = err.stdout ? err.stdout.toString() : "";
480
- if (err.signal === "SIGTERM") {
509
+ const run = await new Promise<{ code: number | null; signal: string | null }>((resolve) => {
510
+ child.on("error", () => resolve({ code: null, signal: null }));
511
+ child.on("close", (code, signal) => resolve({ code, signal }));
512
+ });
513
+ if (run.signal === "SIGTERM") {
481
514
  crashed = "test binary timed out after 30s";
482
- } else {
483
- crashed = `test binary exited abnormally (signal ${err.signal ?? err.code})`;
515
+ } else if (run.signal !== null) {
516
+ crashed = `test binary exited abnormally (signal ${run.signal})`;
517
+ } else if (run.code === null) {
518
+ crashed = "test binary could not be run";
519
+ } else if (run.code !== 0) {
520
+ crashed = `test binary exited abnormally (signal ${run.code})`;
484
521
  }
522
+ } finally {
523
+ fs.closeSync(out_fd);
524
+ fs.closeSync(err_fd);
525
+ }
526
+ runStdout = fs.readFileSync(run_out, "utf8");
527
+ try {
528
+ runStderr = fs.readFileSync(run_err, "utf8");
529
+ } catch {
530
+ runStderr = "";
531
+ }
532
+ if (crashed && runStderr.trim()) {
533
+ // A crashed binary's own diagnosis (a libmalloc abort message, a
534
+ // panic dump) goes to stderr — surface it with the crash record.
535
+ crashed += `\n${runStderr.trim()}`;
485
536
  }
486
537
 
487
538
  const records = parse_records(runStdout);