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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",
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
- // Folder mode checks each file in its own context — anchoring
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
- const input = join(anchor, config.lib);
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, anchor);
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 candidates = [path.join(cli_root, filename), path.join(cli_root, "..", filename)];
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
- // A file "passes" when it reported no failed asserts and didn't crash.
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
- console.log(` ${C.red("✗")} ${rel} ${C.red("(failed to build)")}`);
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