nomen-lang 0.6.7 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.6.7",
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([
@@ -160,7 +164,7 @@ export function print_help(): void {
160
164
  " nomen check --in [file/folder] Parse and check only",
161
165
  " nomen format [--in folder] Reformat every .nm file",
162
166
  " nomen docs [--in file] Generate markdown documentation",
163
- " nomen test [--in folder] Discover and run *.test.nm files",
167
+ " nomen test [--in folder] Discover and run *.test.nm files (binary CWD = --in folder)",
164
168
  " nomen init <name> Scaffold a new project in ./<name>",
165
169
  " nomen lib-path Print the bundled System library path",
166
170
  "",
@@ -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,11 +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
+ release: args.release,
113
+ jobs: args.jobs !== undefined ? parseInt(args.jobs, 10) : undefined,
112
114
  });
113
115
  process.exit(ok ? 0 : 1);
114
116
  }
@@ -282,7 +284,10 @@ function resolve_audit_runtime(config: Config, input_path: string): string | und
282
284
  if (parent === dir) break;
283
285
  dir = parent;
284
286
  }
285
- 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();
286
291
  }
287
292
 
288
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,13 +281,15 @@ 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,
279
288
  audit = false,
280
289
  audit_runtime?: string,
281
- ): TestFileResult {
290
+ release = false,
291
+ cwd?: string,
292
+ ): Promise<TestFileResult> {
282
293
  const start = performance.now();
283
294
  const source_text = fs.readFileSync(entry_path, "utf8");
284
295
  const tests = extract_test_functions(source_text);
@@ -318,6 +329,7 @@ export function run_test_file(
318
329
  const buildResult = build(parsed.root, {
319
330
  arch: arch as "aarch64" | "c",
320
331
  audit,
332
+ optimize: release,
321
333
  });
322
334
  if (buildResult.errors && buildResult.errors.length) {
323
335
  result.ok = false;
@@ -336,9 +348,13 @@ export function run_test_file(
336
348
  // "LEAK: N allocation(s)" when the balance is nonzero.
337
349
  let audit_obj: string | undefined;
338
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.
339
355
  const runtime_src = audit_runtime
340
356
  ? path.resolve(audit_runtime)
341
- : find_audit_runtime(path.dirname(resolved));
357
+ : (find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime());
342
358
  if (!runtime_src || !fs.existsSync(runtime_src)) {
343
359
  result.ok = false;
344
360
  result.phase = "setup";
@@ -348,7 +364,7 @@ export function run_test_file(
348
364
  return result;
349
365
  }
350
366
  audit_obj = path.join(buildDir, "audit_runtime.o");
351
- execFileSync("clang", ["-c", runtime_src, "-o", audit_obj]);
367
+ await execFile_async("clang", ["-c", runtime_src, "-o", audit_obj]);
352
368
  }
353
369
  const platform = default_platform();
354
370
  // Match the run/build command's emission: ObjC-bearing sources (GUI code
@@ -363,11 +379,28 @@ export function run_test_file(
363
379
  fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
364
380
  fs.writeFileSync(codefile, buildResult.code);
365
381
 
382
+ // Companion C (raw-block functions + the pool/fiber/file-scope C infra):
383
+ // `run`/`build` compile and link it next to the main TU. The aarch64 .s
384
+ // references its symbols via `bl` (spawn machinery), so dropping it makes
385
+ // companion-bearing test files fail to link. Same .m/.c rule as the
386
+ // run/build command's companion emission.
387
+ let companionfile: string | undefined;
388
+ if (buildResult.companion) {
389
+ const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
390
+ companionfile = path.join(buildDir, path.basename(entry_path, ".nm") + "_companion" + comp_ext);
391
+ fs.writeFileSync(companionfile, buildResult.companion);
392
+ }
393
+
366
394
  // Link the harness binary. Frameworks are passed on Apple platforms for
367
395
  // the same reason as the run/build command: the joined source may reference
368
396
  // ObjC runtime symbols even when the test body doesn't, and unused
369
- // frameworks cost nothing at link time.
397
+ // frameworks cost nothing at link time. Release mode compiles the C (and
398
+ // companion C) with -O2, matching the run/build command; the aarch64 .s is
399
+ // assembled verbatim — its optimizations are the passes enabled via
400
+ // `optimize` above.
370
401
  const link_args = ["-o", outfile, codefile];
402
+ if (companionfile) link_args.push(companionfile);
403
+ if (release) link_args.unshift("-O2");
371
404
  if (audit_obj) link_args.push(audit_obj);
372
405
  if (platform === "macos" || platform === "ios") {
373
406
  link_args.push(
@@ -381,10 +414,7 @@ export function run_test_file(
381
414
  );
382
415
  }
383
416
  try {
384
- execFileSync("clang", link_args, {
385
- encoding: "utf8",
386
- stdio: ["ignore", "pipe", "pipe"],
387
- });
417
+ await execFile_async("clang", link_args, { encoding: "utf8" });
388
418
  } catch (err: any) {
389
419
  const stderr = err.stderr ? err.stderr.toString() : (err.message ?? "");
390
420
  result.ok = false;
@@ -396,16 +426,21 @@ export function run_test_file(
396
426
 
397
427
  // Run the binary and collect the records it streams over stdout. A
398
428
  // non-zero exit (crash, abort) still surfaces whatever records were
399
- // emitted before the crash; a timeout is treated as a crash.
429
+ // emitted before the crash; a timeout is treated as a crash. The
430
+ // documented CWD contract: the binary runs in the `--in` root (the
431
+ // folder `nomen test` scanned for *.test.nm files), so fixture paths in
432
+ // tests resolve against that folder regardless of where the CLI was
433
+ // invoked from.
400
434
  let runStdout = "";
401
435
  let crashed: string | undefined;
402
436
  try {
403
- runStdout = execFileSync(outfile, [], {
437
+ const run = await execFile_async(outfile, [], {
404
438
  encoding: "utf8",
405
- stdio: ["ignore", "pipe", "pipe"],
406
439
  timeout: 30_000,
407
440
  maxBuffer: 16 * 1024 * 1024,
441
+ cwd: cwd ?? process.cwd(),
408
442
  });
443
+ runStdout = run.stdout;
409
444
  } catch (err: any) {
410
445
  runStdout = err.stdout ? err.stdout.toString() : "";
411
446
  if (err.signal === "SIGTERM") {
@@ -519,12 +554,28 @@ export interface RunTestsOptions {
519
554
  filter?: RegExp;
520
555
  audit?: boolean;
521
556
  audit_runtime?: string;
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);
522
572
  }
523
573
 
524
574
  /**
525
575
  * Locate audit_runtime.c for an audited test build: an explicit path wins,
526
576
  * otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
527
- * 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`).
528
579
  */
529
580
  function find_audit_runtime(dir: string): string | undefined {
530
581
  let cur = dir;
@@ -539,7 +590,7 @@ function find_audit_runtime(dir: string): string | undefined {
539
590
  }
540
591
 
541
592
  /** Discover, run, and report every `*.test.nm` under `root`. */
542
- export function runTests(root: string, options: RunTestsOptions = {}): boolean {
593
+ export async function runTests(root: string, options: RunTestsOptions = {}): Promise<boolean> {
543
594
  const arch = options.arch ?? "aarch64";
544
595
  const lib = resolve_lib_for(root);
545
596
  const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
@@ -547,13 +598,37 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
547
598
  console.log(`Found ${files.length} test file(s)\n`);
548
599
 
549
600
  const startTime = performance.now();
550
-
551
- const results: TestFileResult[] = [];
552
- for (const file of files) {
553
- const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime);
554
- report_file(result);
555
- results.push(result);
556
- }
601
+ // The CWD contract: test binaries run in the --in root, so relative
602
+ // fixture paths resolve there no matter where the CLI was invoked from.
603
+ const binary_cwd = path.resolve(root);
604
+
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);
557
632
 
558
633
  const elapsed = performance.now() - startTime;
559
634
  const totalFiles = results.length;