nomen-lang 0.0.8 → 0.0.10

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 CHANGED
@@ -68,11 +68,11 @@ func safe = (string[] s, int i: i >= 0 && i < s.length, out string) {
68
68
 
69
69
  ### Memory modifiers
70
70
 
71
- | Modifier | Meaning |
72
- |----------|---------|
73
- | `ref T` | mutable borrow; caller must also write `ref` |
74
- | `view T` | read-only borrow |
75
- | `mov T` | transfer ownership; caller writes `mov` |
71
+ | Modifier | Meaning |
72
+ | -------- | ---------------------------------------------- |
73
+ | `ref T` | mutable borrow; caller must also write `ref` |
74
+ | `view T` | read-only borrow |
75
+ | `mov T` | transfer ownership; caller writes `mov` |
76
76
  | `out T` | output parameter, assigned inside the function |
77
77
 
78
78
  `const` values cannot be passed to `ref` — rebind the caller as `var` first.
package/dist/index.mjs CHANGED
@@ -16308,16 +16308,33 @@ function describe(token) {
16308
16308
  }
16309
16309
  //#endregion
16310
16310
  //#region ../src/join.ts
16311
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
16312
- const DEFAULT_LIB_PATH = path.resolve(__dirname, "../../core/src");
16313
- function join$1(entry_file_path, lib_path) {
16311
+ const DEFAULT_LIB_REL = "../../core/src";
16312
+ let default_lib_path;
16313
+ function fallback_lib_path() {
16314
+ if (default_lib_path === void 0) try {
16315
+ default_lib_path = path.resolve(path.dirname(fileURLToPath(import.meta.url)), DEFAULT_LIB_REL);
16316
+ } catch {
16317
+ default_lib_path = "";
16318
+ }
16319
+ return default_lib_path;
16320
+ }
16321
+ let test_src_dir;
16322
+ function join$1(entry_file_path, lib_path, options) {
16314
16323
  const folder_path = path.dirname(entry_file_path);
16315
16324
  const file_path = path.basename(entry_file_path);
16316
16325
  const inputs = /* @__PURE__ */ new Map();
16317
- const resolved_lib_path = lib_path ? path.resolve(lib_path, "src") : DEFAULT_LIB_PATH;
16318
- add_source(folder_path, `./${file_path}`, inputs, resolved_lib_path);
16319
- gather_module_siblings(folder_path, file_path, inputs, resolved_lib_path);
16320
- gather_module_parent(folder_path, file_path, inputs, resolved_lib_path);
16326
+ const resolved_lib_path = lib_path ? path.resolve(lib_path, "src") : fallback_lib_path();
16327
+ const for_test = options?.for_test ?? file_path.endsWith(".test.nm");
16328
+ const prev_test_src_dir = test_src_dir;
16329
+ test_src_dir = for_test ? resolve_src_module(folder_path) : void 0;
16330
+ try {
16331
+ add_source(folder_path, `./${file_path}`, inputs, resolved_lib_path);
16332
+ gather_module_siblings(folder_path, file_path, inputs, resolved_lib_path);
16333
+ gather_module_parent(folder_path, file_path, inputs, resolved_lib_path);
16334
+ if (test_src_dir) gather_src_module(test_src_dir, inputs, resolved_lib_path);
16335
+ } finally {
16336
+ test_src_dir = prev_test_src_dir;
16337
+ }
16321
16338
  return Array.from(inputs.values()).join("\n\n") + "\n";
16322
16339
  }
16323
16340
  function gather_module_siblings(folder_path, entry_file, inputs, lib_path) {
@@ -16361,6 +16378,26 @@ function gather_module_parent(folder_path, entry_file, inputs, lib_path) {
16361
16378
  add_source(parent_path, `./${name}`, inputs, lib_path);
16362
16379
  }
16363
16380
  }
16381
+ /** The program's `src/` module dir for a file in `entry_dir`, if one exists. */
16382
+ function resolve_src_module(entry_dir) {
16383
+ for (const dir of [path.join(entry_dir, "src"), path.join(path.dirname(entry_dir), "src")]) try {
16384
+ if (fs.statSync(dir).isDirectory()) return dir;
16385
+ } catch {}
16386
+ }
16387
+ function gather_src_module(src_dir, inputs, lib_path) {
16388
+ let names;
16389
+ try {
16390
+ names = fs.readdirSync(src_dir);
16391
+ } catch {
16392
+ return;
16393
+ }
16394
+ for (const name of names.sort()) {
16395
+ if (!name.endsWith(".nm")) continue;
16396
+ const import_file_path = `./${name}`;
16397
+ if (inputs.has(import_file_path)) continue;
16398
+ add_source(src_dir, import_file_path, inputs, lib_path);
16399
+ }
16400
+ }
16364
16401
  function has_main(source) {
16365
16402
  return /\bfunc\s+main\b/.test(source);
16366
16403
  }
@@ -16370,8 +16407,9 @@ function add_source(folder_path, file_path, inputs, lib_path) {
16370
16407
  const lib_source_path = path.resolve(lib_path, file_path);
16371
16408
  if (fs.existsSync(lib_source_path)) source_path = lib_source_path;
16372
16409
  }
16373
- let source = `// file://${source_path}\n`;
16374
- source += fs.readFileSync(source_path, "utf8");
16410
+ let source = fs.readFileSync(source_path, "utf8");
16411
+ if (test_src_dir && is_within$1(source_path, test_src_dir)) source = strip_main_functions(source);
16412
+ source = `// file://${source_path}\n` + source;
16375
16413
  source = source.replaceAll(/^import(.*)$/gm, (match, name) => {
16376
16414
  const trimmed = name.trim();
16377
16415
  if (trimmed === "System" || trimmed.startsWith("System/")) return match;
@@ -16381,6 +16419,105 @@ function add_source(folder_path, file_path, inputs, lib_path) {
16381
16419
  });
16382
16420
  inputs.set(file_path, source);
16383
16421
  }
16422
+ function is_within$1(child, parent) {
16423
+ const rel = path.relative(parent, child);
16424
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
16425
+ }
16426
+ /**
16427
+ * Remove every top-level `func main` (optionally `pub`) declaration, body and
16428
+ * all. Used when compiling a test: the generated harness provides `main`, so
16429
+ * the program's own `main` would collide as a duplicate declaration.
16430
+ */
16431
+ function strip_main_functions(source) {
16432
+ let result = "";
16433
+ let i = 0;
16434
+ const n = source.length;
16435
+ let depth = 0;
16436
+ while (i < n) {
16437
+ if (depth === 0 && is_main_declaration(source, i)) {
16438
+ i = skip_main_function(source, i);
16439
+ continue;
16440
+ }
16441
+ const ch = source[i];
16442
+ result += ch;
16443
+ if (ch === "\"" || ch === "'") {
16444
+ i++;
16445
+ let escaped = false;
16446
+ while (i < n) {
16447
+ const c = source[i];
16448
+ result += c;
16449
+ i++;
16450
+ if (escaped) escaped = false;
16451
+ else if (c === "\\") escaped = true;
16452
+ else if (c === ch) break;
16453
+ }
16454
+ continue;
16455
+ }
16456
+ if (ch === "{") depth++;
16457
+ if (ch === "}") depth--;
16458
+ i++;
16459
+ }
16460
+ return result;
16461
+ }
16462
+ function is_main_declaration(source, i) {
16463
+ if (i > 0 && source[i - 1] !== "\n") return false;
16464
+ const line_end = source.indexOf("\n", i);
16465
+ const line = line_end === -1 ? source.slice(i) : source.slice(i, line_end);
16466
+ return /^\s*(pub\s+)?func\s+main\b/.test(line);
16467
+ }
16468
+ function skip_main_function(source, i) {
16469
+ const n = source.length;
16470
+ let line_end = i;
16471
+ while (line_end < n && source[line_end] !== "\n") line_end++;
16472
+ const decl_line = source.slice(i, line_end);
16473
+ const brace = decl_line.indexOf("{");
16474
+ if (brace === -1) {
16475
+ if (decl_line.includes("=>")) return line_end < n ? line_end + 1 : n;
16476
+ let j = line_end;
16477
+ while (j < n && source[j] !== "{") {
16478
+ if (source[j] === "\"" || source[j] === "'") {
16479
+ const quote = source[j];
16480
+ j++;
16481
+ while (j < n && source[j] !== quote) j++;
16482
+ }
16483
+ j++;
16484
+ }
16485
+ if (j >= n) return n;
16486
+ return skip_balanced_block(source, j);
16487
+ }
16488
+ return skip_balanced_block(source, i + brace);
16489
+ }
16490
+ function skip_balanced_block(source, brace) {
16491
+ const n = source.length;
16492
+ let depth = 0;
16493
+ let j = brace;
16494
+ while (j < n) {
16495
+ const ch = source[j];
16496
+ if (ch === "\"" || ch === "'") {
16497
+ const quote = ch;
16498
+ j++;
16499
+ let escaped = false;
16500
+ while (j < n) {
16501
+ const c = source[j++];
16502
+ if (escaped) escaped = false;
16503
+ else if (c === "\\") escaped = true;
16504
+ else if (c === quote) break;
16505
+ }
16506
+ continue;
16507
+ }
16508
+ if (ch === "{") depth++;
16509
+ if (ch === "}") {
16510
+ depth--;
16511
+ if (depth === 0) {
16512
+ j++;
16513
+ while (j < n && (source[j] === "\r" || source[j] === "\n")) j++;
16514
+ return j;
16515
+ }
16516
+ }
16517
+ j++;
16518
+ }
16519
+ return n;
16520
+ }
16384
16521
  //#endregion
16385
16522
  //#region ../src/lib.ts
16386
16523
  function read_library_config(lib_dir) {
@@ -22513,6 +22650,7 @@ function dedupe_warnings(warnings) {
22513
22650
  function warn_unused_function(func, referenced, status, struct) {
22514
22651
  if (func.is_library || struct && struct.is_library) return;
22515
22652
  if (func.name === "main" || is_discard(func.name) || func.name.startsWith("#")) return;
22653
+ if (func.visibility === "pub") return;
22516
22654
  if (func.is_generic) return;
22517
22655
  if (struct && struct.traits.length > 0) return;
22518
22656
  if (referenced.has(func.name)) return;
@@ -25084,22 +25222,22 @@ function package_jsonc(name) {
25084
25222
  "entry": "src/main.nm"
25085
25223
  // The System library is resolved automatically from your nomen-lang
25086
25224
  // install. To pin a local checkout instead, uncomment:
25087
- // "imports": { "System": "../core" }
25225
+ // "imports": { "System": "~/Source/nomen/core" }
25088
25226
  }
25089
25227
  `;
25090
25228
  }
25091
25229
  const MAIN_NM = `import System
25092
25230
 
25231
+ pub func add = (int a, int b) => a + b
25232
+
25093
25233
  pub func main = (Init init) {
25094
- Console.write("Hello world!\\n")
25234
+ Console.write("Hello world, 2 + 2 is \\{add(2, 2)}!\\n")
25095
25235
  }
25096
25236
  `;
25097
25237
  const TEST_NM = `import System
25098
25238
  import System/Test
25099
25239
 
25100
- func add = (int a, int b, out int) => a + b
25101
-
25102
- pub func test_smoke = (ref Tester t) {
25240
+ pub func test_add = (ref Tester t) {
25103
25241
  t.expect(add(2, 2) == 4, "2 + 2 should equal 4")
25104
25242
  }
25105
25243
  `;
@@ -25164,19 +25302,43 @@ function run_init(name) {
25164
25302
  fs.writeFileSync(path.join(target, ".gitignore"), GITIGNORE);
25165
25303
  fs.writeFileSync(path.join(target, "package.jsonc"), package_jsonc(name));
25166
25304
  fs.writeFileSync(path.join(src, "main.nm"), MAIN_NM);
25167
- fs.writeFileSync(path.join(test, "smoke.test.nm"), TEST_NM);
25305
+ fs.writeFileSync(path.join(test, "main.test.nm"), TEST_NM);
25168
25306
  fs.writeFileSync(path.join(target, "README.md"), readme(name));
25169
25307
  fs.writeFileSync(path.join(target, "AGENTS.md"), fs.readFileSync(agents_template));
25170
25308
  console.log(`Created ${name}/`);
25171
25309
  console.log(` ${name}/.gitignore`);
25172
25310
  console.log(` ${name}/package.jsonc`);
25173
25311
  console.log(` ${name}/src/main.nm`);
25174
- console.log(` ${name}/test/smoke.test.nm`);
25312
+ console.log(` ${name}/test/main.test.nm`);
25175
25313
  console.log(` ${name}/README.md`);
25176
25314
  console.log(` ${name}/AGENTS.md`);
25177
25315
  console.log(`\nNext: cd ${name} && nomen run`);
25178
25316
  }
25179
25317
  //#endregion
25318
+ //#region src/paths.ts
25319
+ /**
25320
+ * The project root for `input_path`: the nearest ancestor folder (or the input
25321
+ * folder itself) containing a `package.jsonc`. Falls back to the input's own
25322
+ * folder for standalone files, so builds stay next to their source.
25323
+ */
25324
+ function project_root_for(input_path) {
25325
+ let dir = fs.lstatSync(input_path).isDirectory() ? input_path : path.dirname(input_path);
25326
+ for (let i = 0; i < 20; i++) {
25327
+ if (fs.existsSync(path.join(dir, "package.jsonc"))) return dir;
25328
+ const parent = path.dirname(dir);
25329
+ if (parent === dir) break;
25330
+ dir = parent;
25331
+ }
25332
+ return path.dirname(input_path);
25333
+ }
25334
+ /**
25335
+ * The folder that receives compiler output for `input_path`: the project's
25336
+ * `build/`, or `build/test` for `*.test.nm` files.
25337
+ */
25338
+ function build_dir_for(input_path, is_test) {
25339
+ return is_test ? path.join(project_root_for(input_path), "build", "test") : path.join(project_root_for(input_path), "build");
25340
+ }
25341
+ //#endregion
25180
25342
  //#region src/test.ts
25181
25343
  const RECORD_PREFIX = "\\nomen|";
25182
25344
  /** Recursively collect `*.test.nm` files under `root`, skipping build output. */
@@ -25357,7 +25519,7 @@ function run_test_file(entry_path, lib_path, arch) {
25357
25519
  result.ms = performance.now() - start;
25358
25520
  return result;
25359
25521
  }
25360
- const buildDir = path.join(path.dirname(resolved), "build");
25522
+ const buildDir = build_dir_for(resolved, true);
25361
25523
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
25362
25524
  const ext = arch === "aarch64" ? ".s" : ".c";
25363
25525
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
@@ -25443,8 +25605,8 @@ function report_file(result) {
25443
25605
  const mark = result.ok ? C.green("✓") : C.red("✗");
25444
25606
  console.log(` ${mark} ${rel} ${C.dim(`(${totalTests} tests)`)} ${C.dim(format_duration(result.ms))}`);
25445
25607
  if (result.crashed) console.log(C.red(` ${result.crashed}`));
25446
- for (const f of result.fails) console.log(` ${C.red("✗")} ${C.bold(f.test)} ${C.dim(">")} ${f.message}`);
25447
- for (const b of result.benches) console.log(` ${C.cyan("⏱")} ${C.bold(b.label)} ${C.dim(`(n=${b.n})`)} ${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`);
25608
+ for (const f of result.fails) console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
25609
+ for (const b of result.benches) console.log(` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`);
25448
25610
  if (result.other.length) {
25449
25611
  console.log(C.dim(" --- test stdout ---"));
25450
25612
  for (const line of result.other) console.log(C.dim(` ${line}`));
@@ -25455,7 +25617,7 @@ function runTests(root, options = {}) {
25455
25617
  const arch = options.arch ?? "aarch64";
25456
25618
  const lib = resolve_lib_for(root);
25457
25619
  const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
25458
- console.log(`\n~ NOMEN TEST ~ ${files.length} file(s)\n`);
25620
+ console.log(`Found ${files.length} test file(s)\n`);
25459
25621
  const startTime = performance.now();
25460
25622
  const results = [];
25461
25623
  for (const file of files) {
@@ -25465,14 +25627,16 @@ function runTests(root, options = {}) {
25465
25627
  }
25466
25628
  const elapsed = performance.now() - startTime;
25467
25629
  const totalFiles = results.length;
25630
+ const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
25631
+ const totalFilesPassed = totalFiles - totalFilesFailed;
25468
25632
  const totalTests = results.reduce((a, r) => a + r.tests.length, 0);
25469
25633
  const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
25470
25634
  const totalPassed = totalTests - totalFailed;
25471
25635
  const anyFailed = results.some((r) => !r.ok);
25472
25636
  console.log("");
25473
- console.log(` ${C.bold("Files ")} ${totalFiles} ${anyFailed ? C.red("failed") : C.green("passed")} (${totalFiles})`);
25474
- console.log(` ${C.bold("Tests ")} ${totalPassed} ${C.green("passed")}` + (totalFailed ? ` | ${C.red(`${totalFailed} failed`)}` : "") + ` (${totalTests})`);
25475
- console.log(` ${C.bold("Time ")} ${format_duration(elapsed)}`);
25637
+ console.log(` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` + (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") + C.dim(` (${totalFiles})`));
25638
+ console.log(` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` + (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") + C.dim(` (${totalTests})`));
25639
+ console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
25476
25640
  console.log("");
25477
25641
  return !anyFailed;
25478
25642
  }
@@ -25535,8 +25699,7 @@ function collect_nm_files(folder) {
25535
25699
  }
25536
25700
  return out;
25537
25701
  }
25538
- let build_root;
25539
- console.log("\n~ NOMEN ~\n");
25702
+ console.error("\n~ NOMEN ~\n");
25540
25703
  const args = parse_args();
25541
25704
  const command = args.command;
25542
25705
  try {
@@ -25596,7 +25759,6 @@ try {
25596
25759
  }
25597
25760
  args.in = args.in ?? resolve_input();
25598
25761
  if (!args.in) process.exit(0);
25599
- if (!build_root) build_root = fs.lstatSync(args.in).isDirectory() ? args.in : path.dirname(args.in);
25600
25762
  if (fs.existsSync(args.in)) {
25601
25763
  let config = {
25602
25764
  arch: "aarch64",
@@ -25626,19 +25788,13 @@ function resolve_input() {
25626
25788
  if (fs.existsSync(config_path)) try {
25627
25789
  const json = fs.readFileSync(config_path, "utf8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
25628
25790
  const parsed = JSON.parse(json);
25629
- if (parsed.entry) {
25630
- build_root = cwd;
25631
- return path.resolve(cwd, parsed.entry);
25632
- }
25791
+ if (parsed.entry) return path.resolve(cwd, parsed.entry);
25633
25792
  } catch {}
25634
25793
  let nm_files = [];
25635
25794
  try {
25636
25795
  nm_files = fs.readdirSync(cwd).filter((f) => f.endsWith(".nm"));
25637
25796
  } catch {}
25638
- if (nm_files.length === 1) {
25639
- build_root = cwd;
25640
- return path.resolve(cwd, nm_files[0]);
25641
- }
25797
+ if (nm_files.length === 1) return path.resolve(cwd, nm_files[0]);
25642
25798
  if (nm_files.length > 1) {
25643
25799
  console.log(`Found ${nm_files.length} .nm files in ${cwd}. Specify which to run with --in:\n ` + nm_files.join("\n "));
25644
25800
  return;
@@ -25731,7 +25887,7 @@ function processFile(filename, config, mode) {
25731
25887
  return;
25732
25888
  }
25733
25889
  const basename = path.basename(filename, ".nm");
25734
- const buildDir = path.join(build_root ?? path.dirname(filename), "build");
25890
+ const buildDir = build_dir_for(resolved_path, filename.endsWith(".test.nm"));
25735
25891
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
25736
25892
  const ext = arch === "aarch64" ? ".s" : platform === "macos" || platform === "ios" ? ".m" : ".c";
25737
25893
  const headerfile = path.join(buildDir, "main.h");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ 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
16
  import { find_bundled, run_init } from "./init.ts";
17
+ import { build_dir_for } from "./paths.ts";
17
18
  import { runTests } from "./test.ts";
18
19
  import type Config from "./types/Config.ts";
19
20
 
@@ -66,12 +67,7 @@ function collect_nm_files(folder: string): string[] {
66
67
  return out;
67
68
  }
68
69
 
69
- // The folder whose `build/` subdirectory receives compiler output. Set during
70
- // input resolution: the --in folder, the .nm file's folder, or — for
71
- // package.jsonc discovery — the package folder (cwd), not the entry's folder.
72
- let build_root: string | undefined;
73
-
74
- console.log("\n~ NOMEN ~\n");
70
+ console.error("\n~ NOMEN ~\n");
75
71
 
76
72
  const args: Args = parse_args();
77
73
  const command = args.command;
@@ -154,11 +150,6 @@ try {
154
150
  if (!args.in) {
155
151
  process.exit(0);
156
152
  }
157
- // For an explicit --in, build next to whatever was passed (the folder
158
- // itself, or the file's folder). Discovery cases set build_root themselves.
159
- if (!build_root) {
160
- build_root = fs.lstatSync(args.in).isDirectory() ? args.in : path.dirname(args.in);
161
- }
162
153
 
163
154
  if (fs.existsSync(args.in)) {
164
155
  let config: Config = { arch: "aarch64", platform: default_platform() };
@@ -213,7 +204,6 @@ function resolve_input(): string | undefined {
213
204
  const json = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
214
205
  const parsed = JSON.parse(json);
215
206
  if (parsed.entry) {
216
- build_root = cwd;
217
207
  return path.resolve(cwd, parsed.entry);
218
208
  }
219
209
  } catch {
@@ -229,7 +219,6 @@ function resolve_input(): string | undefined {
229
219
  // unreadable working folder
230
220
  }
231
221
  if (nm_files.length === 1) {
232
- build_root = cwd;
233
222
  return path.resolve(cwd, nm_files[0]);
234
223
  }
235
224
  if (nm_files.length > 1) {
@@ -368,7 +357,8 @@ function processFile(filename: string, config: Config, mode: Mode) {
368
357
  }
369
358
 
370
359
  const basename = path.basename(filename, ".nm");
371
- const buildDir = path.join(build_root ?? path.dirname(filename), "build");
360
+ // Output lives in the project's `build/` — `build/test` for *.test.nm.
361
+ const buildDir = build_dir_for(resolved_path, filename.endsWith(".test.nm"));
372
362
  if (!fs.existsSync(buildDir)) {
373
363
  fs.mkdirSync(buildDir, { recursive: true });
374
364
  }
package/src/init.ts CHANGED
@@ -16,24 +16,24 @@ function package_jsonc(name: string): string {
16
16
  "entry": "src/main.nm"
17
17
  // The System library is resolved automatically from your nomen-lang
18
18
  // install. To pin a local checkout instead, uncomment:
19
- // "imports": { "System": "../core" }
19
+ // "imports": { "System": "~/Source/nomen/core" }
20
20
  }
21
21
  `;
22
22
  }
23
23
 
24
24
  const MAIN_NM = `import System
25
25
 
26
+ pub func add = (int a, int b) => a + b
27
+
26
28
  pub func main = (Init init) {
27
- Console.write("Hello world!\\n")
29
+ Console.write("Hello world, 2 + 2 is \\{add(2, 2)}!\\n")
28
30
  }
29
31
  `;
30
32
 
31
33
  const TEST_NM = `import System
32
34
  import System/Test
33
35
 
34
- func add = (int a, int b, out int) => a + b
35
-
36
- pub func test_smoke = (ref Tester t) {
36
+ pub func test_add = (ref Tester t) {
37
37
  t.expect(add(2, 2) == 4, "2 + 2 should equal 4")
38
38
  }
39
39
  `;
@@ -113,7 +113,7 @@ export function run_init(name: string | undefined): void {
113
113
  fs.writeFileSync(path.join(target, ".gitignore"), GITIGNORE);
114
114
  fs.writeFileSync(path.join(target, "package.jsonc"), package_jsonc(name));
115
115
  fs.writeFileSync(path.join(src, "main.nm"), MAIN_NM);
116
- fs.writeFileSync(path.join(test, "smoke.test.nm"), TEST_NM);
116
+ fs.writeFileSync(path.join(test, "main.test.nm"), TEST_NM);
117
117
  fs.writeFileSync(path.join(target, "README.md"), readme(name));
118
118
  fs.writeFileSync(path.join(target, "AGENTS.md"), fs.readFileSync(agents_template));
119
119
 
@@ -121,7 +121,7 @@ export function run_init(name: string | undefined): void {
121
121
  console.log(` ${name}/.gitignore`);
122
122
  console.log(` ${name}/package.jsonc`);
123
123
  console.log(` ${name}/src/main.nm`);
124
- console.log(` ${name}/test/smoke.test.nm`);
124
+ console.log(` ${name}/test/main.test.nm`);
125
125
  console.log(` ${name}/README.md`);
126
126
  console.log(` ${name}/AGENTS.md`);
127
127
  console.log(`\nNext: cd ${name} && nomen run`);
package/src/paths.ts ADDED
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * The project root for `input_path`: the nearest ancestor folder (or the input
6
+ * folder itself) containing a `package.jsonc`. Falls back to the input's own
7
+ * folder for standalone files, so builds stay next to their source.
8
+ */
9
+ export function project_root_for(input_path: string): string {
10
+ let dir = fs.lstatSync(input_path).isDirectory() ? input_path : path.dirname(input_path);
11
+ for (let i = 0; i < 20; i++) {
12
+ if (fs.existsSync(path.join(dir, "package.jsonc"))) return dir;
13
+ const parent = path.dirname(dir);
14
+ if (parent === dir) break;
15
+ dir = parent;
16
+ }
17
+ return path.dirname(input_path);
18
+ }
19
+
20
+ /**
21
+ * The folder that receives compiler output for `input_path`: the project's
22
+ * `build/`, or `build/test` for `*.test.nm` files.
23
+ */
24
+ export function build_dir_for(input_path: string, is_test: boolean): string {
25
+ return is_test
26
+ ? path.join(project_root_for(input_path), "build", "test")
27
+ : path.join(project_root_for(input_path), "build");
28
+ }
package/src/test.ts CHANGED
@@ -8,6 +8,7 @@ import join from "../../src/join.ts";
8
8
  import { get_library } from "../../src/lib.ts";
9
9
  import parse from "../../src/parse.ts";
10
10
  import { find_bundled } from "./init.ts";
11
+ import { build_dir_for } from "./paths.ts";
11
12
 
12
13
  const RECORD_PREFIX = "\\nomen|";
13
14
 
@@ -303,7 +304,7 @@ export function run_test_file(
303
304
  return result;
304
305
  }
305
306
 
306
- const buildDir = path.join(path.dirname(resolved), "build");
307
+ const buildDir = build_dir_for(resolved, true);
307
308
  if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
308
309
  const ext = arch === "aarch64" ? ".s" : ".c";
309
310
  const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
@@ -407,11 +408,11 @@ export function report_file(result: TestFileResult): void {
407
408
  // short-circuiting a test has at most one failure, so the fail lines are
408
409
  // the per-test detail and a separate count line would just repeat names.
409
410
  for (const f of result.fails) {
410
- console.log(` ${C.red("✗")} ${C.bold(f.test)} ${C.dim(">")} ${f.message}`);
411
+ console.log(` ${C.red("✗")} ${f.test} ${C.dim(">")} ${f.message}`);
411
412
  }
412
413
  for (const b of result.benches) {
413
414
  console.log(
414
- ` ${C.cyan("⏱")} ${C.bold(b.label)} ${C.dim(`(n=${b.n})`)} ` +
415
+ ` ${C.cyan("⏱")} ${b.label} ${C.dim(`(n=${b.n})`)} ` +
415
416
  `${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ` +
416
417
  `${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ` +
417
418
  `${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`,
@@ -434,7 +435,8 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
434
435
  const lib = resolve_lib_for(root);
435
436
  const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
436
437
 
437
- console.log(`\n~ NOMEN TEST ~ ${files.length} file(s)\n`);
438
+ console.log(`Found ${files.length} test file(s)\n`);
439
+
438
440
  const startTime = performance.now();
439
441
 
440
442
  const results: TestFileResult[] = [];
@@ -446,6 +448,8 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
446
448
 
447
449
  const elapsed = performance.now() - startTime;
448
450
  const totalFiles = results.length;
451
+ const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
452
+ const totalFilesPassed = totalFiles - totalFilesFailed;
449
453
  // `tests[].failed` (from each test's `done` record) is the authoritative
450
454
  // failure count; `result.fails` holds the same failures' messages for
451
455
  // display, so don't sum both or failures are double-counted.
@@ -455,15 +459,20 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
455
459
  const anyFailed = results.some((r) => !r.ok);
456
460
 
457
461
  console.log("");
462
+ //console.log(
463
+ // ` ${C.dim("Files ")} ${totalFiles} ${anyFailed ? C.red("failed") : C.green("passed")} (${totalFiles})`,
464
+ //);
458
465
  console.log(
459
- ` ${C.bold("Files ")} ${totalFiles} ${anyFailed ? C.red("failed") : C.green("passed")} (${totalFiles})`,
466
+ ` ${C.dim("Files ")} ${C.green(`${totalFilesPassed} passed`)}` +
467
+ (totalFilesFailed ? ` ${C.dim("|")} ${C.red(`${totalFilesFailed} failed`)}` : "") +
468
+ C.dim(` (${totalFiles})`),
460
469
  );
461
470
  console.log(
462
- ` ${C.bold("Tests ")} ${totalPassed} ${C.green("passed")}` +
463
- (totalFailed ? ` | ${C.red(`${totalFailed} failed`)}` : "") +
464
- ` (${totalTests})`,
471
+ ` ${C.dim("Tests ")} ${C.green(`${totalPassed} passed`)}` +
472
+ (totalFailed ? ` ${C.dim("|")} ${C.red(`${totalFailed} failed`)}` : "") +
473
+ C.dim(` (${totalTests})`),
465
474
  );
466
- console.log(` ${C.bold("Time ")} ${format_duration(elapsed)}`);
475
+ console.log(` ${C.dim(" Time ")} ${format_duration(elapsed)}`);
467
476
  console.log("");
468
477
 
469
478
  return !anyFailed;