nomen-lang 0.6.10 → 0.6.12

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
  }
@@ -20867,10 +20884,12 @@ function build_function_node$1(node, status) {
20867
20884
  if (moved_param_save_slots.size > 0 && node.name !== "main") {
20868
20885
  const need_guard = return_is_class;
20869
20886
  const need_save = !!node.return_type?.name;
20887
+ const returns_fat_pair = node.return_type?.name === "string" && !node.return_type.is_array;
20870
20888
  let return_save;
20871
20889
  if (need_guard || need_save) {
20872
- return_save = allocate_stack_space(status, 8);
20873
- emit_asm(status, `str x0, [x29, #${return_save}]\n`);
20890
+ return_save = allocate_stack_space(status, returns_fat_pair ? 16 : 8);
20891
+ if (returns_fat_pair) emit_asm(status, `stp x0, x1, [x29, #${return_save}]\n`);
20892
+ else emit_asm(status, `str x0, [x29, #${return_save}]\n`);
20874
20893
  }
20875
20894
  for (const [name, info] of moved_param_save_slots) {
20876
20895
  if (moved_set?.has(name)) continue;
@@ -20881,12 +20900,25 @@ function build_function_node$1(node, status) {
20881
20900
  emit_asm(status, `cmp x0, x1\n`);
20882
20901
  emit_asm(status, `beq ${keep_prefix}_${name}\n`);
20883
20902
  }
20903
+ if (info.value_struct_destroy_name) {
20904
+ const vstruct = status.structs.find((s) => s.name === info.value_struct_destroy_name);
20905
+ release_heap_string_fields(status, name, info.value_struct_destroy_name);
20906
+ if (vstruct && has_destroy(vstruct)) {
20907
+ emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
20908
+ emit_asm(status, `bl ${info.value_struct_destroy_name}_destroy\n`);
20909
+ } else if (vstruct) emit_field_destroys(status, vstruct, name, void 0, void 0, false);
20910
+ if (need_guard) emit_asm(status, `${keep_prefix}_${name}:\n`);
20911
+ continue;
20912
+ }
20884
20913
  emit_destroy_for_anchor_slot(status, info.offset, info.type_name, info.type_args, info.is_nullable);
20885
20914
  emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
20886
20915
  emit_free(status);
20887
20916
  if (need_guard) emit_asm(status, `${keep_prefix}_${name}:\n`);
20888
20917
  }
20889
- if (need_guard || need_save) emit_asm(status, `ldr x0, [x29, #${return_save}]\n`);
20918
+ if (need_guard || need_save) {
20919
+ if (returns_fat_pair) emit_asm(status, `ldp x0, x1, [x29, #${return_save}]\n`);
20920
+ else emit_asm(status, `ldr x0, [x29, #${return_save}]\n`);
20921
+ }
20890
20922
  }
20891
20923
  const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
20892
20924
  status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
@@ -21091,6 +21123,7 @@ function init_nullable_field_default(node, field, base_reg, status) {
21091
21123
  * sites record it (record_ctor_heap_default_fields) so scope exit frees it.
21092
21124
  */
21093
21125
  function init_expr_field_default(node, field, offset, base_reg, status) {
21126
+ status.last_result_is_heap = false;
21094
21127
  build_node(field.value, status);
21095
21128
  ensure_newline(status);
21096
21129
  const t = field.type;
@@ -22216,7 +22249,10 @@ function build_struct_functions$1(node, status) {
22216
22249
  const moved_param_save_slots = /* @__PURE__ */ new Map();
22217
22250
  for (const param of func.params) {
22218
22251
  if (!param.is_moved || param.is_self_param) continue;
22219
- if (!status.structs.find((s) => s.name === param.type.name && s.is_class)) continue;
22252
+ const is_class = !!status.structs.find((s) => s.name === param.type.name && s.is_class);
22253
+ const vstruct = !is_class ? resolve_struct_type(param.type, status) : void 0;
22254
+ const is_owning_value_struct = !!vstruct && !vstruct.is_class && !param.type.is_nullable && struct_needs_destroy(vstruct, status);
22255
+ if (!is_class && !is_owning_value_struct) continue;
22220
22256
  const reg = status.function_param_regs.get(param.name);
22221
22257
  if (reg) {
22222
22258
  const save_offset = allocate_stack_space(status, 8);
@@ -22225,7 +22261,8 @@ function build_struct_functions$1(node, status) {
22225
22261
  offset: save_offset,
22226
22262
  type_name: param.type.name,
22227
22263
  type_args: param.type.type_args,
22228
- is_nullable: param.type.is_nullable
22264
+ is_nullable: param.type.is_nullable,
22265
+ value_struct_destroy_name: is_owning_value_struct ? vstruct.name : void 0
22229
22266
  });
22230
22267
  } else {
22231
22268
  const offset = status.stack_offsets.get(param.name);
@@ -22233,7 +22270,8 @@ function build_struct_functions$1(node, status) {
22233
22270
  offset,
22234
22271
  type_name: param.type.name,
22235
22272
  type_args: param.type.type_args,
22236
- is_nullable: param.type.is_nullable
22273
+ is_nullable: param.type.is_nullable,
22274
+ value_struct_destroy_name: is_owning_value_struct ? vstruct.name : void 0
22237
22275
  });
22238
22276
  }
22239
22277
  }
@@ -22273,11 +22311,13 @@ function build_struct_functions$1(node, status) {
22273
22311
  if (moved_param_save_slots.size > 0) {
22274
22312
  const ret_is_class = !!func.return_type?.name && !!status.structs.find((s) => s.name === func.return_type.name && s.is_class);
22275
22313
  const need_save = !!func.return_type?.name;
22314
+ const returns_fat_pair = func.return_type?.name === "string" && !func.return_type.is_array;
22276
22315
  const keep_prefix = `.Lkeep_mparam_${func_label.replace(/[^\w]/g, "_")}`;
22277
22316
  let return_save;
22278
22317
  if (ret_is_class || need_save) {
22279
- return_save = allocate_stack_space(status, 8);
22280
- emit_asm(status, `str x0, [x29, #${return_save}]\n`);
22318
+ return_save = allocate_stack_space(status, returns_fat_pair ? 16 : 8);
22319
+ if (returns_fat_pair) emit_asm(status, `stp x0, x1, [x29, #${return_save}]\n`);
22320
+ else emit_asm(status, `str x0, [x29, #${return_save}]\n`);
22281
22321
  }
22282
22322
  for (const [name, info] of moved_param_save_slots) {
22283
22323
  if (status.moved?.has(name) && !moved_before.has(name)) continue;
@@ -22288,12 +22328,25 @@ function build_struct_functions$1(node, status) {
22288
22328
  emit_asm(status, `cmp x0, x1\n`);
22289
22329
  emit_asm(status, `beq ${keep_prefix}_${name}\n`);
22290
22330
  }
22331
+ if (info.value_struct_destroy_name) {
22332
+ const vstruct = status.structs.find((s) => s.name === info.value_struct_destroy_name);
22333
+ release_heap_string_fields(status, name, info.value_struct_destroy_name);
22334
+ if (vstruct && has_destroy(vstruct)) {
22335
+ emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
22336
+ emit_asm(status, `bl ${info.value_struct_destroy_name}_destroy\n`);
22337
+ } else if (vstruct) emit_field_destroys(status, vstruct, name, void 0, void 0, false);
22338
+ if (ret_is_class) emit_asm(status, `${keep_prefix}_${name}:\n`);
22339
+ continue;
22340
+ }
22291
22341
  emit_destroy_for_anchor_slot(status, info.offset, info.type_name, info.type_args, info.is_nullable);
22292
22342
  emit_asm(status, `ldr x0, [x29, #${info.offset}]\n`);
22293
22343
  emit_free(status);
22294
22344
  if (ret_is_class) emit_asm(status, `${keep_prefix}_${name}:\n`);
22295
22345
  }
22296
- if (ret_is_class || need_save) emit_asm(status, `ldr x0, [x29, #${return_save}]\n`);
22346
+ if (ret_is_class || need_save) {
22347
+ if (returns_fat_pair) emit_asm(status, `ldp x0, x1, [x29, #${return_save}]\n`);
22348
+ else emit_asm(status, `ldr x0, [x29, #${return_save}]\n`);
22349
+ }
22297
22350
  }
22298
22351
  const total_stack = Math.ceil((status.stack_size || 0) / 16) * 16;
22299
22352
  status.code = status.code.replace(`sub sp, sp, #${stack_placeholder}`, total_stack > 0 ? `sub sp, sp, #${total_stack}` : `// no stack needed`);
@@ -22733,6 +22786,7 @@ function build_block_node$1(node, status) {
22733
22786
  const prologue = status.module_init_statements;
22734
22787
  status.module_init_statements = void 0;
22735
22788
  for (const stmt of prologue) {
22789
+ status.last_result_is_heap = false;
22736
22790
  emit_allocations$1(stmt, status);
22737
22791
  emit_stmt_from_nir$1(stmt, prologue.indexOf(stmt), prologue, status);
22738
22792
  }
@@ -22742,6 +22796,7 @@ function build_block_node$1(node, status) {
22742
22796
  if (module_init_set.has(child)) continue;
22743
22797
  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
22798
  if (child.node_type !== "while") emit_allocations$1(child, status);
22799
+ status.last_result_is_heap = false;
22745
22800
  const prev_forwarded = status.forwarded_param_inits;
22746
22801
  const consumed = emit_stmt_from_nir$1(child, index, node.statements, status);
22747
22802
  index += consumed - 1;
@@ -23918,7 +23973,6 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23918
23973
  }
23919
23974
  hoist_field_overrides(node.value, build_node, status, "", node.name);
23920
23975
  status.last_result_is_heap = false;
23921
- const prev_heap = status.last_result_is_heap;
23922
23976
  if (node.value?.node_type === "value") {
23923
23977
  const inlined = status.top_level_consts?.get(node.value.value);
23924
23978
  if (inlined?.value) node.value = fold_string_const(inlined, status.top_level_consts) ?? inlined.value;
@@ -23940,7 +23994,6 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
23940
23994
  consolidate_temp_anchors$1(status, node.value, node.type.name);
23941
23995
  }
23942
23996
  }
23943
- status.last_result_is_heap = prev_heap;
23944
23997
  }
23945
23998
  if (node.func_params) {
23946
23999
  if (status.function_return_label) {
@@ -24828,8 +24881,12 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
24828
24881
  emit_init_value$1(node.value, nir_init, status);
24829
24882
  ensure_newline(status);
24830
24883
  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);
24884
+ if (size === 16 && node.type.name === "string") {
24885
+ const force_heap_init = (status.force_heap_strings?.has(node.name) ?? false) && raw !== "null";
24886
+ if (force_heap_init) emit_strdup_string(status);
24887
+ emit_pair_store_x29(status, offset);
24888
+ if (force_heap_init) mark_heap_string(status, node.name);
24889
+ } else emit_var_store(status, "x0", node.name, size);
24833
24890
  } else if (is_float_type$1(node.type.name)) {
24834
24891
  const label = `_float_const_${decl_const_counter++}`;
24835
24892
  emit_data(status, `${label}: .double ${raw}\n.p2align 2\n`);
@@ -31099,6 +31156,10 @@ function build_auto_free(status) {
31099
31156
  function free_scoped_declarations(status, decls, persist_string_field_records = false) {
31100
31157
  let commented = false;
31101
31158
  if (status.heap_string_fields?.size) for (const dec of decls) {
31159
+ if (dec.moved_value_struct_param) {
31160
+ const dec_struct = status.structs.find((s) => s.name === mono_type_name(dec.type) && !s.is_simple_type && !s.is_generic);
31161
+ if (dec_struct && has_destroy(dec_struct)) continue;
31162
+ }
31102
31163
  const prefix = `${dec.name}.`;
31103
31164
  const keys = Array.from(status.heap_string_fields).filter((key) => key.startsWith(prefix));
31104
31165
  if (!keys.length) continue;
@@ -31130,6 +31191,18 @@ function free_scoped_declarations(status, decls, persist_string_field_records =
31130
31191
  }
31131
31192
  if (persist_string_field_records && dec.force_owned_string === true) continue;
31132
31193
  if (status.moved?.has(dec.name)) continue;
31194
+ if (dec.moved_value_struct_param) {
31195
+ const mono_name = mono_type_name(dec.type);
31196
+ const param_struct = status.structs.find((s) => s.name === mono_name && !s.is_simple_type && !s.is_generic);
31197
+ if (param_struct && struct_needs_destroy(param_struct, status)) {
31198
+ if (!commented) {
31199
+ status.code += "\n// Auto-free\n";
31200
+ commented = true;
31201
+ }
31202
+ emit_struct_destroys(status, param_struct, `(*${c_function_name(dec.name)})`);
31203
+ }
31204
+ continue;
31205
+ }
31133
31206
  const cname = c_function_name(dec.name);
31134
31207
  if (dec.type.name === "func") {
31135
31208
  if (!commented) {
@@ -33620,13 +33693,15 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
33620
33693
  }
33621
33694
  }
33622
33695
  }
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) {
33696
+ const assign_rhs_is_moved_value = node.right_value.node_type === "value" && node.right_value.is_moved;
33697
+ const assign_rhs_is_fresh_call = !node.swap && node.right_value.node_type === "func_call";
33698
+ 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
33699
  const access_lhs = node.left_value;
33625
33700
  const field_type = access_lhs.access.type;
33626
33701
  const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
33627
33702
  const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
33628
33703
  if (field_struct) {
33629
- splice_decl_from_c_scopes(status, node.right_value.value);
33704
+ if (assign_rhs_is_moved_value) splice_decl_from_c_scopes(status, node.right_value.value);
33630
33705
  const saved_len = begin_code_scratch(status);
33631
33706
  build_node$1(node.left_value, status);
33632
33707
  const field_access = end_code_scratch(status, saved_len);
@@ -36845,9 +36920,16 @@ function build_struct_functions(node, status, skip_init = false) {
36845
36920
  for (const param of func.params) {
36846
36921
  if (param.is_self_param) continue;
36847
36922
  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)) {
36923
+ if (!param.is_moved) continue;
36924
+ if (param_struct?.is_class && !moved_param_is_consumed(func, param.name, param_struct.name, status.structs)) {
36849
36925
  const pname = c_function_name(param.name);
36850
36926
  status.scoped_declarations.push(new DeclarationNode(param.start, "private", "move", pname, param.type));
36927
+ } 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)) {
36928
+ const pname = c_function_name(param.name);
36929
+ const decl = new DeclarationNode(param.start, "private", "move", pname, param.type);
36930
+ decl.moved_value_struct_param = true;
36931
+ decl.string_fields_via_pointer = true;
36932
+ status.scoped_declarations.push(decl);
36851
36933
  }
36852
36934
  }
36853
36935
  const saved_sig = begin_code_scratch(status);
@@ -37734,6 +37816,12 @@ function build_function_node(node, status) {
37734
37816
  if (param_trait && !param_struct?.is_class) decl.trait_class_trait = param.type.name;
37735
37817
  status.scoped_declarations.push(decl);
37736
37818
  }
37819
+ 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)) {
37820
+ const decl = new DeclarationNode(param.start, "private", "move", pname, param.type);
37821
+ decl.moved_value_struct_param = true;
37822
+ decl.string_fields_via_pointer = true;
37823
+ status.scoped_declarations.push(decl);
37824
+ }
37737
37825
  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
37826
  const fn_name = node.name;
37739
37827
  const fn_label = emission_label(node);
@@ -41741,7 +41829,9 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41741
41829
  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
41830
  const rhs_is_borrow_reception = !node.swap && size === 16 && is_string_borrow(node.right_value);
41743
41831
  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);
41832
+ 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;
41833
+ const rhs_is_unrecorded_var_read = rhs_var_name !== void 0 && !status.heap_strings?.has(rhs_var_name);
41834
+ const borrow_needs_dup = (rhs_is_borrow_reception || rhs_is_field_read || rhs_is_unrecorded_var_read) && !!status.force_heap_strings?.has(name);
41745
41835
  const move_source_owns_heap = move_source !== void 0 && !!status.heap_strings?.has(move_source);
41746
41836
  if (rhs_is_view_into_owned) {
41747
41837
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
@@ -41751,7 +41841,7 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41751
41841
  if (move_source !== void 0 && move_source_owns_heap) status.heap_strings?.delete(move_source);
41752
41842
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
41753
41843
  status.heap_strings.add(name);
41754
- } else if (rhs_is_field_read) status.last_result_is_heap = false;
41844
+ } else if (rhs_is_field_read || rhs_is_unrecorded_var_read) status.last_result_is_heap = false;
41755
41845
  else if (status.last_result_is_heap) {
41756
41846
  if (!status.heap_strings) status.heap_strings = /* @__PURE__ */ new Set();
41757
41847
  status.heap_strings.add(name);
@@ -41969,7 +42059,9 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
41969
42059
  get_base_address(access, status, "x0");
41970
42060
  emit_asm(status, `str x0, [sp, #-16]!\n`);
41971
42061
  }
