nomen-lang 0.6.8 → 0.6.9

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.
@@ -0,0 +1,50 @@
1
+ #include <stdio.h>
2
+ #include <stdlib.h>
3
+ #include <string.h>
4
+
5
+ // Atomic: worker threads from the pool malloc/free concurrently, and a
6
+ // plain `long` would lose updates under that contention (showing up as
7
+ // spurious LEAK/negative-count failures in tests that use spawns).
8
+ static volatile long nomen_malloc_count = 0;
9
+
10
+ void *nomen_malloc_wrap(unsigned long size) {
11
+ void *ptr = malloc(size);
12
+ __atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
13
+ return ptr;
14
+ }
15
+
16
+ void *nomen_calloc_wrap(unsigned long count, unsigned long size) {
17
+ void *ptr = calloc(count, size);
18
+ __atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
19
+ return ptr;
20
+ }
21
+
22
+ void *nomen_realloc_wrap(void *old_ptr, unsigned long size) {
23
+ if (!old_ptr) {
24
+ __atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
25
+ }
26
+ void *ptr = realloc(old_ptr, size);
27
+ return ptr;
28
+ }
29
+
30
+ void nomen_free_wrap(void *ptr) {
31
+ if (ptr) {
32
+ free(ptr);
33
+ __atomic_sub_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
34
+ }
35
+ }
36
+
37
+ void *nomen_strdup_wrap(const char *s) {
38
+ unsigned long len = strlen(s) + 1;
39
+ void *ptr = malloc(len);
40
+ if (ptr) memcpy(ptr, s, len);
41
+ __atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
42
+ return ptr;
43
+ }
44
+
45
+ void nomen_audit_check(void) {
46
+ long count = __atomic_load_n(&nomen_malloc_count, __ATOMIC_SEQ_CST);
47
+ if (count != 0) {
48
+ printf("LEAK: %ld allocation(s)\n", count);
49
+ }
50
+ }
@@ -241,7 +241,10 @@ pub struct StringBuilder {
241
241
  }
242
242
 
243
243
  // Finalize: null-terminate, return the byte buffer as a string, and
244
- // release ownership (sets `data` to 0 so the destructor won't free it).
244
+ // release ownership (sets `data` to 0 so the destructor won't free it,
245
+ // and resets `len`/`cap` so the builder is a valid empty builder again —
246
+ // a stale `cap` would let a later append skip the grow and memcpy to a
247
+ // null buffer). Reuse after `to_string` yields a fresh builder.
245
248
  // Explicitly `pub`: struct members default to pub, but this is the
246
249
  // builder's primary read-back — the contract should not depend on the
247
250
  // default.
