nomen-lang 0.6.8 → 0.6.9
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/audit_runtime.c +50 -0
- package/core/System/StringBuilder.nm +10 -1
- package/dist/index.mjs +99 -35
- package/package.json +3 -2
- package/src/args.ts +7 -2
- package/src/index.ts +7 -3
- package/src/init.ts +10 -0
- package/src/test.ts +66 -28
package/audit_runtime.c
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#include <stdio.h>
|
|
2
|
+
#include <stdlib.h>
|
|
3
|
+
#include <string.h>
|
|
4
|
+
|
|
5
|
+
// Atomic: worker threads from the pool malloc/free concurrently, and a
|
|
6
|
+
// plain `long` would lose updates under that contention (showing up as
|
|
7
|
+
// spurious LEAK/negative-count failures in tests that use spawns).
|
|
8
|
+
static volatile long nomen_malloc_count = 0;
|
|
9
|
+
|
|
10
|
+
void *nomen_malloc_wrap(unsigned long size) {
|
|
11
|
+
void *ptr = malloc(size);
|
|
12
|
+
__atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
|
|
13
|
+
return ptr;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
void *nomen_calloc_wrap(unsigned long count, unsigned long size) {
|
|
17
|
+
void *ptr = calloc(count, size);
|
|
18
|
+
__atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
|
|
19
|
+
return ptr;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
void *nomen_realloc_wrap(void *old_ptr, unsigned long size) {
|
|
23
|
+
if (!old_ptr) {
|
|
24
|
+
__atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
|
|
25
|
+
}
|
|
26
|
+
void *ptr = realloc(old_ptr, size);
|
|
27
|
+
return ptr;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
void nomen_free_wrap(void *ptr) {
|
|
31
|
+
if (ptr) {
|
|
32
|
+
free(ptr);
|
|
33
|
+
__atomic_sub_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void *nomen_strdup_wrap(const char *s) {
|
|
38
|
+
unsigned long len = strlen(s) + 1;
|
|
39
|
+
void *ptr = malloc(len);
|
|
40
|
+
if (ptr) memcpy(ptr, s, len);
|
|
41
|
+
__atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
|
|
42
|
+
return ptr;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
void nomen_audit_check(void) {
|
|
46
|
+
long count = __atomic_load_n(&nomen_malloc_count, __ATOMIC_SEQ_CST);
|
|
47
|
+
if (count != 0) {
|
|
48
|
+
printf("LEAK: %ld allocation(s)\n", count);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -241,7 +241,10 @@ pub struct StringBuilder {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
// Finalize: null-terminate, return the byte buffer as a string, and
|
|
244
|
-
// release ownership (sets `data` to 0 so the destructor won't free it
|
|
244
|
+
// release ownership (sets `data` to 0 so the destructor won't free it,
|
|
245
|
+
// and resets `len`/`cap` so the builder is a valid empty builder again —
|
|
246
|
+
// a stale `cap` would let a later append skip the grow and memcpy to a
|
|
247
|
+
// null buffer). Reuse after `to_string` yields a fresh builder.
|
|
245
248
|
// Explicitly `pub`: struct members default to pub, but this is the
|
|
246
249
|
// builder's primary read-back — the contract should not depend on the
|
|
247
250
|
// default.
|
|
@@ -260,6 +263,8 @@ pub struct StringBuilder {
|
|
|
260
263
|
((unsigned char*)(unsigned long long)self->data)[idx] = 0;
|
|
261
264
|
unsigned long long result = self->data;
|
|
262
265
|
self->data = 0;
|
|
266
|
+
self->len = 0;
|
|
267
|
+
self->cap = 0;
|
|
263
268
|
return (nomen_string){ (char*)result, (long)idx };
|
|
264
269
|
```
|
|
265
270
|
```
|
|
@@ -290,6 +295,10 @@ pub struct StringBuilder {
|
|
|
290
295
|
strb wzr, [x0, x1] // data[len] = 0
|
|
291
296
|
// transfer ownership: clear self->data. x0 retains the old ptr.
|
|
292
297
|
str xzr, [x19, #8]
|
|
298
|
+
// reset the stale bookkeeping so reuse yields a valid empty builder
|
|
299
|
+
// (a stale cap would skip the append's grow against the null buffer)
|
|
300
|
+
str xzr, [x19, #16]
|
|
301
|
+
str xzr, [x19, #24]
|
|
293
302
|
// Fat-string ABI: (x0 = data, x1 = len) is already the return pair.
|
|
294
303
|
```
|
|
295
304
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import { execFile, execFileSync, execSync } from "node:child_process";
|
|
3
3
|
import fs, { stat, unwatchFile, watch, watchFile } from "node:fs";
|
|
4
4
|
import * as sp from "node:path";
|
|
5
5
|
import path, { join, resolve, sep } from "node:path";
|
|
6
6
|
import { EventEmitter } from "node:events";
|
|
7
7
|
import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises";
|
|
8
8
|
import { Readable } from "node:stream";
|
|
9
|
-
import { type } from "node:os";
|
|
9
|
+
import os, { type } from "node:os";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { promisify } from "node:util";
|
|
11
12
|
//#region ../node_modules/.pnpm/readdirp@5.1.1/node_modules/readdirp/index.js
|
|
12
13
|
const EntryTypes = {
|
|
13
14
|
FILE_TYPE: "files",
|
|
@@ -15134,6 +15135,33 @@ function emit_destroy_for_scope(status, declarations_before) {
|
|
|
15134
15135
|
emit_asm(status, `ldr x19, [sp], #16\n`);
|
|
15135
15136
|
continue;
|
|
15136
15137
|
}
|
|
15138
|
+
if (status.heap_array_vars?.has(decl.name)) {
|
|
15139
|
+
if (status.heap_owned_string_arrays?.has(decl.name)) {
|
|
15140
|
+
emit_asm(status, `str x19, [sp, #-16]!\n`);
|
|
15141
|
+
emit_asm(status, `str x20, [sp, #-16]!\n`);
|
|
15142
|
+
emit_var_load(status, "x0", decl.name, 8);
|
|
15143
|
+
emit_asm(status, `mov x19, x0\n`);
|
|
15144
|
+
emit_asm(status, `ldr x20, [x19]\n`);
|
|
15145
|
+
emit_asm(status, `add x19, x19, #8\n`);
|
|
15146
|
+
const loop = `.Lhosa_${decl.name}`;
|
|
15147
|
+
emit_asm(status, `${loop}:\n`);
|
|
15148
|
+
emit_asm(status, `cbz x20, .Lhosa_done_${decl.name}\n`);
|
|
15149
|
+
emit_asm(status, `ldr x0, [x19]\n`);
|
|
15150
|
+
emit_free(status);
|
|
15151
|
+
emit_asm(status, `add x19, x19, #16\n`);
|
|
15152
|
+
emit_asm(status, `sub x20, x20, #1\n`);
|
|
15153
|
+
emit_asm(status, `b ${loop}\n`);
|
|
15154
|
+
emit_asm(status, `.Lhosa_done_${decl.name}:\n`);
|
|
15155
|
+
emit_var_load(status, "x0", decl.name, 8);
|
|
15156
|
+
emit_free(status);
|
|
15157
|
+
emit_asm(status, `ldr x20, [sp], #16\n`);
|
|
15158
|
+
emit_asm(status, `ldr x19, [sp], #16\n`);
|
|
15159
|
+
continue;
|
|
15160
|
+
}
|
|
15161
|
+
emit_var_load(status, "x0", decl.name, 8);
|
|
15162
|
+
emit_free(status);
|
|
15163
|
+
continue;
|
|
15164
|
+
}
|
|
15137
15165
|
if (status.heap_strings?.has(decl.name)) {
|
|
15138
15166
|
emit_var_load(status, "x0", decl.name, 8);
|
|
15139
15167
|
emit_free(status);
|
|
@@ -33540,7 +33568,8 @@ function build_assignment_node$1(node, status, nir_rhs, nir_swap) {
|
|
|
33540
33568
|
if (!node.operator && node.left_value.node_type === "access" && node.left_value.access.node_type === "access_field" && node.right_value.node_type === "value" && node.right_value.is_moved) {
|
|
33541
33569
|
const access_lhs = node.left_value;
|
|
33542
33570
|
const field_type = access_lhs.access.type;
|
|
33543
|
-
const
|
|
33571
|
+
const field_resolved = field_type?.name && !field_type.is_view && !field_type.is_array ? resolve_struct_type(field_type, status) : void 0;
|
|
33572
|
+
const field_struct = field_resolved && !field_resolved.is_class ? field_resolved : void 0;
|
|
33544
33573
|
if (field_struct) {
|
|
33545
33574
|
splice_decl_from_c_scopes(status, node.right_value.value);
|
|
33546
33575
|
const saved_len = begin_code_scratch(status);
|
|
@@ -39017,10 +39046,31 @@ function build_access_node$1(node, status) {
|
|
|
39017
39046
|
status.code += `${ret_c} ${ctor_temp_val} = `;
|
|
39018
39047
|
}
|
|
39019
39048
|
}
|
|
39049
|
+
const owned_string_recv = !ctor_temp_receiver && (node.target.node_type === "func_call" || node.target.node_type === "access" && node.target.access.node_type === "access_func") && method_type?.name === "string" && !method_type.is_view && !is_string_borrow(node.target);
|
|
39050
|
+
let owned_recv_temp;
|
|
39051
|
+
let owned_recv_val;
|
|
39052
|
+
if (owned_string_recv) {
|
|
39053
|
+
const id = status.label_counter = (status.label_counter ?? 0) + 1;
|
|
39054
|
+
owned_recv_temp = `_osrecv_${id}`;
|
|
39055
|
+
status.code += `({ nomen_string ${owned_recv_temp} = `;
|
|
39056
|
+
{
|
|
39057
|
+
const saved_suppress = status.suppress_dereference;
|
|
39058
|
+
status.suppress_dereference = true;
|
|
39059
|
+
build_node$1(node.target, status);
|
|
39060
|
+
status.suppress_dereference = saved_suppress;
|
|
39061
|
+
}
|
|
39062
|
+
status.code += `; `;
|
|
39063
|
+
const recv_ret_c = c_return_type((target_method ?? trait_default_func)?.return_type ?? access_func.type, status);
|
|
39064
|
+
if (recv_ret_c !== "void") {
|
|
39065
|
+
owned_recv_val = `_osrecvv_${id}`;
|
|
39066
|
+
status.code += `${recv_ret_c} ${owned_recv_val} = `;
|
|
39067
|
+
}
|
|
39068
|
+
}
|
|
39020
39069
|
if (lambda_ret_tmp) status.code += `${lambda_ret_c} ${lambda_ret_tmp} = `;
|
|
39021
39070
|
status.code += `${label}(`;
|
|
39022
39071
|
if (!access_func.is_static) {
|
|
39023
39072
|
if (ctor_temp_free) status.code += ctor_temp_free;
|
|
39073
|
+
else if (owned_recv_temp) status.code += owned_recv_temp;
|
|
39024
39074
|
else if (array_wrap) status.code += array_wrap.self_expr;
|
|
39025
39075
|
else {
|
|
39026
39076
|
const method_self_is_ref = !!target_method?.params?.some((p) => p.is_self_param && (p.is_ref || p.type?.is_ref));
|
|
@@ -39105,6 +39155,11 @@ function build_access_node$1(node, status) {
|
|
|
39105
39155
|
if (ctor_temp_val) status.code += ` ${ctor_temp_val};`;
|
|
39106
39156
|
status.code += ` })`;
|
|
39107
39157
|
}
|
|
39158
|
+
if (owned_recv_temp) {
|
|
39159
|
+
status.code += `; ${status.audit ? "nomen_free_wrap" : "free"}(${owned_recv_temp}.ptr);`;
|
|
39160
|
+
if (owned_recv_val) status.code += ` ${owned_recv_val};`;
|
|
39161
|
+
status.code += ` })`;
|
|
39162
|
+
}
|
|
39108
39163
|
if (lambda_arg_temps.size > 0) {
|
|
39109
39164
|
for (const tmp of lambda_arg_temps.values()) status.code += `; ${closure_dispose_arm(tmp)}`;
|
|
39110
39165
|
if (lambda_ret_tmp) status.code += `; ${lambda_ret_tmp}`;
|
|
@@ -45635,7 +45690,7 @@ function build_access_method(node, access_func, status) {
|
|
|
45635
45690
|
status.last_result_is_heap = false;
|
|
45636
45691
|
build_node(node.target, status);
|
|
45637
45692
|
ensure_newline(status);
|
|
45638
|
-
frees_string_receiver =
|
|
45693
|
+
frees_string_receiver = status.last_result_is_heap;
|
|
45639
45694
|
status.last_result_is_heap = false;
|
|
45640
45695
|
emit_asm(status, `stp x0, x1, [sp, #-16]!\n`);
|
|
45641
45696
|
}
|
|
@@ -62443,7 +62498,9 @@ const STRING_OPTIONS = /* @__PURE__ */ new Map([
|
|
|
62443
62498
|
["p", "platform"],
|
|
62444
62499
|
["lib", "lib"],
|
|
62445
62500
|
["l", "lib"],
|
|
62446
|
-
["audit-runtime", "audit_runtime"]
|
|
62501
|
+
["audit-runtime", "audit_runtime"],
|
|
62502
|
+
["jobs", "jobs"],
|
|
62503
|
+
["j", "jobs"]
|
|
62447
62504
|
]);
|
|
62448
62505
|
const BOOLEAN_OPTIONS = /* @__PURE__ */ new Map([
|
|
62449
62506
|
["watch", "watch"],
|
|
@@ -62553,8 +62610,9 @@ function print_help() {
|
|
|
62553
62610
|
" --platform, -p <name> Target platform: macos, ios, linux, android, windows, web",
|
|
62554
62611
|
" --lib, -l <path> Path to System library directory (containing package.jsonc)",
|
|
62555
62612
|
" --audit Audit the generated program for memory issues",
|
|
62556
|
-
" --audit-runtime <path> Path to audit_runtime.c
|
|
62557
|
-
" --
|
|
62613
|
+
" --audit-runtime <path> Path to audit_runtime.c (default: auto-discovered — walk-up or the CLI-bundled copy)",
|
|
62614
|
+
" --jobs, -j <n> For `nomen test`: max test files run in parallel (default: CPU count)",
|
|
62615
|
+
" --release, -r Build with optimizations (clang -O2; aarch64 asm cleanup passes)",
|
|
62558
62616
|
" --fast-math Allow FP reassociation (float loop reductions vectorize on aarch64)",
|
|
62559
62617
|
" --check For `nomen format`: report files that would change",
|
|
62560
62618
|
" -h, --help Show this help"
|
|
@@ -62902,6 +62960,9 @@ function find_bundled(filename) {
|
|
|
62902
62960
|
const candidates = !cli_root.includes("node_modules") ? [path.join(repo_root, filename), path.join(cli_root, filename)] : [path.join(cli_root, filename), path.join(repo_root, filename)];
|
|
62903
62961
|
for (const c of candidates) if (fs.existsSync(c)) return path.resolve(c);
|
|
62904
62962
|
}
|
|
62963
|
+
function find_bundled_audit_runtime() {
|
|
62964
|
+
return find_bundled("src/audit_runtime.c") ?? find_bundled("audit_runtime.c");
|
|
62965
|
+
}
|
|
62905
62966
|
/**
|
|
62906
62967
|
* `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
|
|
62907
62968
|
* starting layout (package.jsonc, src/main.nm, a starter test, README and an
|
|
@@ -62980,6 +63041,7 @@ function outfile_for(input_path, is_test, out) {
|
|
|
62980
63041
|
//#endregion
|
|
62981
63042
|
//#region src/test.ts
|
|
62982
63043
|
const RECORD_PREFIX = "\\nomen|";
|
|
63044
|
+
const execFile_async = promisify(execFile);
|
|
62983
63045
|
/** Recursively collect `*.test.nm` files under `root`, skipping build output. */
|
|
62984
63046
|
function collect_test_files(root) {
|
|
62985
63047
|
const out = [];
|
|
@@ -63129,7 +63191,7 @@ function extract_leaks(stdout) {
|
|
|
63129
63191
|
* Compile one `*.test.nm` file (with the generated harness) and run it,
|
|
63130
63192
|
* returning its parsed records and any crash info.
|
|
63131
63193
|
*/
|
|
63132
|
-
function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
|
|
63194
|
+
async function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
|
|
63133
63195
|
const start = performance.now();
|
|
63134
63196
|
const source_text = fs.readFileSync(entry_path, "utf8");
|
|
63135
63197
|
const harness = generate_harness(extract_test_functions(source_text), extract_bench_functions(source_text));
|
|
@@ -63170,7 +63232,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63170
63232
|
if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir, { recursive: true });
|
|
63171
63233
|
let audit_obj;
|
|
63172
63234
|
if (audit) {
|
|
63173
|
-
const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved));
|
|
63235
|
+
const runtime_src = audit_runtime ? path.resolve(audit_runtime) : find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime();
|
|
63174
63236
|
if (!runtime_src || !fs.existsSync(runtime_src)) {
|
|
63175
63237
|
result.ok = false;
|
|
63176
63238
|
result.phase = "setup";
|
|
@@ -63179,7 +63241,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63179
63241
|
return result;
|
|
63180
63242
|
}
|
|
63181
63243
|
audit_obj = path.join(buildDir, "audit_runtime.o");
|
|
63182
|
-
|
|
63244
|
+
await execFile_async("clang", [
|
|
63183
63245
|
"-c",
|
|
63184
63246
|
runtime_src,
|
|
63185
63247
|
"-o",
|
|
@@ -63208,14 +63270,7 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63208
63270
|
if (audit_obj) link_args.push(audit_obj);
|
|
63209
63271
|
if (platform === "macos" || platform === "ios") link_args.push("-framework", "CoreGraphics", "-framework", "Foundation", "-framework", "AppKit", "-lobjc");
|
|
63210
63272
|
try {
|
|
63211
|
-
|
|
63212
|
-
encoding: "utf8",
|
|
63213
|
-
stdio: [
|
|
63214
|
-
"ignore",
|
|
63215
|
-
"pipe",
|
|
63216
|
-
"pipe"
|
|
63217
|
-
]
|
|
63218
|
-
});
|
|
63273
|
+
await execFile_async("clang", link_args, { encoding: "utf8" });
|
|
63219
63274
|
} catch (err) {
|
|
63220
63275
|
const stderr = err.stderr ? err.stderr.toString() : err.message ?? "";
|
|
63221
63276
|
result.ok = false;
|
|
@@ -63227,17 +63282,12 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime,
|
|
|
63227
63282
|
let runStdout = "";
|
|
63228
63283
|
let crashed;
|
|
63229
63284
|
try {
|
|
63230
|
-
runStdout =
|
|
63285
|
+
runStdout = (await execFile_async(outfile, [], {
|
|
63231
63286
|
encoding: "utf8",
|
|
63232
|
-
stdio: [
|
|
63233
|
-
"ignore",
|
|
63234
|
-
"pipe",
|
|
63235
|
-
"pipe"
|
|
63236
|
-
],
|
|
63237
63287
|
timeout: 3e4,
|
|
63238
63288
|
maxBuffer: 16777216,
|
|
63239
63289
|
cwd: cwd ?? process.cwd()
|
|
63240
|
-
});
|
|
63290
|
+
})).stdout;
|
|
63241
63291
|
} catch (err) {
|
|
63242
63292
|
runStdout = err.stdout ? err.stdout.toString() : "";
|
|
63243
63293
|
if (err.signal === "SIGTERM") crashed = "test binary timed out after 30s";
|
|
@@ -63296,10 +63346,16 @@ function report_file(result) {
|
|
|
63296
63346
|
for (const line of result.other) console.log(C.dim(` ${line}`));
|
|
63297
63347
|
}
|
|
63298
63348
|
}
|
|
63349
|
+
/** The default concurrency for `runTests` when no `jobs` is given. */
|
|
63350
|
+
function default_jobs() {
|
|
63351
|
+
const n = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
|
|
63352
|
+
return Math.max(1, n);
|
|
63353
|
+
}
|
|
63299
63354
|
/**
|
|
63300
63355
|
* Locate audit_runtime.c for an audited test build: an explicit path wins,
|
|
63301
63356
|
* otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
|
|
63302
|
-
* convention the run/build commands use).
|
|
63357
|
+
* convention the run/build commands use). When both miss, the caller falls
|
|
63358
|
+
* back to the CLI-bundled runtime (`find_bundled_audit_runtime`).
|
|
63303
63359
|
*/
|
|
63304
63360
|
function find_audit_runtime(dir) {
|
|
63305
63361
|
let cur = dir;
|
|
@@ -63312,19 +63368,25 @@ function find_audit_runtime(dir) {
|
|
|
63312
63368
|
}
|
|
63313
63369
|
}
|
|
63314
63370
|
/** Discover, run, and report every `*.test.nm` under `root`. */
|
|
63315
|
-
function runTests(root, options = {}) {
|
|
63371
|
+
async function runTests(root, options = {}) {
|
|
63316
63372
|
const arch = options.arch ?? "aarch64";
|
|
63317
63373
|
const lib = resolve_lib_for(root);
|
|
63318
63374
|
const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
|
|
63319
63375
|
console.log(`Found ${files.length} test file(s)\n`);
|
|
63320
63376
|
const startTime = performance.now();
|
|
63321
63377
|
const binary_cwd = path.resolve(root);
|
|
63322
|
-
const
|
|
63323
|
-
|
|
63324
|
-
|
|
63325
|
-
|
|
63326
|
-
|
|
63327
|
-
|
|
63378
|
+
const jobs = Math.max(1, Math.min(options.jobs ?? default_jobs(), files.length));
|
|
63379
|
+
const results = new Array(files.length);
|
|
63380
|
+
let next = 0;
|
|
63381
|
+
const worker = async () => {
|
|
63382
|
+
for (;;) {
|
|
63383
|
+
const i = next++;
|
|
63384
|
+
if (i >= files.length) return;
|
|
63385
|
+
results[i] = await run_test_file(files[i], lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
|
|
63386
|
+
}
|
|
63387
|
+
};
|
|
63388
|
+
await Promise.all(Array.from({ length: jobs }, worker));
|
|
63389
|
+
for (const result of results) report_file(result);
|
|
63328
63390
|
const elapsed = performance.now() - startTime;
|
|
63329
63391
|
const totalFiles = results.length;
|
|
63330
63392
|
const totalFilesFailed = results.reduce((a, r) => a + (r.ok ? 0 : 1), 0);
|
|
@@ -63425,12 +63487,13 @@ try {
|
|
|
63425
63487
|
if (command === "test") {
|
|
63426
63488
|
const root = args.in ?? process.cwd();
|
|
63427
63489
|
const filter = args.filter ? new RegExp(args.filter) : void 0;
|
|
63428
|
-
const ok = runTests(root, {
|
|
63490
|
+
const ok = await runTests(root, {
|
|
63429
63491
|
arch: args.arch ?? "aarch64",
|
|
63430
63492
|
filter,
|
|
63431
63493
|
audit: args.audit,
|
|
63432
63494
|
audit_runtime: args.audit_runtime,
|
|
63433
|
-
release: args.release
|
|
63495
|
+
release: args.release,
|
|
63496
|
+
jobs: args.jobs !== void 0 ? parseInt(args.jobs, 10) : void 0
|
|
63434
63497
|
});
|
|
63435
63498
|
process.exit(ok ? 0 : 1);
|
|
63436
63499
|
}
|
|
@@ -63541,6 +63604,7 @@ function resolve_audit_runtime(config, input_path) {
|
|
|
63541
63604
|
if (parent === dir) break;
|
|
63542
63605
|
dir = parent;
|
|
63543
63606
|
}
|
|
63607
|
+
return find_bundled_audit_runtime();
|
|
63544
63608
|
}
|
|
63545
63609
|
function compile_audit_runtime(config, input_path, buildDir) {
|
|
63546
63610
|
const runtime_src = resolve_audit_runtime(config, input_path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nomen-lang",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"description": "The CLI for the Nomen programming language.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "ISC",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"src",
|
|
19
19
|
"core",
|
|
20
|
-
"NOMEN_AGENTS.md"
|
|
20
|
+
"NOMEN_AGENTS.md",
|
|
21
|
+
"audit_runtime.c"
|
|
21
22
|
],
|
|
22
23
|
"type": "module",
|
|
23
24
|
"devDependencies": {
|
package/src/args.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface Args {
|
|
|
13
13
|
audit_runtime?: string;
|
|
14
14
|
check: boolean;
|
|
15
15
|
release: boolean;
|
|
16
|
+
/** `nomen test` concurrency (max test files in flight); default = CPU count. */
|
|
17
|
+
jobs?: string;
|
|
16
18
|
/** Opt in to FP reassociation: float reductions vectorize under NEON
|
|
17
19
|
* (results may differ in the last ulp vs the scalar loop). */
|
|
18
20
|
fast_math: boolean;
|
|
@@ -39,6 +41,8 @@ const STRING_OPTIONS: Map<string, string> = new Map([
|
|
|
39
41
|
["lib", "lib"],
|
|
40
42
|
["l", "lib"],
|
|
41
43
|
["audit-runtime", "audit_runtime"],
|
|
44
|
+
["jobs", "jobs"],
|
|
45
|
+
["j", "jobs"],
|
|
42
46
|
]);
|
|
43
47
|
|
|
44
48
|
const BOOLEAN_OPTIONS: Map<string, string> = new Map([
|
|
@@ -174,8 +178,9 @@ export function print_help(): void {
|
|
|
174
178
|
" --platform, -p <name> Target platform: macos, ios, linux, android, windows, web",
|
|
175
179
|
" --lib, -l <path> Path to System library directory (containing package.jsonc)",
|
|
176
180
|
" --audit Audit the generated program for memory issues",
|
|
177
|
-
" --audit-runtime <path> Path to audit_runtime.c
|
|
178
|
-
" --
|
|
181
|
+
" --audit-runtime <path> Path to audit_runtime.c (default: auto-discovered — walk-up or the CLI-bundled copy)",
|
|
182
|
+
" --jobs, -j <n> For `nomen test`: max test files run in parallel (default: CPU count)",
|
|
183
|
+
" --release, -r Build with optimizations (clang -O2; aarch64 asm cleanup passes)",
|
|
179
184
|
" --fast-math Allow FP reassociation (float loop reductions vectorize on aarch64)",
|
|
180
185
|
" --check For `nomen format`: report files that would change",
|
|
181
186
|
" -h, --help Show this help",
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import parse from "../../src/parse.ts";
|
|
|
13
13
|
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
|
-
import { find_bundled, run_init } from "./init.ts";
|
|
16
|
+
import { find_bundled, find_bundled_audit_runtime, run_init } from "./init.ts";
|
|
17
17
|
import { build_dir_for, outfile_for } from "./paths.ts";
|
|
18
18
|
import { runTests } from "./test.ts";
|
|
19
19
|
import type Config from "./types/Config.ts";
|
|
@@ -104,12 +104,13 @@ try {
|
|
|
104
104
|
const root = args.in ?? process.cwd();
|
|
105
105
|
const filter = args.filter ? new RegExp(args.filter) : undefined;
|
|
106
106
|
const arch = args.arch ?? "aarch64";
|
|
107
|
-
const ok = runTests(root, {
|
|
107
|
+
const ok = await runTests(root, {
|
|
108
108
|
arch,
|
|
109
109
|
filter,
|
|
110
110
|
audit: args.audit,
|
|
111
111
|
audit_runtime: args.audit_runtime,
|
|
112
112
|
release: args.release,
|
|
113
|
+
jobs: args.jobs !== undefined ? parseInt(args.jobs, 10) : undefined,
|
|
113
114
|
});
|
|
114
115
|
process.exit(ok ? 0 : 1);
|
|
115
116
|
}
|
|
@@ -283,7 +284,10 @@ function resolve_audit_runtime(config: Config, input_path: string): string | und
|
|
|
283
284
|
if (parent === dir) break;
|
|
284
285
|
dir = parent;
|
|
285
286
|
}
|
|
286
|
-
|
|
287
|
+
// Neither the config nor the walk-up found it: fall back to the runtime
|
|
288
|
+
// bundled with the CLI (live repo copy in the dev tree, shipped copy when
|
|
289
|
+
// published) so `--audit` works outside the nomen checkout with no flag.
|
|
290
|
+
return find_bundled_audit_runtime();
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
function compile_audit_runtime(config: Config, input_path: string, buildDir: string): string {
|
package/src/init.ts
CHANGED
|
@@ -85,6 +85,16 @@ export function find_bundled(filename: string): string | undefined {
|
|
|
85
85
|
return undefined;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// The audit runtime for `--audit` builds: the dev tree's LIVE copy
|
|
89
|
+
// (repo_root/src/audit_runtime.c — a stale bundled snapshot must not shadow
|
|
90
|
+
// it, same posture as the cli/core rule above), else the copy shipped at the
|
|
91
|
+
// package root by `scripts/bundle-assets.mjs` in a published install. Lets
|
|
92
|
+
// `nomen test/run --audit` work outside the nomen checkout with no
|
|
93
|
+
// --audit-runtime flag.
|
|
94
|
+
export function find_bundled_audit_runtime(): string | undefined {
|
|
95
|
+
return find_bundled("src/audit_runtime.c") ?? find_bundled("audit_runtime.c");
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
/**
|
|
89
99
|
* `nomen init <name>`: scaffold a new project under `./<name>` with a sensible
|
|
90
100
|
* starting layout (package.jsonc, src/main.nm, a starter test, README and an
|
package/src/test.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
5
7
|
|
|
6
8
|
import build, { default_platform } from "../../src/build.ts";
|
|
7
9
|
import join from "../../src/join.ts";
|
|
8
10
|
import { get_library } from "../../src/lib.ts";
|
|
9
11
|
import parse from "../../src/parse.ts";
|
|
10
|
-
import { find_bundled } from "./init.ts";
|
|
12
|
+
import { find_bundled, find_bundled_audit_runtime } from "./init.ts";
|
|
11
13
|
import { build_dir_for } from "./paths.ts";
|
|
12
14
|
|
|
13
15
|
const RECORD_PREFIX = "\\nomen|";
|
|
14
16
|
|
|
17
|
+
// Promise-returning `execFile`: `nomen test` builds and runs each `*.test.nm`
|
|
18
|
+
// file in its own build folder and its own process, so the (latency-bound)
|
|
19
|
+
// clang/link/run subprocesses overlap across files instead of serializing.
|
|
20
|
+
// A rejected error carries the same `.stdout` / `.stderr` / `.signal` /
|
|
21
|
+
// `.code` fields the previous synchronous `execFileSync` catch sites read.
|
|
22
|
+
const execFile_async = promisify(execFile);
|
|
23
|
+
|
|
15
24
|
// A discovered test function: `pub func <name> = (ref Tester t)`.
|
|
16
25
|
export interface TestFunction {
|
|
17
26
|
name: string;
|
|
@@ -272,7 +281,7 @@ export function extract_leaks(stdout: string): string[] {
|
|
|
272
281
|
* Compile one `*.test.nm` file (with the generated harness) and run it,
|
|
273
282
|
* returning its parsed records and any crash info.
|
|
274
283
|
*/
|
|
275
|
-
export function run_test_file(
|
|
284
|
+
export async function run_test_file(
|
|
276
285
|
entry_path: string,
|
|
277
286
|
lib_path: string | undefined,
|
|
278
287
|
arch: string,
|
|
@@ -280,7 +289,7 @@ export function run_test_file(
|
|
|
280
289
|
audit_runtime?: string,
|
|
281
290
|
release = false,
|
|
282
291
|
cwd?: string,
|
|
283
|
-
): TestFileResult {
|
|
292
|
+
): Promise<TestFileResult> {
|
|
284
293
|
const start = performance.now();
|
|
285
294
|
const source_text = fs.readFileSync(entry_path, "utf8");
|
|
286
295
|
const tests = extract_test_functions(source_text);
|
|
@@ -339,9 +348,13 @@ export function run_test_file(
|
|
|
339
348
|
// "LEAK: N allocation(s)" when the balance is nonzero.
|
|
340
349
|
let audit_obj: string | undefined;
|
|
341
350
|
if (audit) {
|
|
351
|
+
// Explicit flag wins; else walk up from the test file (a nomen
|
|
352
|
+
// checkout's src/audit_runtime.c); else the runtime bundled with the
|
|
353
|
+
// CLI (live repo copy in the dev tree, shipped copy when published)
|
|
354
|
+
// so `--audit` works outside the nomen checkout with no flag.
|
|
342
355
|
const runtime_src = audit_runtime
|
|
343
356
|
? path.resolve(audit_runtime)
|
|
344
|
-
: find_audit_runtime(path.dirname(resolved));
|
|
357
|
+
: (find_audit_runtime(path.dirname(resolved)) ?? find_bundled_audit_runtime());
|
|
345
358
|
if (!runtime_src || !fs.existsSync(runtime_src)) {
|
|
346
359
|
result.ok = false;
|
|
347
360
|
result.phase = "setup";
|
|
@@ -351,7 +364,7 @@ export function run_test_file(
|
|
|
351
364
|
return result;
|
|
352
365
|
}
|
|
353
366
|
audit_obj = path.join(buildDir, "audit_runtime.o");
|
|
354
|
-
|
|
367
|
+
await execFile_async("clang", ["-c", runtime_src, "-o", audit_obj]);
|
|
355
368
|
}
|
|
356
369
|
const platform = default_platform();
|
|
357
370
|
// Match the run/build command's emission: ObjC-bearing sources (GUI code
|
|
@@ -401,10 +414,7 @@ export function run_test_file(
|
|
|
401
414
|
);
|
|
402
415
|
}
|
|
403
416
|
try {
|
|
404
|
-
|
|
405
|
-
encoding: "utf8",
|
|
406
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
407
|
-
});
|
|
417
|
+
await execFile_async("clang", link_args, { encoding: "utf8" });
|
|
408
418
|
} catch (err: any) {
|
|
409
419
|
const stderr = err.stderr ? err.stderr.toString() : (err.message ?? "");
|
|
410
420
|
result.ok = false;
|
|
@@ -424,13 +434,13 @@ export function run_test_file(
|
|
|
424
434
|
let runStdout = "";
|
|
425
435
|
let crashed: string | undefined;
|
|
426
436
|
try {
|
|
427
|
-
|
|
437
|
+
const run = await execFile_async(outfile, [], {
|
|
428
438
|
encoding: "utf8",
|
|
429
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
430
439
|
timeout: 30_000,
|
|
431
440
|
maxBuffer: 16 * 1024 * 1024,
|
|
432
441
|
cwd: cwd ?? process.cwd(),
|
|
433
442
|
});
|
|
443
|
+
runStdout = run.stdout;
|
|
434
444
|
} catch (err: any) {
|
|
435
445
|
runStdout = err.stdout ? err.stdout.toString() : "";
|
|
436
446
|
if (err.signal === "SIGTERM") {
|
|
@@ -545,12 +555,27 @@ export interface RunTestsOptions {
|
|
|
545
555
|
audit?: boolean;
|
|
546
556
|
audit_runtime?: string;
|
|
547
557
|
release?: boolean;
|
|
558
|
+
/**
|
|
559
|
+
* Maximum test files built and run concurrently. Each file compiles into
|
|
560
|
+
* its own build folder and runs in its own process, so the latency-bound
|
|
561
|
+
* clang/link/run steps overlap across files. Defaults to the machine's
|
|
562
|
+
* available parallelism; 1 forces the historical sequential order.
|
|
563
|
+
*/
|
|
564
|
+
jobs?: number;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** The default concurrency for `runTests` when no `jobs` is given. */
|
|
568
|
+
export function default_jobs(): number {
|
|
569
|
+
const n =
|
|
570
|
+
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
|
|
571
|
+
return Math.max(1, n);
|
|
548
572
|
}
|
|
549
573
|
|
|
550
574
|
/**
|
|
551
575
|
* Locate audit_runtime.c for an audited test build: an explicit path wins,
|
|
552
576
|
* otherwise walk up from `dir` looking for a `src/audit_runtime.c` (the same
|
|
553
|
-
* convention the run/build commands use).
|
|
577
|
+
* convention the run/build commands use). When both miss, the caller falls
|
|
578
|
+
* back to the CLI-bundled runtime (`find_bundled_audit_runtime`).
|
|
554
579
|
*/
|
|
555
580
|
function find_audit_runtime(dir: string): string | undefined {
|
|
556
581
|
let cur = dir;
|
|
@@ -565,7 +590,7 @@ function find_audit_runtime(dir: string): string | undefined {
|
|
|
565
590
|
}
|
|
566
591
|
|
|
567
592
|
/** Discover, run, and report every `*.test.nm` under `root`. */
|
|
568
|
-
export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
593
|
+
export async function runTests(root: string, options: RunTestsOptions = {}): Promise<boolean> {
|
|
569
594
|
const arch = options.arch ?? "aarch64";
|
|
570
595
|
const lib = resolve_lib_for(root);
|
|
571
596
|
const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
|
|
@@ -577,20 +602,33 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
|
577
602
|
// fixture paths resolve there no matter where the CLI was invoked from.
|
|
578
603
|
const binary_cwd = path.resolve(root);
|
|
579
604
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
605
|
+
// Bounded-concurrency pool: at most `jobs` files are in flight. The
|
|
606
|
+
// parse/build halves are synchronous (no internal awaits), so they run
|
|
607
|
+
// atomically on the main thread between the subprocess awaits — the
|
|
608
|
+
// compiler's in-process state never interleaves. Results are collected by
|
|
609
|
+
// file INDEX and reported after the pool drains, so the report stays in
|
|
610
|
+
// discovery order regardless of completion order (and stdout isn't
|
|
611
|
+
// interleaved).
|
|
612
|
+
const jobs = Math.max(1, Math.min(options.jobs ?? default_jobs(), files.length));
|
|
613
|
+
const results: TestFileResult[] = new Array(files.length);
|
|
614
|
+
let next = 0;
|
|
615
|
+
const worker = async (): Promise<void> => {
|
|
616
|
+
for (;;) {
|
|
617
|
+
const i = next++;
|
|
618
|
+
if (i >= files.length) return;
|
|
619
|
+
results[i] = await run_test_file(
|
|
620
|
+
files[i],
|
|
621
|
+
lib,
|
|
622
|
+
arch,
|
|
623
|
+
options.audit,
|
|
624
|
+
options.audit_runtime,
|
|
625
|
+
options.release,
|
|
626
|
+
binary_cwd,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
await Promise.all(Array.from({ length: jobs }, worker));
|
|
631
|
+
for (const result of results) report_file(result);
|
|
594
632
|
|
|
595
633
|
const elapsed = performance.now() - startTime;
|
|
596
634
|
const totalFiles = results.length;
|