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/NOMEN_AGENTS.md +12 -3
- package/audit_runtime.c +50 -0
- package/core/System/Buffer.nm +38 -0
- package/core/System/StringBuilder.nm +10 -1
- package/dist/index.mjs +520 -97
- package/package.json +3 -2
- package/src/args.ts +8 -3
- package/src/index.ts +8 -3
- package/src/init.ts +10 -0
- package/src/test.ts +98 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nomen-lang",
|
|
3
|
-
"version": "0.6.
|
|
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
|
|
178
|
-
" --
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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;
|