41972
- if (!node.operator && node.right_value.node_type === "value" && node.right_value.is_moved) {
42062
+ const field_rhs_is_moved_value = node.right_value.node_type === "value" && node.right_value.is_moved;
42063
+ const field_rhs_is_fresh_call = !node.swap && node.right_value.node_type === "func_call";
42064
+ if (!node.operator && (field_rhs_is_moved_value || field_rhs_is_fresh_call)) {
41973
42065
  const field_resolved = resolve_struct_type(field_type, status);
41974
42066
  const field_target_var = access.target.node_type === "value" ? access.target.value : "";
41975
42067
  const first_init_write = is_first_init_field_write(field_target_var, field_name, status);
@@ -63202,6 +63294,7 @@ function generate_harness(tests, benches) {
63202
63294
  }).join("\n");
63203
63295
  let main = "\nimport System\nimport System::Test\n";
63204
63296
  main += "\npub func main = () {\n";
63297
+ main += `\tConsole.unbuffered()\n`;
63205
63298
  main += " var Tester t = Tester()\n";
63206
63299
  main += " var int64 audit_mark = 0\n";
63207
63300
  main += " var int64 audit_leaked = 0\n";
@@ -63334,7 +63427,7 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63334
63427
  result.ms = performance.now() - start;
63335
63428
  return result;
63336
63429
  }
