nomen-lang 0.0.6 → 0.0.8
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 +235 -0
- package/core/System/Ansi.nm +150 -0
- package/core/System/Array.nm +358 -0
- package/core/System/BigInt.nm +887 -0
- package/core/System/Buffer.nm +526 -0
- package/core/System/Channel.nm +198 -0
- package/core/System/ClassBuffer.nm +229 -0
- package/core/System/Console.nm +211 -0
- package/core/System/Controls/Button.nm +138 -0
- package/core/System/Controls/CheckBox.nm +158 -0
- package/core/System/Controls/Container.nm +1562 -0
- package/core/System/Controls/Control.nm +28 -0
- package/core/System/Controls/Geometry.nm +101 -0
- package/core/System/Controls/Layout.nm +228 -0
- package/core/System/Controls/LayoutParams.nm +50 -0
- package/core/System/Controls/Text.nm +129 -0
- package/core/System/Controls/TextBox.nm +157 -0
- package/core/System/Controls/Window.nm +280 -0
- package/core/System/Disposable.nm +8 -0
- package/core/System/Enumerable.nm +10 -0
- package/core/System/Equatable.nm +10 -0
- package/core/System/Graph.nm +68 -0
- package/core/System/Hashable.nm +13 -0
- package/core/System/Init.nm +21 -0
- package/core/System/LinkedList.nm +64 -0
- package/core/System/List.nm +44 -0
- package/core/System/Map.nm +153 -0
- package/core/System/Math.nm +54 -0
- package/core/System/Mutex.nm +78 -0
- package/core/System/Nursery.nm +27 -0
- package/core/System/Sendable.nm +9 -0
- package/core/System/Set.nm +97 -0
- package/core/System/Stream/Directory.nm +176 -0
- package/core/System/Stream/File.nm +432 -0
- package/core/System/Stream/Http.nm +453 -0
- package/core/System/String.nm +154 -0
- package/core/System/StringBuilder.nm +249 -0
- package/core/System/Stringable.nm +8 -0
- package/core/System/Task.nm +263 -0
- package/core/System/Test.nm +241 -0
- package/core/System/Text/Json.nm +377 -0
- package/core/System/Text/JsonTree.nm +187 -0
- package/core/System/Text/Regex.nm +655 -0
- package/core/System/Time.nm +112 -0
- package/core/System/Tree.nm +70 -0
- package/core/System/Viewable.nm +27 -0
- package/core/System/bool.nm +66 -0
- package/core/System/char.nm +42 -0
- package/core/System/float.nm +43 -0
- package/core/System/int.nm +54 -0
- package/core/System/int64.nm +42 -0
- package/core/System/int8.nm +42 -0
- package/core/System/uint.nm +42 -0
- package/core/System/uint64.nm +42 -0
- package/core/System/uint8.nm +42 -0
- package/core/docs/System/Ansi.md +40 -0
- package/core/docs/System/Array.md +16 -0
- package/core/docs/System/BigInt.md +30 -0
- package/core/docs/System/Buffer.md +31 -0
- package/core/docs/System/Channel.md +10 -0
- package/core/docs/System/ClassBuffer.md +15 -0
- package/core/docs/System/Console.md +13 -0
- package/core/docs/System/Controls/Button.md +12 -0
- package/core/docs/System/Controls/CheckBox.md +15 -0
- package/core/docs/System/Controls/Container.md +82 -0
- package/core/docs/System/Controls/Control.md +5 -0
- package/core/docs/System/Controls/Geometry.md +26 -0
- package/core/docs/System/Controls/Layout.md +37 -0
- package/core/docs/System/Controls/LayoutParams.md +13 -0
- package/core/docs/System/Controls/Text.md +12 -0
- package/core/docs/System/Controls/TextBox.md +13 -0
- package/core/docs/System/Controls/Window.md +20 -0
- package/core/docs/System/Disposable.md +5 -0
- package/core/docs/System/Enumerable.md +5 -0
- package/core/docs/System/Graph.md +17 -0
- package/core/docs/System/Init.md +9 -0
- package/core/docs/System/LinkedList.md +16 -0
- package/core/docs/System/List.md +13 -0
- package/core/docs/System/Map.md +14 -0
- package/core/docs/System/Math.md +11 -0
- package/core/docs/System/Mutex.md +10 -0
- package/core/docs/System/Nursery.md +5 -0
- package/core/docs/System/Sendable.md +5 -0
- package/core/docs/System/Set.md +14 -0
- package/core/docs/System/Stream/Directory.md +12 -0
- package/core/docs/System/Stream/File.md +21 -0
- package/core/docs/System/String.md +14 -0
- package/core/docs/System/StringBuilder.md +12 -0
- package/core/docs/System/Stringable.md +5 -0
- package/core/docs/System/Task.md +15 -0
- package/core/docs/System/Text/Json.md +12 -0
- package/core/docs/System/Text/JsonTree.md +21 -0
- package/core/docs/System/Text/Regex.md +12 -0
- package/core/docs/System/Time.md +11 -0
- package/core/docs/System/Tree.md +18 -0
- package/core/docs/System/Viewable.md +5 -0
- package/core/docs/System/bool.md +9 -0
- package/core/docs/System/char.md +9 -0
- package/core/docs/System/float.md +9 -0
- package/core/docs/System/int.md +10 -0
- package/core/docs/System/int64.md +9 -0
- package/core/docs/System/int8.md +9 -0
- package/core/docs/System/uint.md +9 -0
- package/core/docs/System/uint64.md +9 -0
- package/core/docs/System/uint8.md +9 -0
- package/core/package.jsonc +4 -0
- package/dist/index.mjs +17039 -21482
- package/package.json +33 -30
- package/src/args.ts +171 -0
- package/src/bench_loop.nm +113 -0
- package/src/index.ts +61 -95
- package/src/init.ts +128 -0
- package/src/test.ts +522 -0
- package/tsconfig.json +0 -21
- package/vite.config.ts +0 -7
package/src/init.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
// A name must be a portable folder name: letters, digits, underscore and
|
|
6
|
+
// hyphen only. Anything else (slashes, spaces, dots) would either break the
|
|
7
|
+
// filesystem layout or produce an invalid package name.
|
|
8
|
+
const NAME_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
9
|
+
|
|
10
|
+
const GITIGNORE = `build/
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
function package_jsonc(name: string): string {
|
|
14
|
+
return `{
|
|
15
|
+
"name": "${name}",
|
|
16
|
+
"entry": "src/main.nm"
|
|
17
|
+
// The System library is resolved automatically from your nomen-lang
|
|
18
|
+
// install. To pin a local checkout instead, uncomment:
|
|
19
|
+
// "imports": { "System": "../core" }
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MAIN_NM = `import System
|
|
25
|
+
|
|
26
|
+
pub func main = (Init init) {
|
|
27
|
+
Console.write("Hello world!\\n")
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const TEST_NM = `import System
|
|
32
|
+
import System/Test
|
|
33
|
+
|
|
34
|
+
func add = (int a, int b, out int) => a + b
|
|
35
|
+
|
|
36
|
+
pub func test_smoke = (ref Tester t) {
|
|
37
|
+
t.expect(add(2, 2) == 4, "2 + 2 should equal 4")
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
function readme(name: string): string {
|
|
42
|
+
return `# ${name}
|
|
43
|
+
|
|
44
|
+
A [Nomen](https://github.com/andrewjk/nomen) project.
|
|
45
|
+
|
|
46
|
+
## Build
|
|
47
|
+
|
|
48
|
+
\`\`\`bash
|
|
49
|
+
nomen build
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
## Run
|
|
53
|
+
|
|
54
|
+
\`\`\`bash
|
|
55
|
+
nomen run
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
## Test
|
|
59
|
+
|
|
60
|
+
\`\`\`bash
|
|
61
|
+
nomen test
|
|
62
|
+
\`\`\`
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resolve a bundled asset (core/ or NOMEN_AGENTS.md) shipped alongside the
|
|
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.
|
|
69
|
+
export function find_bundled(filename: string): string | undefined {
|
|
70
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
71
|
+
const cli_root = path.dirname(here);
|
|
72
|
+
const candidates = [path.join(cli_root, filename), path.join(cli_root, "..", filename)];
|
|
73
|
+
for (const c of candidates) {
|
|
74
|
+
if (fs.existsSync(c)) return path.resolve(c);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
|
|
81
|
+
* starting layout (package.jsonc, src/main.nm, a starter test, README and an
|
|
82
|
+
* AGENTS.md copied from the bundled NOMEN_AGENTS.md template).
|
|
83
|
+
*/
|
|
84
|
+
export function run_init(name: string | undefined): void {
|
|
85
|
+
if (!name) {
|
|
86
|
+
console.log("Usage: nomen init <name>");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
if (!NAME_RE.test(name)) {
|
|
90
|
+
console.log(
|
|
91
|
+
`Invalid project name "${name}". Use letters, digits, '-' or '_' (must start with a letter).`,
|
|
92
|
+
);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const target = path.resolve(process.cwd(), name);
|
|
97
|
+
if (fs.existsSync(target)) {
|
|
98
|
+
console.log(`Already exists: ${target}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const agents_template = find_bundled("NOMEN_AGENTS.md");
|
|
103
|
+
if (!agents_template) {
|
|
104
|
+
console.log("Could not locate NOMEN_AGENTS.md template alongside the CLI.");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const src = path.join(target, "src");
|
|
109
|
+
const test = path.join(target, "test");
|
|
110
|
+
fs.mkdirSync(src, { recursive: true });
|
|
111
|
+
fs.mkdirSync(test, { recursive: true });
|
|
112
|
+
|
|
113
|
+
fs.writeFileSync(path.join(target, ".gitignore"), GITIGNORE);
|
|
114
|
+
fs.writeFileSync(path.join(target, "package.jsonc"), package_jsonc(name));
|
|
115
|
+
fs.writeFileSync(path.join(src, "main.nm"), MAIN_NM);
|
|
116
|
+
fs.writeFileSync(path.join(test, "smoke.test.nm"), TEST_NM);
|
|
117
|
+
fs.writeFileSync(path.join(target, "README.md"), readme(name));
|
|
118
|
+
fs.writeFileSync(path.join(target, "AGENTS.md"), fs.readFileSync(agents_template));
|
|
119
|
+
|
|
120
|
+
console.log(`Created ${name}/`);
|
|
121
|
+
console.log(` ${name}/.gitignore`);
|
|
122
|
+
console.log(` ${name}/package.jsonc`);
|
|
123
|
+
console.log(` ${name}/src/main.nm`);
|
|
124
|
+
console.log(` ${name}/test/smoke.test.nm`);
|
|
125
|
+
console.log(` ${name}/README.md`);
|
|
126
|
+
console.log(` ${name}/AGENTS.md`);
|
|
127
|
+
console.log(`\nNext: cd ${name} && nomen run`);
|
|
128
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import build from "../../src/build.ts";
|
|
7
|
+
import join from "../../src/join.ts";
|
|
8
|
+
import { get_library } from "../../src/lib.ts";
|
|
9
|
+
import parse from "../../src/parse.ts";
|
|
10
|
+
import { find_bundled } from "./init.ts";
|
|
11
|
+
|
|
12
|
+
const RECORD_PREFIX = "\\nomen|";
|
|
13
|
+
|
|
14
|
+
// A discovered test function: `pub func <name> = (ref Tester t)`.
|
|
15
|
+
export interface TestFunction {
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A discovered benchmark: `pub func <name> = (ref Tester t)` whose body calls
|
|
20
|
+
// `t.bench(label, fn)` (or `t.bench_n(...)`). `fn` is the function the harness
|
|
21
|
+
// must time.
|
|
22
|
+
export interface BenchFunction {
|
|
23
|
+
name: string;
|
|
24
|
+
// The function reference passed to `t.bench`/`t.bench_n`, e.g. `add_once`.
|
|
25
|
+
target: string;
|
|
26
|
+
// The label string passed to `t.bench`.
|
|
27
|
+
label: string;
|
|
28
|
+
// Explicit sample count from `bench_n`, or undefined for the default.
|
|
29
|
+
samples?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TestFileResult {
|
|
33
|
+
file: string;
|
|
34
|
+
ok: boolean;
|
|
35
|
+
tests: TestRecord[];
|
|
36
|
+
fails: FailRecord[];
|
|
37
|
+
benches: BenchRecord[];
|
|
38
|
+
other: string[];
|
|
39
|
+
// Set when the compiled binary crashed before finishing (e.g. a segfault
|
|
40
|
+
// in a test). We still surface whatever records it managed to emit.
|
|
41
|
+
crashed?: string;
|
|
42
|
+
ms: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TestRecord {
|
|
46
|
+
name: string;
|
|
47
|
+
passed: number;
|
|
48
|
+
failed: number;
|
|
49
|
+
ns: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface FailRecord {
|
|
53
|
+
test: string;
|
|
54
|
+
message: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface BenchRecord {
|
|
58
|
+
label: string;
|
|
59
|
+
n: number;
|
|
60
|
+
min: number;
|
|
61
|
+
median: number;
|
|
62
|
+
max: number;
|
|
63
|
+
mean: number;
|
|
64
|
+
stddev: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Recursively collect `*.test.nm` files under `root`, skipping build output. */
|
|
68
|
+
export function collect_test_files(root: string): string[] {
|
|
69
|
+
const out: string[] = [];
|
|
70
|
+
const walk = (dir: string) => {
|
|
71
|
+
let entries: fs.Dirent[];
|
|
72
|
+
try {
|
|
73
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
78
|
+
const full = path.join(dir, entry.name);
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
if (entry.name === "build" || entry.name === "node_modules") continue;
|
|
81
|
+
walk(full);
|
|
82
|
+
} else if (entry.name.endsWith(".test.nm")) {
|
|
83
|
+
out.push(full);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk(root);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Pull `pub func <name> = (ref Tester t)` declarations out of source text. */
|
|
92
|
+
export function extract_test_functions(source: string): TestFunction[] {
|
|
93
|
+
const tests: TestFunction[] = [];
|
|
94
|
+
// `pub func NAME = (ref Tester t)` — Tester may be passed by value too,
|
|
95
|
+
// but `ref` is required for mutation, so accept either.
|
|
96
|
+
const re = /pub\s+func\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\(\s*(?:ref\s+)?Tester\s+t\s*\)/g;
|
|
97
|
+
let m: RegExpExecArray | null;
|
|
98
|
+
while ((m = re.exec(source))) {
|
|
99
|
+
tests.push({ name: m[1] });
|
|
100
|
+
}
|
|
101
|
+
return tests;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Find benchmark functions: test functions whose body calls `t.bench(...)` or
|
|
106
|
+
* `t.bench_n(...)`. Returns the target function (the thing to time) and label.
|
|
107
|
+
*/
|
|
108
|
+
export function extract_bench_functions(source: string): BenchFunction[] {
|
|
109
|
+
const benches: BenchFunction[] = [];
|
|
110
|
+
const testRe = /pub\s+func\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\(\s*(?:ref\s+)?Tester\s+t\s*\)/g;
|
|
111
|
+
let m: RegExpExecArray | null;
|
|
112
|
+
while ((m = testRe.exec(source))) {
|
|
113
|
+
const name = m[1];
|
|
114
|
+
const bodyStart = testRe.lastIndex;
|
|
115
|
+
// Find the matching closing brace by counting depth.
|
|
116
|
+
let depth = 0;
|
|
117
|
+
let i = bodyStart;
|
|
118
|
+
let inStr: string | null = null;
|
|
119
|
+
let escaped = false;
|
|
120
|
+
for (; i < source.length; i++) {
|
|
121
|
+
const ch = source[i];
|
|
122
|
+
if (inStr) {
|
|
123
|
+
if (escaped) escaped = false;
|
|
124
|
+
else if (ch === "\\") escaped = true;
|
|
125
|
+
else if (ch === inStr) inStr = null;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '"' || ch === "'") inStr = ch;
|
|
129
|
+
else if (ch === "{") depth++;
|
|
130
|
+
else if (ch === "}") {
|
|
131
|
+
depth--;
|
|
132
|
+
if (depth === 0) break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const body = source.slice(bodyStart, i);
|
|
136
|
+
// `t.bench("label", fn)` or `t.bench_n("label", fn, 123)`
|
|
137
|
+
const benchRe =
|
|
138
|
+
/\bt\.bench(?:_n)?\s*\(\s*"([^"]*)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:,\s*(\d+)\s*)?\)/;
|
|
139
|
+
const bm = body.match(benchRe);
|
|
140
|
+
if (bm) {
|
|
141
|
+
benches.push({
|
|
142
|
+
name,
|
|
143
|
+
label: bm[1],
|
|
144
|
+
target: bm[2],
|
|
145
|
+
samples: bm[3] ? parseInt(bm[3], 10) : undefined,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return benches;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function escape_nm_string(s: string): string {
|
|
153
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Build the `main` + per-benchmark timing harness for one test file. */
|
|
157
|
+
export function generate_harness(tests: TestFunction[], benches: BenchFunction[]): string {
|
|
158
|
+
// A function that is also a benchmark must not run twice — exclude bench
|
|
159
|
+
// functions from the plain test list (they're driven by the bench loop).
|
|
160
|
+
const benchNames = new Set(benches.map((b) => b.name));
|
|
161
|
+
tests = tests.filter((t) => !benchNames.has(t.name));
|
|
162
|
+
|
|
163
|
+
const templatePath = path.join(
|
|
164
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
165
|
+
"..",
|
|
166
|
+
"src",
|
|
167
|
+
"bench_loop.nm",
|
|
168
|
+
);
|
|
169
|
+
const template = fs.readFileSync(templatePath, "utf8");
|
|
170
|
+
|
|
171
|
+
const benchLoops = benches
|
|
172
|
+
.map((b) => {
|
|
173
|
+
// Clamp the sample count: insertion-sorting the results is O(n²),
|
|
174
|
+
// so an absurd `bench_n` would dominate the suite. 4096 keeps the
|
|
175
|
+
// sort bounded while leaving plenty of samples for stable stats.
|
|
176
|
+
const raw = b.samples && b.samples > 0 ? b.samples : 1000;
|
|
177
|
+
const n = Math.min(raw, 4096);
|
|
178
|
+
return template
|
|
179
|
+
.replace(/__NAME__/g, b.name)
|
|
180
|
+
.replace(/__TARGET__/g, b.target)
|
|
181
|
+
.replace(/__N__/g, String(n));
|
|
182
|
+
})
|
|
183
|
+
.join("\n");
|
|
184
|
+
|
|
185
|
+
let main = "\nimport System\nimport System/Test\n\npub func main = () {\n";
|
|
186
|
+
main += "\tvar Tester t = Tester()\n";
|
|
187
|
+
for (const test of tests) {
|
|
188
|
+
main += `\tt.begin_test("${escape_nm_string(test.name)}")\n`;
|
|
189
|
+
main += `\t${test.name}(ref t)\n`;
|
|
190
|
+
main += `\tt.end_test()\n`;
|
|
191
|
+
}
|
|
192
|
+
for (const bench of benches) {
|
|
193
|
+
// Bench functions run after every test, so reset the per-test failure
|
|
194
|
+
// flag (otherwise a prior test's failure would make `t.bench` a no-op)
|
|
195
|
+
// and any leftover `bench_pending` from an earlier bench.
|
|
196
|
+
main += `\tt.has_failed = false\n`;
|
|
197
|
+
main += `\tt.bench_pending = false\n`;
|
|
198
|
+
main += `\t${bench.name}(ref t)\n`;
|
|
199
|
+
main += `\tif t.bench_pending {\n`;
|
|
200
|
+
main += `\t\tbench_loop_${bench.name}(ref t)\n`;
|
|
201
|
+
main += `\t}\n`;
|
|
202
|
+
}
|
|
203
|
+
main += "}\n";
|
|
204
|
+
return benchLoops + main;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface RunRecord {
|
|
208
|
+
tests: TestRecord[];
|
|
209
|
+
fails: FailRecord[];
|
|
210
|
+
benches: BenchRecord[];
|
|
211
|
+
other: string[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Parse the machine-readable records out of a test binary's stdout. */
|
|
215
|
+
export function parse_records(stdout: string): RunRecord {
|
|
216
|
+
const tests: TestRecord[] = [];
|
|
217
|
+
const fails: FailRecord[] = [];
|
|
218
|
+
const benches: BenchRecord[] = [];
|
|
219
|
+
const other: string[] = [];
|
|
220
|
+
for (const line of stdout.split("\n")) {
|
|
221
|
+
if (!line.startsWith(RECORD_PREFIX)) {
|
|
222
|
+
if (line.length) other.push(line);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const parts = line.slice(RECORD_PREFIX.length).split("|");
|
|
226
|
+
const kind = parts[0];
|
|
227
|
+
if (kind === "start") {
|
|
228
|
+
// no-op; the `done` record carries the result
|
|
229
|
+
} else if (kind === "done") {
|
|
230
|
+
tests.push({
|
|
231
|
+
name: parts[1],
|
|
232
|
+
passed: parseInt(parts[2] || "0", 10),
|
|
233
|
+
failed: parseInt(parts[3] || "0", 10),
|
|
234
|
+
ns: parseInt(parts[4] || "0", 10),
|
|
235
|
+
});
|
|
236
|
+
} else if (kind === "fail") {
|
|
237
|
+
fails.push({ test: parts[1], message: parts.slice(2).join("|") });
|
|
238
|
+
} else if (kind === "bench") {
|
|
239
|
+
benches.push({
|
|
240
|
+
label: parts[1],
|
|
241
|
+
n: parseInt(parts[2] || "0", 10),
|
|
242
|
+
min: parseInt(parts[3] || "0", 10),
|
|
243
|
+
median: parseInt(parts[4] || "0", 10),
|
|
244
|
+
max: parseInt(parts[5] || "0", 10),
|
|
245
|
+
mean: parseFloat(parts[6] || "0"),
|
|
246
|
+
stddev: parseFloat(parts[7] || "0"),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { tests, fails, benches, other };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Compile one `*.test.nm` file (with the generated harness) and run it,
|
|
255
|
+
* returning its parsed records and any crash info.
|
|
256
|
+
*/
|
|
257
|
+
export function run_test_file(
|
|
258
|
+
entry_path: string,
|
|
259
|
+
lib_path: string | undefined,
|
|
260
|
+
arch: string,
|
|
261
|
+
): TestFileResult {
|
|
262
|
+
const start = performance.now();
|
|
263
|
+
const source_text = fs.readFileSync(entry_path, "utf8");
|
|
264
|
+
const tests = extract_test_functions(source_text);
|
|
265
|
+
const benches = extract_bench_functions(source_text);
|
|
266
|
+
const harness = generate_harness(tests, benches);
|
|
267
|
+
|
|
268
|
+
const result: TestFileResult = {
|
|
269
|
+
file: entry_path,
|
|
270
|
+
ok: true,
|
|
271
|
+
tests: [],
|
|
272
|
+
fails: [],
|
|
273
|
+
benches: [],
|
|
274
|
+
other: [],
|
|
275
|
+
ms: 0,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const resolved = path.resolve(entry_path);
|
|
279
|
+
const input = join(resolved, lib_path);
|
|
280
|
+
const library = lib_path ? get_library(lib_path) : undefined;
|
|
281
|
+
// `harness` carries `import System`, so `parse` will resolve the library
|
|
282
|
+
// types (Tester, Buffer, Time, Math) and append them. The user source is
|
|
283
|
+
// already concatenated into `input` via `join`, which strips its `import`
|
|
284
|
+
// lines — `harness` re-supplies it so library resolution still triggers.
|
|
285
|
+
const source = input + "\n" + harness;
|
|
286
|
+
const parsed = parse(source, library, resolved);
|
|
287
|
+
if (parsed.errors.length) {
|
|
288
|
+
result.ok = false;
|
|
289
|
+
result.crashed = parsed.errors
|
|
290
|
+
.map((e) => `${e.message} (${e.line ?? "?"}:${e.column ?? "?"})`)
|
|
291
|
+
.join("\n");
|
|
292
|
+
result.ms = performance.now() - start;
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const buildResult = build(parsed.root, { arch: arch as "aarch64" | "c", audit: false });
|
|
297
|
+
if (buildResult.errors && buildResult.errors.length) {
|
|
298
|
+
result.ok = false;
|
|
299
|
+
result.crashed = buildResult.errors
|
|
300
|
+
.map((e) => `${e.message} (${e.line ?? "?"}:${e.column ?? "?"})`)
|
|
301
|
+
.join("\n");
|
|
302
|
+
result.ms = performance.now() - start;
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const buildDir = path.join(path.dirname(resolved), "build");
|
|
307
|
+
if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
|
|
308
|
+
const ext = arch === "aarch64" ? ".s" : ".c";
|
|
309
|
+
const codefile = path.join(buildDir, path.basename(entry_path, ".nm") + ext);
|
|
310
|
+
const outfile = path.join(buildDir, path.basename(entry_path, ".nm"));
|
|
311
|
+
// The C backend's generated source does `#include "main.h"`, so write the
|
|
312
|
+
// header next to the code. (aarch64 inlines everything into the .s file.)
|
|
313
|
+
fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
|
|
314
|
+
fs.writeFileSync(codefile, buildResult.code);
|
|
315
|
+
|
|
316
|
+
// Link the harness binary. The harness uses only Console/Time (printf,
|
|
317
|
+
// clock_gettime), so no platform frameworks are required.
|
|
318
|
+
try {
|
|
319
|
+
execFileSync("clang", ["-o", outfile, codefile], {
|
|
320
|
+
encoding: "utf8",
|
|
321
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
322
|
+
});
|
|
323
|
+
} catch (err: any) {
|
|
324
|
+
const stderr = err.stderr ? err.stderr.toString() : (err.message ?? "");
|
|
325
|
+
result.ok = false;
|
|
326
|
+
result.crashed = `link failed: ${stderr.trim() || "clang error"}`;
|
|
327
|
+
result.ms = performance.now() - start;
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Run the binary and collect the records it streams over stdout. A
|
|
332
|
+
// non-zero exit (crash, abort) still surfaces whatever records were
|
|
333
|
+
// emitted before the crash; a timeout is treated as a crash.
|
|
334
|
+
let runStdout = "";
|
|
335
|
+
let crashed: string | undefined;
|
|
336
|
+
try {
|
|
337
|
+
runStdout = execFileSync(outfile, [], {
|
|
338
|
+
encoding: "utf8",
|
|
339
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
340
|
+
timeout: 30_000,
|
|
341
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
342
|
+
});
|
|
343
|
+
} catch (err: any) {
|
|
344
|
+
runStdout = err.stdout ? err.stdout.toString() : "";
|
|
345
|
+
if (err.signal === "SIGTERM") {
|
|
346
|
+
crashed = "test binary timed out after 30s";
|
|
347
|
+
} else {
|
|
348
|
+
crashed = `test binary exited abnormally (signal ${err.signal ?? err.code})`;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const records = parse_records(runStdout);
|
|
353
|
+
result.tests = records.tests;
|
|
354
|
+
result.fails = records.fails;
|
|
355
|
+
result.benches = records.benches;
|
|
356
|
+
result.other = records.other;
|
|
357
|
+
result.crashed = crashed;
|
|
358
|
+
// A file "passes" when it reported no failed asserts and didn't crash.
|
|
359
|
+
const failed = result.fails.length > 0 || result.tests.some((t) => t.failed > 0);
|
|
360
|
+
result.ok = !failed && crashed === undefined;
|
|
361
|
+
|
|
362
|
+
result.ms = performance.now() - start;
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Format a millisecond duration (as returned by `performance.now()` deltas)
|
|
367
|
+
// as a compact human-readable string.
|
|
368
|
+
function format_duration(ms: number): string {
|
|
369
|
+
if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`;
|
|
370
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
371
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function fmt_ns(ns: number): string {
|
|
375
|
+
if (ns < 1000) return `${ns}ns`;
|
|
376
|
+
if (ns < 1e6) return `${(ns / 1000).toFixed(1)}µs`;
|
|
377
|
+
if (ns < 1e9) return `${(ns / 1e6).toFixed(1)}ms`;
|
|
378
|
+
return `${(ns / 1e9).toFixed(2)}s`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const C = {
|
|
382
|
+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
383
|
+
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
384
|
+
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
385
|
+
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
|
|
386
|
+
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
387
|
+
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/** Print a vitest-style report for one file's results. */
|
|
391
|
+
export function report_file(result: TestFileResult): void {
|
|
392
|
+
const rel = path.relative(process.cwd(), result.file);
|
|
393
|
+
if (result.crashed && result.tests.length === 0 && result.fails.length === 0) {
|
|
394
|
+
console.log(` ${C.red("✗")} ${rel} ${C.red("(failed to build)")}`);
|
|
395
|
+
console.log(C.red(result.crashed));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const totalTests = result.tests.length;
|
|
399
|
+
const mark = result.ok ? C.green("✓") : C.red("✗");
|
|
400
|
+
console.log(
|
|
401
|
+
` ${mark} ${rel} ${C.dim(`(${totalTests} tests)`)} ${C.dim(format_duration(result.ms))}`,
|
|
402
|
+
);
|
|
403
|
+
if (result.crashed) {
|
|
404
|
+
console.log(C.red(` ${result.crashed}`));
|
|
405
|
+
}
|
|
406
|
+
// Each `fail` record already names its test and carries the message; with
|
|
407
|
+
// short-circuiting a test has at most one failure, so the fail lines are
|
|
408
|
+
// the per-test detail and a separate count line would just repeat names.
|
|
409
|
+
for (const f of result.fails) {
|
|
410
|
+
console.log(` ${C.red("✗")} ${C.bold(f.test)} ${C.dim(">")} ${f.message}`);
|
|
411
|
+
}
|
|
412
|
+
for (const b of result.benches) {
|
|
413
|
+
console.log(
|
|
414
|
+
` ${C.cyan("⏱")} ${C.bold(b.label)} ${C.dim(`(n=${b.n})`)} ` +
|
|
415
|
+
`${C.dim("min")} ${fmt_ns(b.min)} ${C.dim("median")} ${fmt_ns(b.median)} ` +
|
|
416
|
+
`${C.dim("mean")} ${fmt_ns(Math.round(b.mean))} ${C.dim("max")} ${fmt_ns(b.max)} ` +
|
|
417
|
+
`${C.dim("±")} ${fmt_ns(Math.round(b.stddev))}`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (result.other.length) {
|
|
421
|
+
console.log(C.dim(" --- test stdout ---"));
|
|
422
|
+
for (const line of result.other) console.log(C.dim(` ${line}`));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export interface RunTestsOptions {
|
|
427
|
+
arch?: string;
|
|
428
|
+
filter?: RegExp;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Discover, run, and report every `*.test.nm` under `root`. */
|
|
432
|
+
export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
433
|
+
const arch = options.arch ?? "aarch64";
|
|
434
|
+
const lib = resolve_lib_for(root);
|
|
435
|
+
const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
|
|
436
|
+
|
|
437
|
+
console.log(`\n~ NOMEN TEST ~ ${files.length} file(s)\n`);
|
|
438
|
+
const startTime = performance.now();
|
|
439
|
+
|
|
440
|
+
const results: TestFileResult[] = [];
|
|
441
|
+
for (const file of files) {
|
|
442
|
+
const result = run_test_file(file, lib, arch);
|
|
443
|
+
report_file(result);
|
|
444
|
+
results.push(result);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const elapsed = performance.now() - startTime;
|
|
448
|
+
const totalFiles = results.length;
|
|
449
|
+
// `tests[].failed` (from each test's `done` record) is the authoritative
|
|
450
|
+
// failure count; `result.fails` holds the same failures' messages for
|
|
451
|
+
// display, so don't sum both or failures are double-counted.
|
|
452
|
+
const totalTests = results.reduce((a, r) => a + r.tests.length, 0);
|
|
453
|
+
const totalFailed = results.reduce((a, r) => a + r.tests.reduce((x, t) => x + t.failed, 0), 0);
|
|
454
|
+
const totalPassed = totalTests - totalFailed;
|
|
455
|
+
const anyFailed = results.some((r) => !r.ok);
|
|
456
|
+
|
|
457
|
+
console.log("");
|
|
458
|
+
console.log(
|
|
459
|
+
` ${C.bold("Files ")} ${totalFiles} ${anyFailed ? C.red("failed") : C.green("passed")} (${totalFiles})`,
|
|
460
|
+
);
|
|
461
|
+
console.log(
|
|
462
|
+
` ${C.bold("Tests ")} ${totalPassed} ${C.green("passed")}` +
|
|
463
|
+
(totalFailed ? ` | ${C.red(`${totalFailed} failed`)}` : "") +
|
|
464
|
+
` (${totalTests})`,
|
|
465
|
+
);
|
|
466
|
+
console.log(` ${C.bold("Time ")} ${format_duration(elapsed)}`);
|
|
467
|
+
console.log("");
|
|
468
|
+
|
|
469
|
+
return !anyFailed;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Find the System library for a test run rooted at `root`. Mirrors the CLI's
|
|
473
|
+
// resolve_lib but also checks the repo's `core/System` layout directly.
|
|
474
|
+
function resolve_lib_for(root: string): string | undefined {
|
|
475
|
+
let dir = path.resolve(root);
|
|
476
|
+
// Climb to the filesystem root looking for the System library.
|
|
477
|
+
for (;;) {
|
|
478
|
+
const candidates = [
|
|
479
|
+
path.join(dir, "core", "System", "package.jsonc"),
|
|
480
|
+
path.join(dir, "core", "package.jsonc"),
|
|
481
|
+
path.join(dir, "package.jsonc"),
|
|
482
|
+
];
|
|
483
|
+
for (const c of candidates) {
|
|
484
|
+
if (fs.existsSync(c)) {
|
|
485
|
+
try {
|
|
486
|
+
const json = fs
|
|
487
|
+
.readFileSync(c, "utf8")
|
|
488
|
+
.replace(/\/\/.*$/gm, "")
|
|
489
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
490
|
+
const parsed = JSON.parse(json);
|
|
491
|
+
// Return the package directory (where this package.jsonc
|
|
492
|
+
// lives). `get_library` reads package.jsonc from here and
|
|
493
|
+
// finds sources under `<dir>/src` (a symlink to `System`
|
|
494
|
+
// in this repo); resolving `imports.System` to a subdir
|
|
495
|
+
// would point at the source folder instead, which
|
|
496
|
+
// `get_library` cannot use.
|
|
497
|
+
if (parsed.imports?.System) {
|
|
498
|
+
return path.dirname(c);
|
|
499
|
+
}
|
|
500
|
+
if (fs.existsSync(path.join(path.dirname(c), "System"))) {
|
|
501
|
+
return path.dirname(c);
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
// ignore
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// A bare `<dir>/System` is only the library if it has a package.jsonc;
|
|
509
|
+
// otherwise we'd match macOS's `/System` on the way to the root.
|
|
510
|
+
const systemDir = path.join(dir, "System");
|
|
511
|
+
if (fs.existsSync(path.join(systemDir, "package.jsonc"))) return systemDir;
|
|
512
|
+
const globalSystem = path.join(dir, "core", "System");
|
|
513
|
+
if (fs.existsSync(globalSystem)) {
|
|
514
|
+
return path.join(dir, "core");
|
|
515
|
+
}
|
|
516
|
+
const parent = path.dirname(dir);
|
|
517
|
+
if (parent === dir) break;
|
|
518
|
+
dir = parent;
|
|
519
|
+
}
|
|
520
|
+
// Nothing local — fall back to the System library bundled with the CLI.
|
|
521
|
+
return find_bundled("core");
|
|
522
|
+
}
|
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "esnext",
|
|
4
|
-
"lib": ["es2023"],
|
|
5
|
-
"moduleDetection": "force",
|
|
6
|
-
"module": "nodenext",
|
|
7
|
-
"moduleResolution": "nodenext",
|
|
8
|
-
"resolveJsonModule": true,
|
|
9
|
-
"types": ["node"],
|
|
10
|
-
"strict": true,
|
|
11
|
-
"noUnusedLocals": true,
|
|
12
|
-
"declaration": true,
|
|
13
|
-
"noEmit": true,
|
|
14
|
-
"allowImportingTsExtensions": true,
|
|
15
|
-
"esModuleInterop": true,
|
|
16
|
-
"isolatedModules": true,
|
|
17
|
-
"verbatimModuleSyntax": true,
|
|
18
|
-
"skipLibCheck": true
|
|
19
|
-
},
|
|
20
|
-
"include": ["src"]
|
|
21
|
-
}
|