@@ -260,6 +263,8 @@ pub struct StringBuilder {
260
263
  ((unsigned char*)(unsigned long long)self->data)[idx] = 0;
261
264
  unsigned long long result = self->data;
262
265
  self->data = 0;
266
+ self->len = 0;
267
+ self->cap = 0;
263
268
  return (nomen_string){ (char*)result, (long)idx };
264
269
  ```
265
270
  ```
@@ -290,6 +295,10 @@ pub struct StringBuilder {
290
295
  strb wzr, [x0, x1] // data[len] = 0
291
296
  // transfer ownership: clear self->data. x0 retains the old ptr.
292
297
  str xzr, [x19, #8]
298
+ // reset the stale bookkeeping so reuse yields a valid empty builder
299
+ // (a stale cap would skip the append's grow against the null buffer)
300
+ str xzr, [x19, #16]
301
+ str xzr, [x19, #24]
293
302
  // Fat-string ABI: (x0 = data, x1 = len) is already the return pair.
294
303
  ```
295
304
  }
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",
@@ -15134,6 +15135,33 @@ function emit_destroy_for_scope(status, declarations_before) {
15134
15135
  emit_asm(status, `ldr x19, [sp], #16\n`);
15135
15136
  continue;
15136
15137
  }
15138
+ if (status.heap_array_vars?.has(decl.name)) {
15139
+ if (status.heap_owned_string_arrays?.has(decl.name)) {
15140
+ emit_asm(status, `str x19, [sp, #-16]!\n`);
15141
+ emit_asm(status, `str x20, [sp, #-16]!\n`);
15142
+ emit_var_load(status, "x0", decl.name, 8);
15143
+ emit_asm(status, `mov x19, x0\n`);
15144
+ emit_asm(status, `ldr x20, [x19]\n`);
15145
+ emit_asm(status, `add x19, x19, #8\n`);
15146
+ const loop = `.Lhosa_${decl.name}`;
15147
+ emit_asm(status, `${loop}:\n`);
15148
+ emit_asm(status, `cbz x20, .Lhosa_done_${decl.name}\n`);
15149
+ emit_asm(status, `ldr x0, [x19]\n`);
15150
+ emit_free(status);
15151
+ emit_asm(status, `add x19, x19, #16\n`);
15152
+ emit_asm(status, `sub x20, x20, #1\n`);
15153
+ emit_asm(status, `b ${loop}\n`);
15154
+ emit_asm(status, `.Lhosa_done_${decl.name}:\n`);
15155
+ emit_var_load(status, "x0", decl.name, 8);
15156
+ emit_free(status);
15157
+ emit_asm(status, `ldr x20, [sp], #16\n`);
15158
+ emit_asm(status, `ldr x19, [sp], #16\n`);
15159
+ continue;
15160
+ }
15161
+ emit_var_load(status, "x0", decl.name, 8);
15162
+ emit_free(status);
15163
+ continue;
15164
+ }
15137
15165
  if (status.heap_strings?.has(decl.name)) {
15138
15166
  emit_var_load(status, "x0", decl.name, 8);
15139
15167
  emit_free(status);
@@ -33540,7 +33568,8 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
33540
33568
  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
33569
  const access_lhs = node.left_value;
33542
33570
  const field_type = access_lhs.access.type;
33543
- const field_struct = field_type?.name && !field_type.is_view && !field_type.is_array ? status.structs.find((s) => s.name === field_type.name && !s.is_simple_type && !s.is_class) : void 0;
33571
+ const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
33572
+ const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
33544
33573
  if (field_struct) {
33545
33574
  splice_decl_from_c_scopes(status, node.right_value.value);
33546
33575
  const saved_len = begin_code_scratch(status);
@@ -39017,10 +39046,31 @@ function build_access_node$1(node, status) {
39017
39046
  status.code += `${ret_c} ${ctor_temp_val} = `;
39018
39047
  }
39019
39048
  }
39049
+ 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);
39050
+ let owned_recv_temp;
39051
+ let owned_recv_val;
39052
+ if (owned_string_recv) {
39053
+ const id = status.label_counter = (status.label_counter ?? 0) + 1;
39054
+ owned_recv_temp = `_osrecv_${id}`;
39055
+ status.code += `({ nomen_string ${owned_recv_temp} = `;
39056
+ {
39057
+ const saved_suppress = status.suppress_dereference;
39058
+ status.suppress_dereference = true;
39059
+ build_node$1(node.target, status);
39060
+ status.suppress_dereference = saved_suppress;
39061
+ }
39062
+ status.code += `; `;
39063
+ const recv_ret_c = c_return_type((target_method ?? trait_default_func)?.return_type ?? access_func.type, status);
39064
+ if (recv_ret_c !== "void") {
39065
+ owned_recv_val = `_osrecvv_${id}`;
39066
+ status.code += `${recv_ret_c} ${owned_recv_val} = `;
39067
+ }
39068
+ }
39020
39069
  if (lambda_ret_tmp) status.code += `${lambda_ret_c} ${lambda_ret_tmp} = `;
39021
39070
  status.code += `${label}(`;
39022
39071
  if (!access_func.is_static) {
39023
39072
  if (ctor_temp_free) status.code += ctor_temp_free;
39073
+ else if (owned_recv_temp) status.code += owned_recv_temp;
39024
39074
  else if (array_wrap) status.code += array_wrap.self_expr;
39025
39075
  else {
39026
39076
  const method_self_is_ref = !!target_method?.params?.some((p) => p.is_self_param && (p.is_ref || p.type?.is_ref));
@@ -39105,6 +39155,11 @@ function build_access_node$1(node, status) {
39105
39155
  if (ctor_temp_val) status.code += ` ${ctor_temp_val};`;
39106
39156
  status.code += ` })`;
39107
39157
  }
39158
+ if (owned_recv_temp) {
39159
+ status.code += `; ${status.audit ? "nomen_free_wrap" : "free"}(${owned_recv_temp}.ptr);`;
39160
+ if (owned_recv_val) status.code += ` ${owned_recv_val};`;
39161
+ status.code += ` })`;
39162
+ }
39108
39163
  if (lambda_arg_temps.size > 0) {
39109
39164
  for (const tmp of lambda_arg_temps.values()) status.code += `; ${closure_dispose_arm(tmp)}`;
39110
39165
  if (lambda_ret_tmp) status.code += `; ${lambda_ret_tmp}`;
@@ -45635,7 +45690,7 @@ function build_access_method(node, access_func, status) {
45635
45690
  status.last_result_is_heap = false;
45636
45691
  build_node(node.target, status);
45637
45692
  ensure_newline(status);
45638
- frees_string_receiver = method_name === "string_to_string" && status.last_result_is_heap;
45693
+ frees_string_receiver = status.last_result_is_heap;
45639
45694
  status.last_result_is_heap = false;
45640
45695
  emit_asm(status, `stp x0, x1, [sp, #-16]!\n`);
45641
45696
  }
@@ -62443,7 +62498,9 @@ const STRING_OPTIONS = /* @__PURE__ */ new Map([
62443
62498
  ["p", "platform"],
62444
62499
  ["lib", "lib"],
62445
62500
  ["l", "lib"],
62446
- ["audit-runtime", "audit_runtime"]
62501
+ ["audit-runtime", "audit_runtime"],
62502
+ ["jobs", "jobs"],
62503
+ ["j", "jobs"]
62447
62504
  ]);
62448
62505
  const BOOLEAN_OPTIONS = /* @__PURE__ */ new Map([
62449
62506
  ["watch", "watch"],
@@ -62553,8 +62610,9 @@ function print_help() {
62553
62610
  " --platform, -p <name> Target platform: macos, ios, linux, android, windows, web",
62554
62611
  " --lib, -l <path> Path to System library directory (containing package.jsonc)",
62555
62612
  " --audit Audit the generated program for memory issues",
62556
- " --audit-runtime <path> Path to audit_runtime.c, linked in when --audit is set",
62557
- " --release, -r Build with optimizations (clang -O2; asm passes on aarch64)",
62613
+ " --audit-runtime <path> Path to audit_runtime.c (default: auto-discovered — walk-up or the CLI-bundled copy)",
62614
+ " --jobs, -j <n> For `nomen test`: max test files run in parallel (default: CPU count)",
62615
+ " --release, -r Build with optimizations (clang -O2; aarch64 asm cleanup passes)",
62558
62616
  " --fast-math Allow FP reassociation (float loop reductions vectorize on aarch64)",
62559
62617
  " --check For `nomen format`: report files that would change",
62560
62618
  " -h, --help Show this help"
@@ -62902,6 +62960,9 @@ function find_bundled(filename) {
62902
62960
  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
62961
  for (const c of candidates) if (fs.existsSync(c)) return path.resolve(c);
62904
62962
  }
62963
+ function find_bundled_audit_runtime() {
62964
+ return find_bundled("src/audit_runtime.c") ?? find_bundled("audit_runtime.c");
62965
+ }
62905
62966
  /**
62906
62967
  * `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
62907
62968
  * starting layout (package.jsonc, src/main.nm, a starter test, README and an
@@ -62980,6 +63041,7 @@ function outfile_for(input_path, is_test, out) {
62980
63041
  //#endregion
62981
63042
  //#region src/test.ts
62982
63043
  const RECORD_PREFIX = "\\nomen|";
63044
+ const execFile_async = promisify(execFile);
62983
63045
  /** Recursively collect `*.test.nm` files under `root`, skipping build output. */
62984
63046
  function collect_test_files(root) {
62985
63047
  const out = [];
@@ -63129,7 +63191,7 @@ function extract_leaks(stdout) {
63129
63191
  * Compile one `*.test.nm` file (with the generated harness) and run it,
63130
63192
  * returning its parsed records and any crash info.
63131
63193
  */
63132
- function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
63194
+ async function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
63133
63195
  const start = performance.now();
63134
63196
  const source_text = fs.readFileSync(entry_path, "utf8");
63135
63197
  const harness = generate_harness(extract_test_functions(source_text), extract_bench_functions(source_text));
@@ -63170,7 +63232,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63170
63232
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
63171
63233
  let audit_obj;
63172
63234
  if (audit) {
63173
- const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved));
63235
+ const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime();
63174
63236
  if (!runtime_src || !fs.existsSync(runtime_src)) {
63175
63237
  result.ok = false;
63176
63238
  result.phase = "setup";
@@ -63179,7 +63241,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63179
63241
  return result;
63180
63242
  }
63181
63243
  audit_obj = path.join(buildDir, "audit_runtime.o");
63182
- execFileSync("clang", [
63244
+ await execFile_async("clang", [
63183
63245
  "-c",
63184
63246
  runtime_src,
63185
63247
  "-o",
@@ -63208,14 +63270,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63208
63270
  if (audit_obj) link_args.push(audit_obj);
63209
63271
  if (platform === "macos" || platform === "ios") link_args.push("-framework", "CoreGraphics", "-framework", "Foundation", "-framework", "AppKit", "-lobjc");
63210
63272
  try {
63211
- execFileSync("clang", link_args, {
63212
- encoding: "utf8",
63213
- stdio: [
63214
- "ignore",
63215
- "pipe",
63216
- "pipe"
63217
- ]
63218
- });
63273
+ await execFile_async("clang", link_args, { encoding: "utf8" });
63219
63274
  } catch (err) {
63220
63275
  const stderr = err.stderr ? err.stderr.toString() : err.message ?? "";
63221
63276
  result.ok = false;
@@ -63227,17 +63282,12 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
63227
63282
  let runStdout = "";
63228
63283
  let crashed;
63229
63284
  try {
63230
- runStdout = execFileSync(outfile, [], {
63285
+ runStdout = (await execFile_async(outfile, [], {
63231
63286
  encoding: "utf8",
63232
- stdio: [
63233
- "ignore",
63234
- "pipe",
63235
- "pipe"
63236
- ],
63237
63287
  timeout: 3e4,
63238
63288
  maxBuffer: 16777216,
63239
63289
  cwd: cwd ?? process.cwd()
63240
- });
63290
+ })).stdout;
63241
63291
  } catch (err) {
63242
63292
  runStdout = err.stdout ? err.stdout.toString() : "";
63243
63293
  if (err.signal === "SIGTERM") crashed = "test binary timed out after 30s";
@@ -63296,10 +63346,16 @@ function report_file(result) {
63296
63346
  for (const line of result.other) console.log(C.dim(` ${line}`));
63297
63347
  }
63298
63348
  }
63349
+ /** The default concurrency for `runTests` when no `jobs` is given. */
63350
+ function default_jobs() {
63351
+ const n = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
63352
+ return Math.max(1, n);
63353
+ }
63299
63354
  /**
63300
63355
  * Locate audit_runtime.c for an audited test build: an explicit path wins,
63301
63356
  * otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
63302
- * convention the run/build commands use).
63357
+ * convention the run/build commands use). When both miss, the caller falls
63358
+ * back to the CLI-bundled runtime (`find_bundled_audit_runtime`).
63303
63359
  */
63304
63360
  function find_audit_runtime(dir) {
63305
63361
  let cur = dir;
@@ -63312,19 +63368,25 @@ function find_audit_runtime(dir) {
63312
63368
  }
63313
63369
  }
63314
63370
  /** Discover, run, and report every `*.test.nm` under `root`. */
63315
- function runTests(root, options = {}) {
63371
+ async function runTests(root, options = {}) {
63316
63372
  const arch = options.arch ?? "aarch64";
63317
63373
  const lib = resolve_lib_for(root);
63318
63374
  const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
63319
63375
  console.log(`Found ${files.length} test file(s)\n`);
63320
63376
  const startTime = performance.now();
63321
63377
  const binary_cwd = path.resolve(root);
63322
- const results = [];
63323
- for (const file of files) {
63324
- const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
63325
- report_file(result);
63326
- results.push(result);
63327
- }
63378
+ const jobs = Math.max(1, Math.min(options.jobs ?? default_jobs(), files.length));
63379
+ const results = new Array(files.length);
63380
+ let next = 0;
63381
+ const worker = async () => {
63382
+ for (;;) {
63383
+ const i = next++;
63384
+ if (i >= files.length) return;
63385
+ results[i] = await run_test_file(files[i], lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
63386
+ }
63387
+ };
63388
+ await Promise.all(Array.from({ length: jobs }, worker));
63389
+ for (const result of results) report_file(result);
63328
63390
  const elapsed = performance.now() - startTime;
63329
63391
  const totalFiles = results.length;
63330
63392
  const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
@@ -63425,12 +63487,13 @@ try {
63425
63487
  if (command === "test") {
63426
63488
  const root = args.in ?? process.cwd();
63427
63489
  const filter = args.filter ? new RegExp(args.filter) : void 0;
63428
- const ok = runTests(root, {
63490
+ const ok = await runTests(root, {
63429
63491
  arch: args.arch ?? "aarch64",
63430
63492
  filter,
63431
63493
  audit: args.audit,
63432
63494
  audit_runtime: args.audit_runtime,
63433
- release: args.release
63495
+ release: args.release,
63496
+ jobs: args.jobs !== void 0 ? parseInt(args.jobs, 10) : void 0
63434
63497
  });
63435
63498
  process.exit(ok ? 0 : 1);
63436
63499
  }
@@ -63541,6 +63604,7 @@ function resolve_audit_runtime(config, input_path) {
63541
63604
  if (parent === dir) break;
63542
63605
  dir = parent;
63543
63606
  }
63607
+ return find_bundled_audit_runtime();
63544
63608
  }
63545
63609
  function compile_audit_runtime(config, input_path, buildDir) {
63546
63610
  const runtime_src = resolve_audit_runtime(config, input_path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",
@@ -17,7 +17,8 @@
17
17
  "dist",
18
18
  "src",
19
19
  "core",
20
- "NOMEN_AGENTS.md"
20
+ "NOMEN_AGENTS.md",
21
+ "audit_runtime.c"
21
22
  ],
22
23
  "type": "module",
23
24
  "devDependencies": {
package/src/args.ts CHANGED
@@ -13,6 +13,8 @@ export interface Args {
13
13
  audit_runtime?: string;
14
14
  check: boolean;
15
15
  release: boolean;
16
+ /** `nomen test` concurrency (max test files in flight); default = CPU count. */
17
+ jobs?: string;
16
18
  /** Opt in to FP reassociation: float reductions vectorize under NEON
17
19
  * (results may differ in the last ulp vs the scalar loop). */
18
20
  fast_math: boolean;
@@ -39,6 +41,8 @@ const STRING_OPTIONS: Map<string, string> = new Map([
39
41
  ["lib", "lib"],
40
42
  ["l", "lib"],
41
43
  ["audit-runtime", "audit_runtime"],
44
+ ["jobs", "jobs"],
45
+ ["j", "jobs"],
42
46
  ]);
43
47
 
44
48
  const BOOLEAN_OPTIONS: Map<string, string> = new Map([
@@ -174,8 +178,9 @@ export function print_help(): void {
174
178
  " --platform, -p <name> Target platform: macos, ios, linux, android, windows, web",
175
179
  " --lib, -l <path> Path to System library directory (containing package.jsonc)",
176
180
  " --audit Audit the generated program for memory issues",
177
- " --audit-runtime <path> Path to audit_runtime.c, linked in when --audit is set",
178
- " --release, -r Build with optimizations (clang -O2; asm passes on aarch64)",
181
+ " --audit-runtime <path> Path to audit_runtime.c (default: auto-discovered — walk-up or the CLI-bundled copy)",
182
+ " --jobs, -j <n> For `nomen test`: max test files run in parallel (default: CPU count)",
183
+ " --release, -r Build with optimizations (clang -O2; aarch64 asm cleanup passes)",
179
184
  " --fast-math Allow FP reassociation (float loop reductions vectorize on aarch64)",
180
185
  " --check For `nomen format`: report files that would change",
181
186
  " -h, --help Show this help",
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@ import parse from "../../src/parse.ts";
13
13
  import { parse_args, print_help, type Args } from "./args.ts";
14
14
  import { run_docs } from "./docs.ts";
15
15
  import render_errors, { render_warnings } from "./format_errors.ts";
16
- import { find_bundled, run_init } from "./init.ts";
16
+ import { find_bundled, find_bundled_audit_runtime, run_init } from "./init.ts";
17
17
  import { build_dir_for, outfile_for } from "./paths.ts";
18
18
  import { runTests } from "./test.ts";
19
19
  import type Config from "./types/Config.ts";
@@ -104,12 +104,13 @@ try {
104
104
  const root = args.in ?? process.cwd();
105
105
  const filter = args.filter ? new RegExp(args.filter) : undefined;
106
106
  const arch = args.arch ?? "aarch64";
107
- const ok = runTests(root, {
107
+ const ok = await runTests(root, {
108
108
  arch,
109
109
  filter,
110
110
  audit: args.audit,
111
111
  audit_runtime: args.audit_runtime,
112
112
  release: args.release,
113
+ jobs: args.jobs !== undefined ? parseInt(args.jobs, 10) : undefined,
113
114
  });
114
115
  process.exit(ok ? 0 : 1);
115
116
  }
@@ -283,7 +284,10 @@ function resolve_audit_runtime(config: Config, input_path: string): string | und
283
284
  if (parent === dir) break;
284
285
  dir = parent;
285
286
  }
286
- return undefined;
287
+ // Neither the config nor the walk-up found it: fall back to the runtime
288
+ // bundled with the CLI (live repo copy in the dev tree, shipped copy when
289
+ // published) so `--audit` works outside the nomen checkout with no flag.
290
+ return find_bundled_audit_runtime();
287
291
  }
288
292
 
289
293
  function compile_audit_runtime(config: Config, input_path: string, buildDir: string): string {
package/src/init.ts CHANGED
@@ -85,6 +85,16 @@ export function find_bundled(filename: string): string | undefined {
85
85
  return undefined;
86
86
  }
87
87
 
88
+ // The audit runtime for `--audit` builds: the dev tree's LIVE copy
89
+ // (repo_root/src/audit_runtime.c — a stale bundled snapshot must not shadow
90
+ // it, same posture as the cli/core rule above), else the copy shipped at the
91
+ // package root by `scripts/bundle-assets.mjs` in a published install. Lets
92
+ // `nomen test/run --audit` work outside the nomen checkout with no
93
+ // --audit-runtime flag.
94
+ export function find_bundled_audit_runtime(): string | undefined {
95
+ return find_bundled("src/audit_runtime.c") ?? find_bundled("audit_runtime.c");
96
+ }
97
+
88
98
  /**
89
99
  * `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
90
100
  * starting layout (package.jsonc, src/main.nm, a starter test, README and an
package/src/test.ts CHANGED
@@ -1,17 +1,26 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFile } from "node:child_process";
2
2
  import fs from "node:fs";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
5
7
 
6
8
  import build, { default_platform } from "../../src/build.ts";
7
9
  import join from "../../src/join.ts";
8
10
  import { get_library } from "../../src/lib.ts";
9
11
  import parse from "../../src/parse.ts";
10
- import { find_bundled } from "./init.ts";
12
+ import { find_bundled, find_bundled_audit_runtime } from "./init.ts";
11
13
  import { build_dir_for } from "./paths.ts";
12
14
 
13
15
  const RECORD_PREFIX = "\\nomen|";
14
16
 
17
+ // Promise-returning `execFile`: `nomen test` builds and runs each `*.test.nm`
18
+ // file in its own build folder and its own process, so the (latency-bound)
19
+ // clang/link/run subprocesses overlap across files instead of serializing.
20
+ // A rejected error carries the same `.stdout` / `.stderr` / `.signal` /
21
+ // `.code` fields the previous synchronous `execFileSync` catch sites read.
22
+ const execFile_async = promisify(execFile);
23
+
15
24
  // A discovered test function: `pub func <name> = (ref Tester t)`.
16
25
  export interface TestFunction {
17
26
  name: string;
@@ -272,7 +281,7 @@ export function extract_leaks(stdout: string): string[] {
272
281
  * Compile one `*.test.nm` file (with the generated harness) and run it,
273
282
  * returning its parsed records and any crash info.
274
283
  */
275
- export function run_test_file(
284
+ export async function run_test_file(
276
285
  entry_path: string,
277
286
  lib_path: string | undefined,
278
287
  arch: string,
@@ -280,7 +289,7 @@ export function run_test_file(
280
289
  audit_runtime?: string,
281
290
  release = false,
282
291
  cwd?: string,
283
- ): TestFileResult {
292
+ ): Promise<TestFileResult> {
284
293
  const start = performance.now();
285
294
  const source_text = fs.readFileSync(entry_path, "utf8");
286
295
  const tests = extract_test_functions(source_text);
@@ -339,9 +348,13 @@ export function run_test_file(
339
348
  // "LEAK: N allocation(s)" when the balance is nonzero.
340
349
  let audit_obj: string | undefined;
341
350
  if (audit) {
351
+ // Explicit flag wins; else walk up from the test file (a nomen
352
+ // checkout's src/audit_runtime.c); else the runtime bundled with the
353
+ // CLI (live repo copy in the dev tree, shipped copy when published)
354
+ // so `--audit` works outside the nomen checkout with no flag.
342
355
  const runtime_src = audit_runtime
343
356
  ? path.resolve(audit_runtime)
344
- : find_audit_runtime(path.dirname(resolved));
357
+ : (find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime());
345
358
  if (!runtime_src || !fs.existsSync(runtime_src)) {
346
359
  result.ok = false;
347
360
  result.phase = "setup";
@@ -351,7 +364,7 @@ export function run_test_file(
351
364
  return result;
352
365
  }
353
366
  audit_obj = path.join(buildDir, "audit_runtime.o");
354
- execFileSync("clang", ["-c", runtime_src, "-o", audit_obj]);
367
+ await execFile_async("clang", ["-c", runtime_src, "-o", audit_obj]);
355
368
  }
356
369
  const platform = default_platform();
357
370
  // Match the run/build command's emission: ObjC-bearing sources (GUI code
@@ -401,10 +414,7 @@ export function run_test_file(
401
414
  );
402
415
  }
403
416
  try {
404
- execFileSync("clang", link_args, {
405
- encoding: "utf8",
406
- stdio: ["ignore", "pipe", "pipe"],
407
- });
417
+ await execFile_async("clang", link_args, { encoding: "utf8" });
408
418
  } catch (err: any) {
409
419
  const stderr = err.stderr ? err.stderr.toString() : (err.message ?? "");
410
420
  result.ok = false;
@@ -424,13 +434,13 @@ export function run_test_file(
424
434
  let runStdout = "";
425
435
  let crashed: string | undefined;
426
436
  try {
427
- runStdout = execFileSync(outfile, [], {
437
+ const run = await execFile_async(outfile, [], {
428
438
  encoding: "utf8",
429
- stdio: ["ignore", "pipe", "pipe"],
430
439
  timeout: 30_000,
431
440
  maxBuffer: 16 * 1024 * 1024,
432
441
  cwd: cwd ?? process.cwd(),
433
442
  });
443
+ runStdout = run.stdout;
434
444
  } catch (err: any) {
435
445
  runStdout = err.stdout ? err.stdout.toString() : "";
436
446
  if (err.signal === "SIGTERM") {
@@ -545,12 +555,27 @@ export interface RunTestsOptions {
545
555
  audit?: boolean;
546
556
  audit_runtime?: string;
547
557
  release?: boolean;
558
+ /**
559
+ * Maximum test files built and run concurrently. Each file compiles into
560
+ * its own build folder and runs in its own process, so the latency-bound
561
+ * clang/link/run steps overlap across files. Defaults to the machine's
562
+ * available parallelism; 1 forces the historical sequential order.
563
+ */
564
+ jobs?: number;
565
+ }
566
+
567
+ /** The default concurrency for `runTests` when no `jobs` is given. */
568
+ export function default_jobs(): number {
569
+ const n =
570
+ typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
571
+ return Math.max(1, n);
548
572
  }
549
573
 
550
574
  /**
551
575
  * Locate audit_runtime.c for an audited test build: an explicit path wins,
552
576
  * otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
553
- * convention the run/build commands use).
577
+ * convention the run/build commands use). When both miss, the caller falls
578
+ * back to the CLI-bundled runtime (`find_bundled_audit_runtime`).
554
579
  */
555
580
  function find_audit_runtime(dir: string): string | undefined {
556
581
  let cur = dir;
@@ -565,7 +590,7 @@ function find_audit_runtime(dir: string): string | undefined {
565
590
  }
566
591
 
567
592
  /** Discover, run, and report every `*.test.nm` under `root`. */
568
- export function runTests(root: string, options: RunTestsOptions = {}): boolean {
593
+ export async function runTests(root: string, options: RunTestsOptions = {}): Promise<boolean> {
569
594
  const arch = options.arch ?? "aarch64";
570
595
  const lib = resolve_lib_for(root);
571
596
  const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
@@ -577,20 +602,33 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
577
602
  // fixture paths resolve there no matter where the CLI was invoked from.
578
603
  const binary_cwd = path.resolve(root);
579
604
 
580
- const results: TestFileResult[] = [];
581
- for (const file of files) {
582
- const result = run_test_file(
583
- file,
584
- lib,
585
- arch,
586
- options.audit,
587
- options.audit_runtime,
588
- options.release,
589
- binary_cwd,
590
- );
591
- report_file(result);
592
- results.push(result);
593
- }
605
+ // Bounded-concurrency pool: at most `jobs` files are in flight. The
606
+ // parse/build halves are synchronous (no internal awaits), so they run
607
+ // atomically on the main thread between the subprocess awaits — the
608
+ // compiler's in-process state never interleaves. Results are collected by
609
+ // file INDEX and reported after the pool drains, so the report stays in
610
+ // discovery order regardless of completion order (and stdout isn't
611
+ // interleaved).
612
+ const jobs = Math.max(1, Math.min(options.jobs ?? default_jobs(), files.length));
613
+ const results: TestFileResult[] = new Array(files.length);
614
+ let next = 0;
615
+ const worker = async (): Promise<void> => {
616
+ for (;;) {
617
+ const i = next++;
618
+ if (i >= files.length) return;
619
+ results[i] = await run_test_file(
620
+ files[i],
621
+ lib,
622
+ arch,
623
+ options.audit,
624
+ options.audit_runtime,
625
+ options.release,
626
+ binary_cwd,
627
+ );
628
+ }
629
+ };
630
+ await Promise.all(Array.from({ length: jobs }, worker));
631
+ for (const result of results) report_file(result);
594
632
 
595
633
  const elapsed = performance.now() - startTime;
596
634
  const totalFiles = results.length;