63337
- const audit_obj = path.join(buildDir, "audit_runtime.o");
63430
+ const audit_obj = path.join(buildDir, path.basename(entry_path, ".nm") + "_audit.o");
63338
63431
  await execFile_async("clang", [
63339
63432
  "-c",
63340
63433
  runtime_src,
@@ -63345,8 +63438,11 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63345
63438
  const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
63346
63439
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
63347
63440
  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);
63441
+ const header_name = path.basename(entry_path, ".nm") + ".main.h";
63442
+ fs.writeFileSync(path.join(buildDir, header_name), buildResult.headers ?? "");
63443
+ let code_text = buildResult.code;
63444
+ if (arch !== "aarch64") code_text = code_text.replace("#include \"main.h\"", `#include "${header_name}"`);
63445
+ fs.writeFileSync(codefile, code_text);
63350
63446
  let companionfile;
63351
63447
  if (buildResult.companion) {
63352
63448
  const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
@@ -63372,20 +63468,48 @@ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_ru
63372
63468
  result.ms = performance.now() - start;
63373
63469
  return result;
63374
63470
  }
63471
+ const run_out = outfile + ".out";
63472
+ const run_err = outfile + ".err";
63375
63473
  let runStdout = "";
63474
+ let runStderr = "";
63376
63475
  let crashed;
63476
+ const out_fd = fs.openSync(run_out, "w");
63477
+ const err_fd = fs.openSync(run_err, "w");
63377
63478
  try {
63378
- runStdout = (await execFile_async(outfile, [], {
63379
- encoding: "utf8",
63479
+ const child = spawn(outfile, [], {
63380
63480
  timeout: 3e4,
63381
- maxBuffer: 16777216,
63481
+ stdio: [
63482
+ "ignore",
63483
+ out_fd,
63484
+ err_fd
63485
+ ],
63382
63486
  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})`;
63487
+ });
63488
+ const run = await new Promise((resolve) => {
63489
+ child.on("error", () => resolve({
63490
+ code: null,
63491
+ signal: null
63492
+ }));
63493
+ child.on("close", (code, signal) => resolve({
63494
+ code,
63495
+ signal
63496
+ }));
63497
+ });
63498
+ if (run.signal === "SIGTERM") crashed = "test binary timed out after 30s";
63499
+ else if (run.signal !== null) crashed = `test binary exited abnormally (signal ${run.signal})`;
63500
+ else if (run.code === null) crashed = "test binary could not be run";
63501
+ else if (run.code !== 0) crashed = `test binary exited abnormally (signal ${run.code})`;
63502
+ } finally {
63503
+ fs.closeSync(out_fd);
63504
+ fs.closeSync(err_fd);
63505
+ }
63506
+ runStdout = fs.readFileSync(run_out, "utf8");
63507
+ try {
63508
+ runStderr = fs.readFileSync(run_err, "utf8");
63509
+ } catch {
63510
+ runStderr = "";
63388
63511
  }
63512
+ if (crashed && runStderr.trim()) crashed += `\n${runStderr.trim()}`;
63389
63513
  const records = parse_records(runStdout);
63390
63514
  result.tests = records.tests;
63391
63515
  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.12",
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);