nomen-lang 0.6.1 → 0.6.3
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/core/System/List.nm +12 -0
- package/core/System/StringBuilder.nm +12 -4
- package/core/System/Text/Regex.nm +704 -77
- package/core/docs/System/Text/Regex.md +1 -1
- package/dist/index.mjs +1362 -362
- package/package.json +1 -1
- package/src/index.ts +8 -56
- package/src/init.ts +12 -3
- package/src/test.ts +57 -3
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -310,9 +310,7 @@ function processFolder(folder: string, config: Config, mode: Mode, program_args:
|
|
|
310
310
|
let dirent;
|
|
311
311
|
while ((dirent = dir.readSync()) !== null) {
|
|
312
312
|
if (shouldProcessFile(dirent.name)) {
|
|
313
|
-
|
|
314
|
-
// here would recompile the whole project once per file.
|
|
315
|
-
processFile(path.join(folder, dirent.name), config, mode, program_args, false);
|
|
313
|
+
processFile(path.join(folder, dirent.name), config, mode, program_args);
|
|
316
314
|
// @ts-ignore
|
|
317
315
|
let _ = fs.watch;
|
|
318
316
|
}
|
|
@@ -324,46 +322,7 @@ function shouldProcessFile(filename: string) {
|
|
|
324
322
|
return path.extname(filename) === SUPPORTED_EXTENSION;
|
|
325
323
|
}
|
|
326
324
|
|
|
327
|
-
|
|
328
|
-
* The project's entry file for `file_path`: walk up to the nearest
|
|
329
|
-
* `package.jsonc` and resolve its `entry` field, so any file inside a project
|
|
330
|
-
* is checked with the entry's import context (`import types` resolves
|
|
331
|
-
* against the project's `src/`, not the file's own folder). Returns
|
|
332
|
-
* undefined when the file is standalone, IS the entry, sits in an
|
|
333
|
-
* entry-less project (e.g. a library checkout), or the config is malformed.
|
|
334
|
-
*/
|
|
335
|
-
function project_entry_for(file_path: string): string | undefined {
|
|
336
|
-
let dir = path.dirname(file_path);
|
|
337
|
-
for (let i = 0; i < 20; i++) {
|
|
338
|
-
const config_path = path.join(dir, "package.jsonc");
|
|
339
|
-
if (fs.existsSync(config_path)) {
|
|
340
|
-
try {
|
|
341
|
-
const raw = fs.readFileSync(config_path, "utf8");
|
|
342
|
-
const json = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
343
|
-
const parsed = JSON.parse(json);
|
|
344
|
-
if (parsed.entry) {
|
|
345
|
-
const entry = path.resolve(dir, parsed.entry);
|
|
346
|
-
if (entry !== file_path && fs.existsSync(entry)) return entry;
|
|
347
|
-
}
|
|
348
|
-
} catch {
|
|
349
|
-
// malformed config — keep the file's own context
|
|
350
|
-
}
|
|
351
|
-
return undefined;
|
|
352
|
-
}
|
|
353
|
-
const parent = path.dirname(dir);
|
|
354
|
-
if (parent === dir) return undefined;
|
|
355
|
-
dir = parent;
|
|
356
|
-
}
|
|
357
|
-
return undefined;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function processFile(
|
|
361
|
-
filename: string,
|
|
362
|
-
config: Config,
|
|
363
|
-
mode: Mode,
|
|
364
|
-
program_args: string[],
|
|
365
|
-
anchor_to_project = true,
|
|
366
|
-
) {
|
|
325
|
+
function processFile(filename: string, config: Config, mode: Mode, program_args: string[]) {
|
|
367
326
|
console.log("Processing", filename);
|
|
368
327
|
|
|
369
328
|
const arch = config.arch || "aarch64";
|
|
@@ -374,22 +333,15 @@ function processFile(
|
|
|
374
333
|
config.lib = resolve_lib(resolved);
|
|
375
334
|
}
|
|
376
335
|
|
|
377
|
-
// A `*.test.nm` file keeps its own join context: the test harness
|
|
378
|
-
// anchors at the test's folder and pulls the program's `src/` module in
|
|
379
|
-
// with `main` stripped, which the entry's context would not do.
|
|
380
|
-
const anchor =
|
|
381
|
-
anchor_to_project && !resolved.endsWith(".test.nm")
|
|
382
|
-
? (project_entry_for(resolved) ?? resolved)
|
|
383
|
-
: resolved;
|
|
384
|
-
if (anchor !== resolved) {
|
|
385
|
-
console.log("Anchored to project entry", anchor);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
336
|
let startTime = performance.now();
|
|
389
337
|
|
|
390
|
-
|
|
338
|
+
// Compile the file the user named (or the discovered entry). `join`
|
|
339
|
+
// resolves its imports relative to its own folder and gathers the
|
|
340
|
+
// project's module declarations, so an explicitly-passed `--in` is never
|
|
341
|
+
// silently replaced by the `package.jsonc` `entry`.
|
|
342
|
+
const input = join(resolved, config.lib);
|
|
391
343
|
const library = config.lib ? get_library(config.lib) : undefined;
|
|
392
|
-
const parsed = parse(input, library,
|
|
344
|
+
const parsed = parse(input, library, resolved);
|
|
393
345
|
|
|
394
346
|
let errors = parsed.errors;
|
|
395
347
|
|
package/src/init.ts
CHANGED
|
@@ -63,13 +63,22 @@ nomen test
|
|
|
63
63
|
`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
// Resolve a bundled asset (core/ or NOMEN_AGENTS.md) shipped alongside the
|
|
66
|
+
// Resolve a bundled asset (core/, n/ or NOMEN_AGENTS.md) shipped alongside the
|
|
67
67
|
// CLI. In the published package these sit next to dist/; in the dev tree they
|
|
68
|
-
// live one level up (the repo root), so check both.
|
|
68
|
+
// live one level up (the repo root), so check both. In the dev tree the
|
|
69
|
+
// REPO's live copy wins — a stale `cli/core` bundle left by an old
|
|
70
|
+
// `pnpm build` must not shadow the current `core/` (a silent-wrong-library
|
|
71
|
+
// tar pit; see FOLLOWUP.md, "Stale cli/core asset copy"). In a published
|
|
72
|
+
// install (running from node_modules) the shipped copy wins, so a consumer
|
|
73
|
+
// project's sibling `core/` directory is never picked up.
|
|
69
74
|
export function find_bundled(filename: string): string | undefined {
|
|
70
75
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
71
76
|
const cli_root = path.dirname(here);
|
|
72
|
-
const
|
|
77
|
+
const repo_root = path.join(cli_root, "..");
|
|
78
|
+
const dev_tree = !cli_root.includes("node_modules");
|
|
79
|
+
const candidates = dev_tree
|
|
80
|
+
? [path.join(repo_root, filename), path.join(cli_root, filename)]
|
|
81
|
+
: [path.join(cli_root, filename), path.join(repo_root, filename)];
|
|
73
82
|
for (const c of candidates) {
|
|
74
83
|
if (fs.existsSync(c)) return path.resolve(c);
|
|
75
84
|
}
|
package/src/test.ts
CHANGED
|
@@ -37,9 +37,18 @@ export interface TestFileResult {
|
|
|
37
37
|
fails: FailRecord[];
|
|
38
38
|
benches: BenchRecord[];
|
|
39
39
|
other: string[];
|
|
40
|
+
// "LEAK: N allocation(s)" lines the audits runtime printed at exit. Empty
|
|
41
|
+
// unless the file was run with --audit; a non-empty list fails the file.
|
|
42
|
+
leaks: string[];
|
|
40
43
|
// Set when the compiled binary crashed before finishing (e.g. a segfault
|
|
41
44
|
// in a test). We still surface whatever records it managed to emit.
|
|
42
45
|
crashed?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The pipeline stage that produced `crashed`, so the report can name it
|
|
48
|
+
* accurately — a binary that BUILT fine and then segfaulted before
|
|
49
|
+
* emitting any record is not a build failure (see report_file).
|
|
50
|
+
*/
|
|
51
|
+
phase?: "parse" | "build" | "setup" | "link" | "run";
|
|
43
52
|
ms: number;
|
|
44
53
|
}
|
|
45
54
|
|
|
@@ -251,6 +260,14 @@ export function parse_records(stdout: string): RunRecord {
|
|
|
251
260
|
return { tests, fails, benches, other };
|
|
252
261
|
}
|
|
253
262
|
|
|
263
|
+
/** Pull the exit-time audit "LEAK: N allocation(s)" lines out of stdout. */
|
|
264
|
+
export function extract_leaks(stdout: string): string[] {
|
|
265
|
+
return stdout
|
|
266
|
+
.split("\n")
|
|
267
|
+
.map((l) => l.trim())
|
|
268
|
+
.filter((l) => l.startsWith("LEAK:"));
|
|
269
|
+
}
|
|
270
|
+
|
|
254
271
|
/**
|
|
255
272
|
* Compile one `*.test.nm` file (with the generated harness) and run it,
|
|
256
273
|
* returning its parsed records and any crash info.
|
|
@@ -275,6 +292,7 @@ export function run_test_file(
|
|
|
275
292
|
fails: [],
|
|
276
293
|
benches: [],
|
|
277
294
|
other: [],
|
|
295
|
+
leaks: [],
|
|
278
296
|
ms: 0,
|
|
279
297
|
};
|
|
280
298
|
|
|
@@ -289,6 +307,7 @@ export function run_test_file(
|
|
|
289
307
|
const parsed = parse(source, library, resolved);
|
|
290
308
|
if (parsed.errors.length) {
|
|
291
309
|
result.ok = false;
|
|
310
|
+
result.phase = "parse";
|
|
292
311
|
result.crashed = parsed.errors
|
|
293
312
|
.map((e) => `${e.message} (${e.line ?? "?"}:${e.column ?? "?"})`)
|
|
294
313
|
.join("\n");
|
|
@@ -302,6 +321,7 @@ export function run_test_file(
|
|
|
302
321
|
});
|
|
303
322
|
if (buildResult.errors && buildResult.errors.length) {
|
|
304
323
|
result.ok = false;
|
|
324
|
+
result.phase = "build";
|
|
305
325
|
result.crashed = buildResult.errors
|
|
306
326
|
.map((e) => `${e.message} (${e.line ?? "?"}:${e.column ?? "?"})`)
|
|
307
327
|
.join("\n");
|
|
@@ -321,6 +341,7 @@ export function run_test_file(
|
|
|
321
341
|
: find_audit_runtime(path.dirname(resolved));
|
|
322
342
|
if (!runtime_src || !fs.existsSync(runtime_src)) {
|
|
323
343
|
result.ok = false;
|
|
344
|
+
result.phase = "setup";
|
|
324
345
|
result.crashed =
|
|
325
346
|
"Audit enabled but audit_runtime.c was not found. Pass --audit-runtime <path>.";
|
|
326
347
|
result.ms = performance.now() - start;
|
|
@@ -367,6 +388,7 @@ export function run_test_file(
|
|
|
367
388
|
} catch (err: any) {
|
|
368
389
|
const stderr = err.stderr ? err.stderr.toString() : (err.message ?? "");
|
|
369
390
|
result.ok = false;
|
|
391
|
+
result.phase = "link";
|
|
370
392
|
result.crashed = `link failed: ${stderr.trim() || "clang error"}`;
|
|
371
393
|
result.ms = performance.now() - start;
|
|
372
394
|
return result;
|
|
@@ -398,10 +420,21 @@ export function run_test_file(
|
|
|
398
420
|
result.fails = records.fails;
|
|
399
421
|
result.benches = records.benches;
|
|
400
422
|
result.other = records.other;
|
|
423
|
+
// Under --audit the exit-time audit check prints "LEAK: N allocation(s)"
|
|
424
|
+
// to stdout when the malloc/free balance is nonzero. Treat any such line
|
|
425
|
+
// as a failure so a leaked test file cannot stay green.
|
|
426
|
+
result.leaks = audit ? extract_leaks(runStdout) : [];
|
|
427
|
+
// Leak lines are surfaced explicitly above; drop them from the generic
|
|
428
|
+
// stdout dump so they are not printed twice.
|
|
429
|
+
if (result.leaks.length) {
|
|
430
|
+
result.other = result.other.filter((l) => !l.trim().startsWith("LEAK:"));
|
|
431
|
+
}
|
|
401
432
|
result.crashed = crashed;
|
|
402
|
-
|
|
433
|
+
if (crashed) result.phase = "run";
|
|
434
|
+
// A file "passes" when it reported no failed asserts, didn't crash, and
|
|
435
|
+
// (under --audit) leaked nothing.
|
|
403
436
|
const failed = result.fails.length > 0 || result.tests.some((t) => t.failed > 0);
|
|
404
|
-
result.ok = !failed && crashed === undefined;
|
|
437
|
+
result.ok = !failed && crashed === undefined && result.leaks.length === 0;
|
|
405
438
|
|
|
406
439
|
result.ms = performance.now() - start;
|
|
407
440
|
return result;
|
|
@@ -435,7 +468,18 @@ const C = {
|
|
|
435
468
|
export function report_file(result: TestFileResult): void {
|
|
436
469
|
const rel = path.relative(process.cwd(), result.file);
|
|
437
470
|
if (result.crashed && result.tests.length === 0 && result.fails.length === 0) {
|
|
438
|
-
|
|
471
|
+
// Name the stage that actually failed. A binary that linked fine and
|
|
472
|
+
// then died before emitting any record (e.g. SIGSEGV) is NOT a build
|
|
473
|
+
// failure — conflating the two sent debugging to the wrong layer.
|
|
474
|
+
const label =
|
|
475
|
+
result.phase === "run"
|
|
476
|
+
? "(crashed before records)"
|
|
477
|
+
: result.phase === "link"
|
|
478
|
+
? "(link failed)"
|
|
479
|
+
: result.phase === "setup"
|
|
480
|
+
? "(setup failed)"
|
|
481
|
+
: "(failed to build)";
|
|
482
|
+
console.log(` ${C.red("✗")} ${rel} ${C.red(label)}`);
|
|
439
483
|
console.log(C.red(result.crashed));
|
|
440
484
|
return;
|
|
441
485
|
}
|
|
@@ -453,6 +497,9 @@ export function report_file(result: TestFileResult): void {
|
|
|
453
497
|
for (const f of result.fails) {
|
|
454
498
|
console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
|
|
455
499
|
}
|
|
500
|
+
for (const leak of result.leaks) {
|
|
501
|
+
console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
|
|
502
|
+
}
|
|
456
503
|
for (const b of result.benches) {
|
|
457
504
|
console.log(
|
|
458
505
|
` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ` +
|
|
@@ -518,6 +565,7 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
|
518
565
|
const totalTests = results.reduce((a, r) => a + r.tests.length, 0);
|
|
519
566
|
const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
|
|
520
567
|
const totalPassed = totalTests - totalFailed;
|
|
568
|
+
const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
|
|
521
569
|
const anyFailed = results.some((r) => !r.ok);
|
|
522
570
|
|
|
523
571
|
console.log("");
|
|
@@ -534,6 +582,12 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
|
534
582
|
(totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") +
|
|
535
583
|
C.dim(` (${totalTests})`),
|
|
536
584
|
);
|
|
585
|
+
if (totalLeaks) {
|
|
586
|
+
console.log(
|
|
587
|
+
` ${C.dim("Leaks ")} ${C.yellow(`${totalLeaks} reported`)}` +
|
|
588
|
+
C.dim(" (--audit; leaked files count as failures)"),
|
|
589
|
+
);
|
|
590
|
+
}
|
|
537
591
|
console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
|
|
538
592
|
console.log("");
|
|
539
593
|
|