nomen-lang 0.6.2 → 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.2",
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,6 +37,9 @@ 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;
@@ -257,6 +260,14 @@ export function parse_records(stdout: string): RunRecord {
257
260
  return { tests, fails, benches, other };
258
261
  }
259
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
+
260
271
  /**
261
272
  * Compile one `*.test.nm` file (with the generated harness) and run it,
262
273
  * returning its parsed records and any crash info.
@@ -281,6 +292,7 @@ export function run_test_file(
281
292
  fails: [],
282
293
  benches: [],
283
294
  other: [],
295
+ leaks: [],
284
296
  ms: 0,
285
297
  };
286
298
 
@@ -408,11 +420,21 @@ export function run_test_file(
408
420
  result.fails = records.fails;
409
421
  result.benches = records.benches;
410
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
+ }
411
432
  result.crashed = crashed;
412
433
  if (crashed) result.phase = "run";
413
- // A file "passes" when it reported no failed asserts and didn't crash.
434
+ // A file "passes" when it reported no failed asserts, didn't crash, and
435
+ // (under --audit) leaked nothing.
414
436
  const failed = result.fails.length > 0 || result.tests.some((t) => t.failed > 0);
415
- result.ok = !failed && crashed === undefined;
437
+ result.ok = !failed && crashed === undefined && result.leaks.length === 0;
416
438
 
417
439
  result.ms = performance.now() - start;
418
440
  return result;
@@ -475,6 +497,9 @@ export function report_file(result: TestFileResult): void {
475
497
  for (const f of result.fails) {
476
498
  console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
477
499
  }
500
+ for (const leak of result.leaks) {
501
+ console.log(` ${C.yellow("⚠")} ${C.yellow(leak)}`);
502
+ }
478
503
  for (const b of result.benches) {
479
504
  console.log(
480
505
  ` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ` +
@@ -540,6 +565,7 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
540
565
  const totalTests = results.reduce((a, r) => a + r.tests.length, 0);
541
566
  const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
542
567
  const totalPassed = totalTests - totalFailed;
568
+ const totalLeaks = results.reduce((a, r) => a + r.leaks.length, 0);
543
569
  const anyFailed = results.some((r) => !r.ok);
544
570
 
545
571
  console.log("");
@@ -556,6 +582,12 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
556
582
  (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") +
557
583
  C.dim(` (${totalTests})`),
558
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
+ }
559
591
  console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
560
592
  console.log("");
561
593