nomen-lang 0.6.7 → 0.6.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 +12 -3
- package/core/System/Buffer.nm +38 -0
- package/dist/index.mjs +424 -65
- package/package.json +1 -1
- package/src/args.ts +1 -1
- package/src/index.ts +1 -0
- package/src/test.ts +40 -3
package/NOMEN_AGENTS.md
CHANGED
|
@@ -219,9 +219,9 @@ Special function names: `#init` (constructor), `#destroy` (auto-raii destructor)
|
|
|
219
219
|
|
|
220
220
|
```nomen
|
|
221
221
|
if cond { ... }
|
|
222
|
-
switch
|
|
223
|
-
0 { ... }
|
|
224
|
-
1 { ... }
|
|
222
|
+
switch {
|
|
223
|
+
case x == 0 { ... }
|
|
224
|
+
case x == 1 { ... }
|
|
225
225
|
else { ... }
|
|
226
226
|
}
|
|
227
227
|
for i of 0 .. 10 { ... } // exclusive range
|
|
@@ -280,6 +280,15 @@ const typed = Array<int>(1, 2, 3)
|
|
|
280
280
|
for n of nums { Console.write("\{n}\n") }
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
+
### Compile-time embedding
|
|
284
|
+
|
|
285
|
+
```nomen
|
|
286
|
+
const corpus = #embed_file("bench/full-markdown.md") // string literal of the
|
|
287
|
+
// file's contents; path
|
|
288
|
+
// resolves from the
|
|
289
|
+
// package root
|
|
290
|
+
```
|
|
291
|
+
|
|
283
292
|
## Reference
|
|
284
293
|
|
|
285
294
|
- Full language spec: [SPEC.md](https://github.com/andrewjk/nomen/blob/main/SPEC.md)
|
package/core/System/Buffer.nm
CHANGED
|
@@ -510,6 +510,17 @@ pub struct Buffer<T> {
|
|
|
510
510
|
```
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
+
// Move-taking write, mirroring `List.set`: the caller's value is
|
|
514
|
+
// consumed and an independent copy is installed in the slot (replace
|
|
515
|
+
// deep-copies owning elements). The owning entry point a caller uses
|
|
516
|
+
// when it wants to hand over a value — `replace`'s by-value parameter
|
|
517
|
+
// cannot accept the `move` keyword, so a borrow being transferred
|
|
518
|
+
// (e.g. a loaded element) must go through here rather than be bound to
|
|
519
|
+
// a freed local.
|
|
520
|
+
pub func set = (ref self, int i: i >= 0 && i < self.cap, move T value) {
|
|
521
|
+
self.replace(i, value)
|
|
522
|
+
}
|
|
523
|
+
|
|
513
524
|
// Move slot src into slot dst: overwrite dst with src's bytes and zero
|
|
514
525
|
// src, so exactly ONE slot owns the value afterwards. The
|
|
515
526
|
// ownership-transferring primitive behind backward-shift deletion
|
|
@@ -620,6 +631,33 @@ pub struct Buffer<T> {
|
|
|
620
631
|
memset(self.data, 0, len * T_SIZE)
|
|
621
632
|
}
|
|
622
633
|
|
|
634
|
+
// An independent copy: fresh slab, per-element deep copies. Each slot
|
|
635
|
+
// round-trips load→replace through a local, so owning element types get
|
|
636
|
+
// the same treatment a List<T>.copy slot does (the load materializes an
|
|
637
|
+
// owned copy, replace gives the destination its own). Class elements
|
|
638
|
+
// never reach this body (a `Buffer<T>` with T a class monomorphizes to
|
|
639
|
+
// `ClassBuffer<T>`, which has no copy).
|
|
640
|
+
pub func copy = (self, out Buffer<T>: out.cap == self.cap) {
|
|
641
|
+
var dst = Buffer<T>()
|
|
642
|
+
if self.cap > 0 {
|
|
643
|
+
dst.alloc(self.cap)
|
|
644
|
+
var i = 0
|
|
645
|
+
while i < self.cap {
|
|
646
|
+
// Bind the loaded element and hand it to `set` (move): for a
|
|
647
|
+
// string element load returns a BORROW of the source slot, and
|
|
648
|
+
// the move transfers it into the write without a scope-exit
|
|
649
|
+
// reclaim (which would free the source's buffer); for an owning
|
|
650
|
+
// struct element load returns an owned copy that transfers the
|
|
651
|
+
// same way. Mirrors List.copy's `var T v = self.at(i)` +
|
|
652
|
+
// `dst.push(v)`.
|
|
653
|
+
var T v = self.load(i)
|
|
654
|
+
dst.set(i, v)
|
|
655
|
+
i += 1
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return dst
|
|
659
|
+
}
|
|
660
|
+
|
|
623
661
|
internal inline func store_or_int = (ref self, int i: i >= 0 && i < self.cap, int val) {
|
|
624
662
|
```
|
|
625
663
|
#arch: c
|
package/dist/index.mjs
CHANGED
|
@@ -6811,6 +6811,27 @@ function find_enum(name, status) {
|
|
|
6811
6811
|
}
|
|
6812
6812
|
//#endregion
|
|
6813
6813
|
//#region ../src/build_common/mono_name.ts
|
|
6814
|
+
/**
|
|
6815
|
+
* Whether `name` is a monomorphized owning-container struct whose
|
|
6816
|
+
* `<Mono>_copy` is safe to call from the owning-`Buffer.load_T`
|
|
6817
|
+
* specialization — `List_<T>` / `Buffer_<T>` (and their bare generics).
|
|
6818
|
+
* These own a heap slab their `#destroy` frees unconditionally, and `copy`
|
|
6819
|
+
* yields an independent slab, so a returned element's field must be
|
|
6820
|
+
* deep-copied or the caller's unconditional container-field teardown
|
|
6821
|
+
* double-frees the slot's slab.
|
|
6822
|
+
*
|
|
6823
|
+
* `Map`/`Set` are deliberately EXCLUDED from the automatic deep copy: their
|
|
6824
|
+
* monomorphized `copy` (which composes several `Buffer.copy` calls) is
|
|
6825
|
+
* verified standalone on both backends, but invoking it from the load
|
|
6826
|
+
* specialization corrupts the returned struct on aarch64 (a codegen
|
|
6827
|
+
* interaction still open — see FOLLOWUP.md). A `Map`/`Set` FIELD reached
|
|
6828
|
+
* through a container element's `load` therefore stays shallow (the
|
|
6829
|
+
* pre-existing bounded gap); an EXPLICIT `.copy()` on the owning struct or
|
|
6830
|
+
* the Map/Set itself works on both backends.
|
|
6831
|
+
*/
|
|
6832
|
+
function is_load_deep_copy_container(name) {
|
|
6833
|
+
return name === "List" || name === "Buffer" || name.startsWith("List_") || name.startsWith("Buffer_");
|
|
6834
|
+
}
|
|
6814
6835
|
function mono_type_name(type_or_name, type_args) {
|
|
6815
6836
|
const name = typeof type_or_name === "string" ? type_or_name : type_or_name.name;
|
|
6816
6837
|
const args = typeof type_or_name === "string" ? type_args : type_or_name.type_args;
|
|
@@ -7676,10 +7697,15 @@ function direct_string_fields(node) {
|
|
|
7676
7697
|
* callee's own records are scope-local and die at return, so without the
|
|
7677
7698
|
* transfer the stored copy leaks.
|
|
7678
7699
|
*
|
|
7679
|
-
* Only DIRECT statements qualify: a store nested in a
|
|
7680
|
-
* execute, and a record over the pre-call value would
|
|
7681
|
-
* scope exit (possibly rodata — an invalid free).
|
|
7682
|
-
*
|
|
7700
|
+
* Only DIRECT statements qualify for the plain transfer: a store nested in a
|
|
7701
|
+
* branch/loop may not execute, and a record over the pre-call value would
|
|
7702
|
+
* free it at the caller's scope exit (possibly rodata — an invalid free).
|
|
7703
|
+
* Those CONDITIONAL stores are reported separately
|
|
7704
|
+
* (scan_ref_param_string_field_may_writes): the callee's function builder
|
|
7705
|
+
* emits an ENTRY DUP for each of them (`field = strdup(field)` at function
|
|
7706
|
+
* entry, recorded), which makes the field heap-owned on EVERY path and the
|
|
7707
|
+
* may-store record sound to transfer. Memoized on the function node;
|
|
7708
|
+
* forwarding cycles are cut by a visited set (the cycle member's own
|
|
7683
7709
|
* contribution is skipped, bounded-leak).
|
|
7684
7710
|
*/
|
|
7685
7711
|
function scan_ref_param_string_field_writes(func, structs) {
|
|
@@ -7687,6 +7713,51 @@ function scan_ref_param_string_field_writes(func, structs) {
|
|
|
7687
7713
|
if (stamp._ref_param_string_field_writes) return stamp._ref_param_string_field_writes;
|
|
7688
7714
|
const result = /* @__PURE__ */ new Map();
|
|
7689
7715
|
stamp._ref_param_string_field_writes = result;
|
|
7716
|
+
const tracked = tracked_ref_params(func, structs);
|
|
7717
|
+
if (tracked.size) scan_top_level(func.statements ?? [], tracked, structs, result);
|
|
7718
|
+
return result;
|
|
7719
|
+
}
|
|
7720
|
+
/**
|
|
7721
|
+
* The CONDITIONAL (`may-executed`) ref-param field stores: direct stores
|
|
7722
|
+
* nested in if/else branches, switch cases, and loop bodies. Forwarded
|
|
7723
|
+
* calls from a nested position are deliberately EXCLUDED: a helper's store
|
|
7724
|
+
* builds with its own (fresh) record set, so it cannot reclaim the
|
|
7725
|
+
* displaced entry-dup — dup'ing for a forwarded store would trade the
|
|
7726
|
+
* old bounded leak for a new one. Those chains keep the documented
|
|
7727
|
+
* bounded-leak posture (the stored copy leaks, never an invalid free).
|
|
7728
|
+
* The function builder entry-dups these fields (see
|
|
7729
|
+
* ref_param_entry_dup_fields), making the may-store record transferable.
|
|
7730
|
+
*/
|
|
7731
|
+
function scan_ref_param_string_field_may_writes(func, structs) {
|
|
7732
|
+
const stamp = func;
|
|
7733
|
+
if (stamp._ref_param_string_field_may_writes) return stamp._ref_param_string_field_may_writes;
|
|
7734
|
+
const result = /* @__PURE__ */ new Map();
|
|
7735
|
+
stamp._ref_param_string_field_may_writes = result;
|
|
7736
|
+
const tracked = tracked_ref_params(func, structs);
|
|
7737
|
+
if (tracked.size) scan_nested(func.statements ?? [], tracked, result);
|
|
7738
|
+
return result;
|
|
7739
|
+
}
|
|
7740
|
+
/**
|
|
7741
|
+
* The fields a function's builder must ENTRY DUP: the conditional
|
|
7742
|
+
* (may-executed) ref-param string-field stores that are NOT also
|
|
7743
|
+
* definitely-executed (a must-store already leaves the field heap-owned on
|
|
7744
|
+
* every path — dup'ing it too would just add a pointless strdup per call).
|
|
7745
|
+
* The dup runs at callee entry on both backends and records the field, so a
|
|
7746
|
+
* call site may transfer these as heap records soundly.
|
|
7747
|
+
*/
|
|
7748
|
+
function ref_param_entry_dup_fields(func, structs) {
|
|
7749
|
+
const must = scan_ref_param_string_field_writes(func, structs);
|
|
7750
|
+
const may = scan_ref_param_string_field_may_writes(func, structs);
|
|
7751
|
+
const result = /* @__PURE__ */ new Map();
|
|
7752
|
+
for (const [param, fields] of may) {
|
|
7753
|
+
const dup = /* @__PURE__ */ new Set();
|
|
7754
|
+
for (const field of fields) if (!must.get(param)?.has(field)) dup.add(field);
|
|
7755
|
+
if (dup.size) result.set(param, dup);
|
|
7756
|
+
}
|
|
7757
|
+
return result;
|
|
7758
|
+
}
|
|
7759
|
+
/** The tracked ref params of `func`: name -> writable plain string fields. */
|
|
7760
|
+
function tracked_ref_params(func, structs) {
|
|
7690
7761
|
const tracked = /* @__PURE__ */ new Map();
|
|
7691
7762
|
for (const param of func.params ?? []) {
|
|
7692
7763
|
if (param.is_self_param) continue;
|
|
@@ -7695,8 +7766,7 @@ function scan_ref_param_string_field_writes(func, structs) {
|
|
|
7695
7766
|
const fields = ref_param_writable_fields(param.type.name, structs);
|
|
7696
7767
|
if (fields.size) tracked.set(param.name, fields);
|
|
7697
7768
|
}
|
|
7698
|
-
|
|
7699
|
-
return result;
|
|
7769
|
+
return tracked;
|
|
7700
7770
|
}
|
|
7701
7771
|
/** The writable plain string fields of the value struct `type_name` resolves
|
|
7702
7772
|
* to, or the empty set (non-struct, class, or no string fields). */
|
|
@@ -7706,46 +7776,91 @@ function ref_param_writable_fields(type_name, structs) {
|
|
|
7706
7776
|
if (!struct) return /* @__PURE__ */ new Set();
|
|
7707
7777
|
return new Set(direct_string_fields(struct).map((f) => f.name));
|
|
7708
7778
|
}
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7779
|
+
/**
|
|
7780
|
+
* The top-level (definitely-executed) walk: direct `param.field = …`
|
|
7781
|
+
* statements and top-level forwarded calls only — nothing nested.
|
|
7782
|
+
*/
|
|
7783
|
+
function scan_top_level(statements, tracked, structs, result) {
|
|
7784
|
+
for (const stmt of statements) {
|
|
7712
7785
|
if (stmt.node_type === "assign") {
|
|
7713
|
-
|
|
7714
|
-
if (lhs?.node_type === "access") {
|
|
7715
|
-
const access = lhs;
|
|
7716
|
-
if (access.access.node_type === "access_field" && access.target.node_type === "value") {
|
|
7717
|
-
const name = access.target.value ?? "";
|
|
7718
|
-
const fields = tracked.get(name);
|
|
7719
|
-
const field = access.access.name ?? "";
|
|
7720
|
-
if (fields?.has(field)) {
|
|
7721
|
-
if (!result.has(name)) result.set(name, /* @__PURE__ */ new Set());
|
|
7722
|
-
result.get(name).add(field);
|
|
7723
|
-
}
|
|
7724
|
-
}
|
|
7725
|
-
}
|
|
7786
|
+
record_direct_store(stmt, tracked, result);
|
|
7726
7787
|
continue;
|
|
7727
7788
|
}
|
|
7728
7789
|
const call = ref_forward_call(stmt);
|
|
7729
|
-
if (
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7790
|
+
if (call) merge_forwarded(call, tracked, structs, result);
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7793
|
+
/**
|
|
7794
|
+
* The nested walk: direct stores at ANY depth below the top level are
|
|
7795
|
+
* may-stores. Forwarded calls are skipped everywhere (their contributions
|
|
7796
|
+
* would need cross-function displaced-value knowledge the callee's fresh
|
|
7797
|
+
* record set cannot carry — see the may-scan doc).
|
|
7798
|
+
*/
|
|
7799
|
+
function scan_nested(statements, tracked, result) {
|
|
7800
|
+
for (const stmt of statements) {
|
|
7801
|
+
if (stmt.node_type === "assign") {
|
|
7802
|
+
record_direct_store(stmt, tracked, result);
|
|
7803
|
+
continue;
|
|
7804
|
+
}
|
|
7805
|
+
for (const nested of nested_statement_lists(stmt)) scan_nested(nested, tracked, result);
|
|
7806
|
+
}
|
|
7807
|
+
}
|
|
7808
|
+
/** Record a direct `param.<field> = …` store into `result`. */
|
|
7809
|
+
function record_direct_store(stmt, tracked, result) {
|
|
7810
|
+
const lhs = stmt.left_value;
|
|
7811
|
+
if (lhs?.node_type !== "access") return;
|
|
7812
|
+
const access = lhs;
|
|
7813
|
+
if (access.access.node_type !== "access_field" || access.target.node_type !== "value") return;
|
|
7814
|
+
const name = access.target.value ?? "";
|
|
7815
|
+
const fields = tracked.get(name);
|
|
7816
|
+
const field = access.access.name ?? "";
|
|
7817
|
+
if (!fields?.has(field)) return;
|
|
7818
|
+
if (!result.has(name)) result.set(name, /* @__PURE__ */ new Set());
|
|
7819
|
+
result.get(name).add(field);
|
|
7820
|
+
}
|
|
7821
|
+
/** Merge a forwarded call's callee stores for the forwarded param: the
|
|
7822
|
+
* callee's MUST stores at a top-level position. */
|
|
7823
|
+
function merge_forwarded(call, tracked, structs, result) {
|
|
7824
|
+
const [callee, args] = call;
|
|
7825
|
+
const non_self_params = (callee.params ?? []).filter((p) => !p.is_self_param);
|
|
7826
|
+
for (let i = 0; i < args.length && i < non_self_params.length; i++) {
|
|
7827
|
+
const arg = args[i];
|
|
7828
|
+
if (arg?.node_type !== "value") continue;
|
|
7829
|
+
const arg_name = arg.value ?? "";
|
|
7830
|
+
const my_fields = tracked.get(arg_name);
|
|
7831
|
+
if (!my_fields) continue;
|
|
7832
|
+
const callee_param = non_self_params[i];
|
|
7833
|
+
if (!(callee_param.is_ref || callee_param.type.is_ref)) continue;
|
|
7834
|
+
const source = scan_ref_param_string_field_writes(callee, structs).get(callee_param.name);
|
|
7835
|
+
if (!source?.size) continue;
|
|
7836
|
+
if (!result.has(arg_name)) result.set(arg_name, /* @__PURE__ */ new Set());
|
|
7837
|
+
const mine = result.get(arg_name);
|
|
7838
|
+
for (const field of source) if (my_fields.has(field)) mine.add(field);
|
|
7839
|
+
}
|
|
7840
|
+
}
|
|
7841
|
+
/** The statement lists nested inside `stmt` (branch bodies, loop bodies) —
|
|
7842
|
+
* the same walk scan_reassigned_vars uses. */
|
|
7843
|
+
function nested_statement_lists(stmt) {
|
|
7844
|
+
const branch_of = (b) => b?.statements ?? [];
|
|
7845
|
+
const lists = [];
|
|
7846
|
+
if (stmt.node_type === "while" || stmt.node_type === "for") {
|
|
7847
|
+
const nested = stmt.statements;
|
|
7848
|
+
if (nested) lists.push(nested);
|
|
7849
|
+
return lists;
|
|
7850
|
+
}
|
|
7851
|
+
if (stmt.node_type === "if") {
|
|
7852
|
+
const n = stmt;
|
|
7853
|
+
if (n.if_branch) lists.push(branch_of(n.if_branch));
|
|
7854
|
+
if (n.else_branch) lists.push(branch_of(n.else_branch));
|
|
7855
|
+
return lists;
|
|
7856
|
+
}
|
|
7857
|
+
if (stmt.node_type === "switch" || stmt.node_type === "match") {
|
|
7858
|
+
const n = stmt;
|
|
7859
|
+
for (const c of n.cases ?? []) if (c.branch) lists.push(branch_of(c.branch));
|
|
7860
|
+
if (n.else_branch) lists.push(branch_of(n.else_branch));
|
|
7861
|
+
return lists;
|
|
7862
|
+
}
|
|
7863
|
+
return lists;
|
|
7749
7864
|
}
|
|
7750
7865
|
/**
|
|
7751
7866
|
* The forwarded-call shape of a top-level statement: a resolved free-function
|
|
@@ -7776,6 +7891,12 @@ function ref_forward_call(stmt) {
|
|
|
7776
7891
|
* stores (scan_ref_param_string_field_writes) onto the caller's records for
|
|
7777
7892
|
* the argument variable, making the caller's scope exit free them.
|
|
7778
7893
|
*
|
|
7894
|
+
* The callee's CONDITIONAL stores transfer too
|
|
7895
|
+
* (ref_param_entry_dup_fields): the callee's builder entry-dups those
|
|
7896
|
+
* fields (`field = strdup(field)` at entry, recorded), so the field is
|
|
7897
|
+
* heap-owned on EVERY path out of the call — the not-taken path holds the
|
|
7898
|
+
* dup — and a caller-side record is sound.
|
|
7899
|
+
*
|
|
7779
7900
|
* `arg_is_trackable_local` must report true only for variables whose storage
|
|
7780
7901
|
* THIS function's scope exit owns — a plain local. A `ref` param of the
|
|
7781
7902
|
* enclosing function forwards caller storage, so a record here would free the
|
|
@@ -7783,19 +7904,22 @@ function ref_forward_call(stmt) {
|
|
|
7783
7904
|
*/
|
|
7784
7905
|
function transfer_ref_param_field_records(callee, args, status, arg_is_trackable_local) {
|
|
7785
7906
|
const writes = scan_ref_param_string_field_writes(callee, status.structs);
|
|
7786
|
-
|
|
7907
|
+
const dups = ref_param_entry_dup_fields(callee, status.structs);
|
|
7908
|
+
if (!writes.size && !dups.size) return;
|
|
7787
7909
|
const non_self_params = (callee.params ?? []).filter((p) => !p.is_self_param);
|
|
7788
7910
|
for (let i = 0; i < args.length && i < non_self_params.length; i++) {
|
|
7789
7911
|
const param = non_self_params[i];
|
|
7790
7912
|
if (!(param.is_ref || param.type.is_ref)) continue;
|
|
7791
7913
|
const fields = writes.get(param.name);
|
|
7792
|
-
|
|
7914
|
+
const dup_fields = dups.get(param.name);
|
|
7915
|
+
if (!fields?.size && !dup_fields?.size) continue;
|
|
7793
7916
|
const arg = args[i];
|
|
7794
7917
|
if (arg?.node_type !== "value") continue;
|
|
7795
7918
|
const name = arg.value;
|
|
7796
7919
|
if (!name || !arg_is_trackable_local(name)) continue;
|
|
7797
7920
|
if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
|
|
7798
|
-
for (const field of fields) status.heap_string_fields.add(`${name}.${field}`);
|
|
7921
|
+
for (const field of fields ?? []) status.heap_string_fields.add(`${name}.${field}`);
|
|
7922
|
+
for (const field of dup_fields ?? []) status.heap_string_fields.add(`${name}.${field}`);
|
|
7799
7923
|
}
|
|
7800
7924
|
}
|
|
7801
7925
|
//#endregion
|
|
@@ -8288,7 +8412,7 @@ function is_container_borrow_accessor_name(name) {
|
|
|
8288
8412
|
* site (see build_return_node), so it counts as owned for classification.
|
|
8289
8413
|
*/
|
|
8290
8414
|
function is_call_site_borrow_accessor(fn_name) {
|
|
8291
|
-
return fn_name === "at" || fn_name === "first";
|
|
8415
|
+
return fn_name === "at" || fn_name === "first" || fn_name === "load";
|
|
8292
8416
|
}
|
|
8293
8417
|
/**
|
|
8294
8418
|
* Whether `node` is a container/buffer borrow accessor CALL (an
|
|
@@ -17957,7 +18081,7 @@ function emit_load_deep_copy_fields$1(elem, status, sret, slot) {
|
|
|
17957
18081
|
const size = get_type_size(field.type, status);
|
|
17958
18082
|
if (!field.type.is_ref && !field.type.is_view && !field.type.is_array && !field.type.is_nullable && field.type.name !== "string") {
|
|
17959
18083
|
const field_struct = resolve_struct_type(field.type, status);
|
|
17960
|
-
if (field_struct && !field_struct.is_class && field_struct.name
|
|
18084
|
+
if (field_struct && !field_struct.is_class && is_load_deep_copy_container(field_struct.name) && struct_needs_destroy(field_struct, status) && field_struct.functions.find((f) => f.name === "copy")) {
|
|
17961
18085
|
emit_asm(status, `add x8, ${sret}, #${offset}\n`);
|
|
17962
18086
|
emit_asm(status, `add x0, ${slot}, #${offset}\n`);
|
|
17963
18087
|
emit_asm(status, `bl ${field_struct.name}_copy\n`);
|
|
@@ -20611,6 +20735,7 @@ function build_function_node$1(node, status) {
|
|
|
20611
20735
|
} else status.nir_emit_ctx = void 0;
|
|
20612
20736
|
const old_raw_param_reloads = status.raw_param_reloads;
|
|
20613
20737
|
install_raw_reload_plan(status, raw_reload_plan_lines);
|
|
20738
|
+
emit_ref_param_entry_dups$1(node, status);
|
|
20614
20739
|
try {
|
|
20615
20740
|
build_block_node$1(node, status);
|
|
20616
20741
|
} finally {
|
|
@@ -20756,6 +20881,49 @@ function build_function_node$1(node, status) {
|
|
|
20756
20881
|
status.return_buffer_stack_offset = void 0;
|
|
20757
20882
|
status.function_return_type = void 0;
|
|
20758
20883
|
}
|
|
20884
|
+
/**
|
|
20885
|
+
* Entry-dup the ref-param string fields the body stores through
|
|
20886
|
+
* CONDITIONALLY (see ref_param_entry_dup_fields): load the field pair's
|
|
20887
|
+
* ptr half, strdup it, store the fresh pointer back — with the field
|
|
20888
|
+
* recorded in heap_string_fields — so the field is heap-owned on every
|
|
20889
|
+
* path out of the function and the call-site record transfer is sound.
|
|
20890
|
+
* Mirrors the C backend's emit_ref_param_entry_dups; the len half is
|
|
20891
|
+
* unchanged by a dup, so only the ptr half is rewritten. A nullable
|
|
20892
|
+
* string field guards the dup (strdup(NULL) traps; a null pair stores as
|
|
20893
|
+
* null and every reclaim path no-ops on free(NULL)).
|
|
20894
|
+
*/
|
|
20895
|
+
function emit_ref_param_entry_dups$1(node, status) {
|
|
20896
|
+
const dups = ref_param_entry_dup_fields(node, status.structs);
|
|
20897
|
+
if (!dups.size) return;
|
|
20898
|
+
for (const param of node.params ?? []) {
|
|
20899
|
+
if (param.is_self_param) continue;
|
|
20900
|
+
if (!(param.is_ref || param.type.is_ref)) continue;
|
|
20901
|
+
const fields = dups.get(param.name);
|
|
20902
|
+
if (!fields?.size) continue;
|
|
20903
|
+
const struct = status.structs.find((s) => s.name === param.type.name && !s.is_simple_type && !s.is_class);
|
|
20904
|
+
for (const field of fields) {
|
|
20905
|
+
const offset = get_field_offset(param.type.name, field, status);
|
|
20906
|
+
const paramReg = status.function_param_regs?.get(param.name);
|
|
20907
|
+
if (paramReg) emit_asm(status, `mov x0, ${paramReg}\n`);
|
|
20908
|
+
else if (is_local_ref_var(param.name, status)) emit_deref_var_address(status, "x0", param.name);
|
|
20909
|
+
else emit_var_address(status, "x0", param.name);
|
|
20910
|
+
emit_asm(status, `str x0, [sp, #-16]!\n`);
|
|
20911
|
+
emit_asm(status, `ldr x0, [x0, #${offset}]\n`);
|
|
20912
|
+
const nullable = !!struct?.fields.find((f) => f.name === field)?.type.is_nullable;
|
|
20913
|
+
let skip_label;
|
|
20914
|
+
if (nullable) {
|
|
20915
|
+
skip_label = `.Lentry_dup_${status.label_counter = (status.label_counter ?? 0) + 1}`;
|
|
20916
|
+
emit_asm(status, `cbz x0, ${skip_label}\n`);
|
|
20917
|
+
}
|
|
20918
|
+
emit_strdup(status);
|
|
20919
|
+
if (skip_label) emit_asm(status, `${skip_label}:\n`);
|
|
20920
|
+
emit_asm(status, `ldr x1, [sp], #16\n`);
|
|
20921
|
+
emit_asm(status, `str x0, [x1, #${offset}]\n`);
|
|
20922
|
+
if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
|
|
20923
|
+
status.heap_string_fields.add(`${param.name}.${field}`);
|
|
20924
|
+
}
|
|
20925
|
+
}
|
|
20926
|
+
}
|
|
20759
20927
|
//#endregion
|
|
20760
20928
|
//#region ../src/build_aarch64/build_struct_node.ts
|
|
20761
20929
|
let field_strdup_guard_counter = 0;
|
|
@@ -23997,6 +24165,14 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
23997
24165
|
if (element_size === 1) emit_asm(status, `strb w0, [x9, #${slot}]\n`);
|
|
23998
24166
|
else if (element_size === 4) emit_asm(status, `str w0, [x9, #${slot}]\n`);
|
|
23999
24167
|
else emit_asm(status, `str x0, [x9, #${slot}]\n`);
|
|
24168
|
+
} else if (element_size > 8 && struct_element && !struct_element.is_class) {
|
|
24169
|
+
emit_struct_address_param(value, status);
|
|
24170
|
+
ensure_newline(status);
|
|
24171
|
+
emit_asm(status, `ldr x9, [x29, #${offset}]\n`);
|
|
24172
|
+
emit_asm(status, `mov x1, x0\n`);
|
|
24173
|
+
emit_asm(status, `add x0, x9, #${slot}\n`);
|
|
24174
|
+
emit_asm(status, `mov x2, #${element_size}\n`);
|
|
24175
|
+
emit_asm(status, `bl _memcpy\n`);
|
|
24000
24176
|
} else {
|
|
24001
24177
|
emit_init_value$1(value, nir_array_element(nir_init, array_literal_node, i), status);
|
|
24002
24178
|
ensure_newline(status);
|
|
@@ -24036,6 +24212,14 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
24036
24212
|
if (element_size === 1) emit_asm(status, `strb w0, [x9, #${slot}]\n`);
|
|
24037
24213
|
else if (element_size === 4) emit_asm(status, `str w0, [x9, #${slot}]\n`);
|
|
24038
24214
|
else emit_asm(status, `str x0, [x9, #${slot}]\n`);
|
|
24215
|
+
} else if (element_size > 8 && struct_element && !struct_element.is_class) {
|
|
24216
|
+
emit_struct_address_param(value, status);
|
|
24217
|
+
ensure_newline(status);
|
|
24218
|
+
emit_asm(status, `ldr x9, [${node.name}]\n`);
|
|
24219
|
+
emit_asm(status, `mov x1, x0\n`);
|
|
24220
|
+
emit_asm(status, `add x0, x9, #${slot}\n`);
|
|
24221
|
+
emit_asm(status, `mov x2, #${element_size}\n`);
|
|
24222
|
+
emit_asm(status, `bl _memcpy\n`);
|
|
24039
24223
|
} else {
|
|
24040
24224
|
emit_init_value$1(value, nir_array_element(nir_init, array_literal_node, i), status);
|
|
24041
24225
|
ensure_newline(status);
|
|
@@ -24142,6 +24326,13 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
24142
24326
|
if (element_size === 1) emit_asm(status, `strb w0, [x29, #${slot_offset}]\n`);
|
|
24143
24327
|
else if (element_size === 4) emit_asm(status, `str w0, [x29, #${slot_offset}]\n`);
|
|
24144
24328
|
else emit_asm(status, `str x0, [x29, #${slot_offset}]\n`);
|
|
24329
|
+
} else if (element_size > 8 && struct_element && !struct_element.is_class) {
|
|
24330
|
+
emit_struct_address_param(value, status);
|
|
24331
|
+
ensure_newline(status);
|
|
24332
|
+
emit_asm(status, `mov x1, x0\n`);
|
|
24333
|
+
emit_asm(status, `add x0, x29, #${slot_offset}\n`);
|
|
24334
|
+
emit_asm(status, `mov x2, #${element_size}\n`);
|
|
24335
|
+
emit_asm(status, `bl _memcpy\n`);
|
|
24145
24336
|
} else {
|
|
24146
24337
|
emit_init_value$1(value, nir_array_element(nir_init, array_values, i), status);
|
|
24147
24338
|
ensure_newline(status);
|
|
@@ -24167,6 +24358,13 @@ function build_declaration_node$1(node, status, nir_init, nir_swap) {
|
|
|
24167
24358
|
if (element_size === 1) emit_asm(status, `strb w0, [${node.name} + ${i * element_size}]\n`);
|
|
24168
24359
|
else if (element_size === 4) emit_asm(status, `str w0, [${node.name} + ${i * element_size}]\n`);
|
|
24169
24360
|
else emit_asm(status, `str x0, [${node.name} + ${i * element_size}]\n`);
|
|
24361
|
+
} else if (element_size > 8 && struct_element && !struct_element.is_class) {
|
|
24362
|
+
emit_struct_address_param(value, status);
|
|
24363
|
+
ensure_newline(status);
|
|
24364
|
+
emit_asm(status, `mov x1, x0\n`);
|
|
24365
|
+
emit_global_slot_addr(status, node.name, i * element_size);
|
|
24366
|
+
emit_asm(status, `mov x2, #${element_size}\n`);
|
|
24367
|
+
emit_asm(status, `bl _memcpy\n`);
|
|
24170
24368
|
} else {
|
|
24171
24369
|
emit_init_value$1(value, nir_array_element(nir_init, array_values, i), status);
|
|
24172
24370
|
ensure_newline(status);
|
|
@@ -34953,6 +35151,7 @@ function build_if_else_node(node, status, nir) {
|
|
|
34953
35151
|
status.code += `) {\n`;
|
|
34954
35152
|
let then_fields;
|
|
34955
35153
|
if (node.if_branch) {
|
|
35154
|
+
status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
|
|
34956
35155
|
build_block_with_cursor(node.if_branch, nir?.then_branch, status);
|
|
34957
35156
|
build_auto_free(status);
|
|
34958
35157
|
}
|
|
@@ -34961,7 +35160,7 @@ function build_if_else_node(node, status, nir) {
|
|
|
34961
35160
|
if (node.else_branch) {
|
|
34962
35161
|
status.scoped_declarations = enter_c_scope(status);
|
|
34963
35162
|
status.deferred_frees = [];
|
|
34964
|
-
status.heap_string_fields = pre_heap_string_fields;
|
|
35163
|
+
status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
|
|
34965
35164
|
status.code += `} else {\n`;
|
|
34966
35165
|
build_block_with_cursor(node.else_branch, nir?.else_branch, status);
|
|
34967
35166
|
build_auto_free(status);
|
|
@@ -35395,7 +35594,7 @@ function build_switch_node(node, status, nir) {
|
|
|
35395
35594
|
for (let i = 0; i < node.cases.length; i++) {
|
|
35396
35595
|
const c = node.cases[i];
|
|
35397
35596
|
status.scoped_declarations = enter_c_scope(status);
|
|
35398
|
-
status.heap_string_fields = pre_heap_string_fields;
|
|
35597
|
+
status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
|
|
35399
35598
|
const saved_code = begin_code_scratch(status);
|
|
35400
35599
|
build_node$1(c.condition, status);
|
|
35401
35600
|
let cond_code = end_code_scratch(status, saved_code);
|
|
@@ -35441,7 +35640,7 @@ function build_switch_node(node, status, nir) {
|
|
|
35441
35640
|
if (node.else_branch) {
|
|
35442
35641
|
status.code += "} else ";
|
|
35443
35642
|
status.scoped_declarations = enter_c_scope(status);
|
|
35444
|
-
status.heap_string_fields = pre_heap_string_fields;
|
|
35643
|
+
status.heap_string_fields = new Set(pre_heap_string_fields ?? []);
|
|
35445
35644
|
status.code += `{\n`;
|
|
35446
35645
|
build_block_with_cursor(node.else_branch, nir?.otherwise ?? void 0, status);
|
|
35447
35646
|
build_auto_free(status);
|
|
@@ -35864,11 +36063,13 @@ function emit_transfer_owning_fields(elem, src, status, prefix = "") {
|
|
|
35864
36063
|
/**
|
|
35865
36064
|
* Emit the deep-copy half of owning load_T: for each field whose type has a
|
|
35866
36065
|
* monomorphized `copy` helper that yields an independent value (a `List<T>`
|
|
35867
|
-
* field — `<
|
|
35868
|
-
* or an enum-with-data field whose string payloads
|
|
35869
|
-
* overwrite the shallow-copied bytes with an owned
|
|
35870
|
-
*
|
|
35871
|
-
*
|
|
36066
|
+
* /`Buffer<T>`/`Map`/`Set` field — `<Mono>_copy` deep-copies the slots or
|
|
36067
|
+
* member buffers — or an enum-with-data field whose string payloads
|
|
36068
|
+
* `<E>_copy` duplicates), overwrite the shallow-copied bytes with an owned
|
|
36069
|
+
* copy. A struct local's CONTAINER fields are reclaimed unconditionally at
|
|
36070
|
+
* scope exit, so a shallow return would double-free the slot's slab. Fields
|
|
36071
|
+
* without a copyable shape (class fields, nested owning user structs without
|
|
36072
|
+
* a synthesized copy) stay shallow — recorded in FOLLOWUP.md.
|
|
35872
36073
|
*/
|
|
35873
36074
|
function emit_load_deep_copy_fields(elem, dst, status) {
|
|
35874
36075
|
for (const field of elem.fields) {
|
|
@@ -35876,7 +36077,7 @@ function emit_load_deep_copy_fields(elem, dst, status) {
|
|
|
35876
36077
|
if (field.type.is_nullable) continue;
|
|
35877
36078
|
if (field.type.name === "string") continue;
|
|
35878
36079
|
const field_struct = resolve_struct_type(field.type, status);
|
|
35879
|
-
if (field_struct && !field_struct.is_class && field_struct.name
|
|
36080
|
+
if (field_struct && !field_struct.is_class && is_load_deep_copy_container(field_struct.name) && struct_needs_destroy(field_struct, status) && field_struct.functions.find((f) => f.name === "copy")) {
|
|
35880
36081
|
status.code += `${dst}.${field.name} = ${field_struct.name}_copy(&${dst}.${field.name});\n`;
|
|
35881
36082
|
continue;
|
|
35882
36083
|
}
|
|
@@ -37470,6 +37671,7 @@ function build_function_node(node, status) {
|
|
|
37470
37671
|
stmts: nir.body,
|
|
37471
37672
|
ast: node.statements
|
|
37472
37673
|
} : void 0;
|
|
37674
|
+
emit_ref_param_entry_dups(node, status);
|
|
37473
37675
|
build_block_node(node, status, false);
|
|
37474
37676
|
status.nir_emit_ctx = old_nir_ctx;
|
|
37475
37677
|
status.function_ref_params = old_ref_params;
|
|
@@ -37515,6 +37717,37 @@ function build_function_node(node, status) {
|
|
|
37515
37717
|
status.stack_array_lengths = old_stack_array_lengths;
|
|
37516
37718
|
status.moved = old_moved;
|
|
37517
37719
|
}
|
|
37720
|
+
/**
|
|
37721
|
+
* Emit the entry-dup for every `ref` value-struct param string field the
|
|
37722
|
+
* body stores through CONDITIONALLY (see ref_param_entry_dup_fields):
|
|
37723
|
+
* `{ field = nomen_str_dup(field) }` at function entry, recorded in
|
|
37724
|
+
* heap_string_fields, so the field is heap-owned on every path out of the
|
|
37725
|
+
* function and the call-site record transfer is sound. Nullable string
|
|
37726
|
+
* fields guard the dup (a null pair stores as null — free(NULL) is a
|
|
37727
|
+
* no-op on every reclaim path).
|
|
37728
|
+
*/
|
|
37729
|
+
function emit_ref_param_entry_dups(node, status) {
|
|
37730
|
+
const dups = ref_param_entry_dup_fields(node, status.structs);
|
|
37731
|
+
if (!dups.size) return;
|
|
37732
|
+
for (const param of node.params ?? []) {
|
|
37733
|
+
if (param.is_self_param) continue;
|
|
37734
|
+
if (!(param.is_ref || param.type.is_ref)) continue;
|
|
37735
|
+
const fields = dups.get(param.name);
|
|
37736
|
+
if (!fields?.size) continue;
|
|
37737
|
+
const struct = status.structs.find((s) => s.name === param.type.name && !s.is_simple_type && !s.is_class);
|
|
37738
|
+
const base = c_function_name(param.name);
|
|
37739
|
+
for (const field of fields) {
|
|
37740
|
+
const nullable = !!struct?.fields.find((f) => f.name === field)?.type.is_nullable;
|
|
37741
|
+
if (nullable) status.code += `if (${base}->${field}.ptr) {\n`;
|
|
37742
|
+
const temp = `_dup_t_${status.label_counter = (status.label_counter ?? 0) + 1}`;
|
|
37743
|
+
status.code += `nomen_string ${temp} = nomen_str_dup(${base}->${field});\n`;
|
|
37744
|
+
status.code += `${base}->${field} = ${temp};\n`;
|
|
37745
|
+
if (nullable) status.code += `}\n`;
|
|
37746
|
+
if (!status.heap_string_fields) status.heap_string_fields = /* @__PURE__ */ new Set();
|
|
37747
|
+
status.heap_string_fields.add(`${param.name}.${field}`);
|
|
37748
|
+
}
|
|
37749
|
+
}
|
|
37750
|
+
}
|
|
37518
37751
|
function emit_nested_declarations(node, status) {
|
|
37519
37752
|
const block = node;
|
|
37520
37753
|
for (let child of block.statements) switch (child.node_type) {
|
|
@@ -37805,6 +38038,17 @@ function build_function_call_node(node, status) {
|
|
|
37805
38038
|
status.code += `${func_name}(${is_closure_callee ? "NULL" : ""}`;
|
|
37806
38039
|
if (is_closure_callee && node.params.length > 0) status.code += ", ";
|
|
37807
38040
|
const variadic_idx = node.variadic_param_name ? node.params.findIndex((p) => p.node_type === "array") : -1;
|
|
38041
|
+
if (!node.variadic_param_name) {
|
|
38042
|
+
const resolved = node.resolved_function ?? (is_struct ? status.structs.find((s) => s.name === node.name)?.functions.find((f) => f.name === "#init") : void 0);
|
|
38043
|
+
const variadic_param = resolved?.params?.find((p) => p.is_variadic);
|
|
38044
|
+
if (resolved && variadic_param) {
|
|
38045
|
+
if (node.params.length === 1 && node.params[0].node_type === "array") node.variadic_param_name = variadic_param.name;
|
|
38046
|
+
else if (node.params.length === 0 && resolved.params.filter((p) => !p.is_self_param && !p.is_variadic).length === 0) {
|
|
38047
|
+
const elem_c = c_type(variadic_param.type.name);
|
|
38048
|
+
status.code += `0, (${elem_c}[]){ }`;
|
|
38049
|
+
}
|
|
38050
|
+
}
|
|
38051
|
+
}
|
|
37808
38052
|
for (let i = 0; i < node.params.length; i++) {
|
|
37809
38053
|
if (i > 0) status.code += ", ";
|
|
37810
38054
|
if (i === variadic_idx && node.params[i].node_type === "array") {
|
|
@@ -41412,7 +41656,11 @@ function build_assignment_node(node, status, nir_rhs, nir_swap) {
|
|
|
41412
41656
|
if (name === "self" && status.current_struct) target_type = new Type(status.current_struct.name);
|
|
41413
41657
|
else if (status.variable_types?.has(name)) target_type = status.variable_types.get(name);
|
|
41414
41658
|
}
|
|
41415
|
-
|
|
41659
|
+
let field_type = access.access.type;
|
|
41660
|
+
if (!field_type?.name && target_type?.name) {
|
|
41661
|
+
const tf = status.structs.find((s) => s.name === target_type.name)?.fields.find((f) => f.name === field_name);
|
|
41662
|
+
if (tf) field_type = tf.type;
|
|
41663
|
+
}
|
|
41416
41664
|
const field_is_struct = is_struct_type$2(field_type, status);
|
|
41417
41665
|
if (access.target.node_type === "value") {
|
|
41418
41666
|
const name = access.target.value;
|
|
@@ -56114,6 +56362,14 @@ function check_function_call(node, status, func, target_type, self_value, self_p
|
|
|
56114
56362
|
}
|
|
56115
56363
|
node.type = func.return_type;
|
|
56116
56364
|
node.is_static = func.is_static;
|
|
56365
|
+
if (target_type?.type_args?.length && node.type?.type_args?.length) {
|
|
56366
|
+
const owner = func.scope;
|
|
56367
|
+
if (owner && owner.node_type === "struct" && owner.is_generic && owner.type_params.length === target_type.type_args.length) {
|
|
56368
|
+
const rebind = /* @__PURE__ */ new Map();
|
|
56369
|
+
owner.type_params.forEach((tp, i) => rebind.set(tp, target_type.type_args[i].name));
|
|
56370
|
+
node.type = substitute_type(node.type, rebind);
|
|
56371
|
+
}
|
|
56372
|
+
}
|
|
56117
56373
|
if (node.type?.type_args?.length) instantiate_generic_type(node.type, status);
|
|
56118
56374
|
if (node.type && node.type.name && self_value && is_class_type$1(node.type.name, status) && receiver_is_const(target_type, self_value, status)) {
|
|
56119
56375
|
node.type = clone_type$2(node.type);
|
|
@@ -56297,7 +56553,10 @@ function check_function_call(node, status, func, target_type, self_value, self_p
|
|
|
56297
56553
|
else {
|
|
56298
56554
|
const stamped = String(func_param.type.length.value);
|
|
56299
56555
|
const incoming = String(param_type.length.value);
|
|
56300
|
-
if (stamped !== incoming)
|
|
56556
|
+
if (stamped !== incoming) {
|
|
56557
|
+
if (is_heap_array_type(func_param.type)) func_param.type.length = void 0;
|
|
56558
|
+
else add_error(status, `Call passes ${incoming} elements to array parameter '${func_param.name}' whose compile-time length is ${stamped} — the raw array ABI carries no runtime length, so one parameter serves one compile-time length; use Array<${func_param.type.name}> for varying lengths`, param.start);
|
|
56559
|
+
}
|
|
56301
56560
|
}
|
|
56302
56561
|
}
|
|
56303
56562
|
let arg_value;
|
|
@@ -56528,6 +56787,13 @@ function check_function_call(node, status, func, target_type, self_value, self_p
|
|
|
56528
56787
|
if (typeof size_val === "number" && size_val > 0) record_buffer_cap(self_path, size_val, status);
|
|
56529
56788
|
}
|
|
56530
56789
|
}
|
|
56790
|
+
if (self_path && self_path !== "?" && is_bufferish && func.name === "copy") {
|
|
56791
|
+
const source_cap = lookup_buffer_cap(self_path, status);
|
|
56792
|
+
if (source_cap !== void 0) {
|
|
56793
|
+
const lhs_name = find_lhs_var_name(status);
|
|
56794
|
+
if (lhs_name) record_buffer_cap(lhs_name, source_cap, status);
|
|
56795
|
+
}
|
|
56796
|
+
}
|
|
56531
56797
|
if (func.return_constraint) {
|
|
56532
56798
|
const param_to_arg = /* @__PURE__ */ new Map();
|
|
56533
56799
|
if (self_path && self_path !== "?") param_to_arg.set("self", path_to_node(self_path));
|
|
@@ -60581,6 +60847,68 @@ function parse_case_branch(status) {
|
|
|
60581
60847
|
return null;
|
|
60582
60848
|
}
|
|
60583
60849
|
//#endregion
|
|
60850
|
+
//#region ../src/parse/utils/parse_embed_file.ts
|
|
60851
|
+
/**
|
|
60852
|
+
* `#embed_file("relative/path")` — a compile-time string literal whose value
|
|
60853
|
+
* is the contents of the file at parse time. The path resolves against the
|
|
60854
|
+
* package root (the directory containing package.jsonc, found by climbing
|
|
60855
|
+
* from the entry file), falling back to the entry file's folder; with no
|
|
60856
|
+
* known base it resolves against the process CWD.
|
|
60857
|
+
*
|
|
60858
|
+
* The parsed form is a plain string-literal ValueNode: escapes are encoded
|
|
60859
|
+
* with the source-level escape set (`\\`, `\"`, `\n`, `\r`, `\t`, and
|
|
60860
|
+
* three-digit octal for other control bytes — self-terminating, matching the
|
|
60861
|
+
* escape scanner), so every downstream consumer (length counting, C/asm
|
|
60862
|
+
* emission) treats it exactly like a hand-written literal.
|
|
60863
|
+
*/
|
|
60864
|
+
function parse_embed_file(status) {
|
|
60865
|
+
const start = get_index(status);
|
|
60866
|
+
consume(status);
|
|
60867
|
+
const name = consume(status);
|
|
60868
|
+
if (name !== "embed_file") {
|
|
60869
|
+
add_error(status, `Unknown compiler directive '#${name}' — did you mean '#embed_file'?`, start);
|
|
60870
|
+
return new ValueNode(start, "\"\"");
|
|
60871
|
+
}
|
|
60872
|
+
if (!expect("(", status)) return new ValueNode(start, "\"\"");
|
|
60873
|
+
const path_token = status.tokens[status.i];
|
|
60874
|
+
if (!path_token || !path_token.value.startsWith("\"")) {
|
|
60875
|
+
add_error(status, "Expected a string literal path in #embed_file", start);
|
|
60876
|
+
return new ValueNode(start, "\"\"");
|
|
60877
|
+
}
|
|
60878
|
+
status.i += 1;
|
|
60879
|
+
const rel_path = path_token.value.slice(1, -1);
|
|
60880
|
+
if (!expect(")", status)) return new ValueNode(start, "\"\"");
|
|
60881
|
+
if (!rel_path) {
|
|
60882
|
+
add_error(status, "#embed_file path is empty", start);
|
|
60883
|
+
return new ValueNode(start, "\"\"");
|
|
60884
|
+
}
|
|
60885
|
+
const base = status.embed_root ?? process.cwd();
|
|
60886
|
+
const full = path.isAbsolute(rel_path) ? rel_path : path.join(base, rel_path);
|
|
60887
|
+
let content;
|
|
60888
|
+
try {
|
|
60889
|
+
content = fs.readFileSync(full, "utf8");
|
|
60890
|
+
} catch {
|
|
60891
|
+
add_error(status, `#embed_file could not read '${rel_path}' (resolved '${full}')`, start);
|
|
60892
|
+
return new ValueNode(start, "\"\"");
|
|
60893
|
+
}
|
|
60894
|
+
return new ValueNode(start, `"${escape_embedded(content)}"`);
|
|
60895
|
+
}
|
|
60896
|
+
/** Encode file contents as a source-level string literal body. */
|
|
60897
|
+
function escape_embedded(content) {
|
|
60898
|
+
let out = "";
|
|
60899
|
+
for (const ch of content) {
|
|
60900
|
+
const code = ch.charCodeAt(0);
|
|
60901
|
+
if (ch === "\\") out += "\\\\";
|
|
60902
|
+
else if (ch === "\"") out += "\\\"";
|
|
60903
|
+
else if (ch === "\n") out += "\\n";
|
|
60904
|
+
else if (ch === "\r") out += "\\r";
|
|
60905
|
+
else if (ch === " ") out += "\\t";
|
|
60906
|
+
else if (code < 32 || code === 127) out += `\\${code.toString(8).padStart(3, "0")}`;
|
|
60907
|
+
else out += ch;
|
|
60908
|
+
}
|
|
60909
|
+
return out;
|
|
60910
|
+
}
|
|
60911
|
+
//#endregion
|
|
60584
60912
|
//#region ../src/parse/parse_expression.ts
|
|
60585
60913
|
function restructure_op(start, current_op, current_prec, current_node, expression) {
|
|
60586
60914
|
if (current_prec <= operator_precedence(expression.op)) {
|
|
@@ -60720,6 +61048,7 @@ function parse_primary(status, value) {
|
|
|
60720
61048
|
expect(")", status);
|
|
60721
61049
|
return node;
|
|
60722
61050
|
}
|
|
61051
|
+
case "#": return parse_embed_file(status);
|
|
60723
61052
|
case "if": return parse_if_else(status);
|
|
60724
61053
|
case "match": return parse_match(status);
|
|
60725
61054
|
case "switch": return parse_switch(status);
|
|
@@ -61862,7 +62191,8 @@ function parse(source, library, file_path, options) {
|
|
|
61862
62191
|
qualified_paths: [],
|
|
61863
62192
|
library,
|
|
61864
62193
|
errors: [],
|
|
61865
|
-
allow_user_raw: options?.allow_user_raw
|
|
62194
|
+
allow_user_raw: options?.allow_user_raw,
|
|
62195
|
+
embed_root: embed_root_for(file_path)
|
|
61866
62196
|
};
|
|
61867
62197
|
if (library) status.unsafe_boundary = user_source_length;
|
|
61868
62198
|
parse_statement(status);
|
|
@@ -61939,6 +62269,23 @@ function mark_library_nodes(root, boundary) {
|
|
|
61939
62269
|
walk(root);
|
|
61940
62270
|
}
|
|
61941
62271
|
/**
|
|
62272
|
+
* The base directory for `#embed_file` paths: the package root (the nearest
|
|
62273
|
+
* enclosing directory with a package.jsonc), falling back to the entry
|
|
62274
|
+
* file's folder. No file path — undefined, so `#embed_file` resolves
|
|
62275
|
+
* against the process CWD.
|
|
62276
|
+
*/
|
|
62277
|
+
function embed_root_for(file_path) {
|
|
62278
|
+
if (!file_path) return void 0;
|
|
62279
|
+
let dir = path.dirname(path.resolve(file_path));
|
|
62280
|
+
for (let i = 0; i < 32; i++) {
|
|
62281
|
+
if (fs.existsSync(path.join(dir, "package.jsonc"))) return dir;
|
|
62282
|
+
const parent = path.dirname(dir);
|
|
62283
|
+
if (parent === dir) break;
|
|
62284
|
+
dir = parent;
|
|
62285
|
+
}
|
|
62286
|
+
return path.dirname(path.resolve(file_path));
|
|
62287
|
+
}
|
|
62288
|
+
/**
|
|
61942
62289
|
* Append the library source that `source` needs, exactly as `parse` does.
|
|
61943
62290
|
* Exported so tooling (e.g. the editor extension) can reproduce the combined
|
|
61944
62291
|
* source that node offsets refer to.
|
|
@@ -62192,7 +62539,7 @@ function print_help() {
|
|
|
62192
62539
|
" nomen check --in [file/folder] Parse and check only",
|
|
62193
62540
|
" nomen format [--in folder] Reformat every .nm file",
|
|
62194
62541
|
" nomen docs [--in file] Generate markdown documentation",
|
|
62195
|
-
" nomen test [--in folder] Discover and run *.test.nm files",
|
|
62542
|
+
" nomen test [--in folder] Discover and run *.test.nm files (binary CWD = --in folder)",
|
|
62196
62543
|
" nomen init <name> Scaffold a new project in ./<name>",
|
|
62197
62544
|
" nomen lib-path Print the bundled System library path",
|
|
62198
62545
|
"",
|
|
@@ -62782,7 +63129,7 @@ function extract_leaks(stdout) {
|
|
|
62782
63129
|
* Compile one `*.test.nm` file (with the generated harness) and run it,
|
|
62783
63130
|
* returning its parsed records and any crash info.
|
|
62784
63131
|
*/
|
|
62785
|
-
function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime) {
|
|
63132
|
+
function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime, release = false, cwd) {
|
|
62786
63133
|
const start = performance.now();
|
|
62787
63134
|
const source_text = fs.readFileSync(entry_path, "utf8");
|
|
62788
63135
|
const harness = generate_harness(extract_test_functions(source_text), extract_bench_functions(source_text));
|
|
@@ -62809,7 +63156,8 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
|
|
|
62809
63156
|
}
|
|
62810
63157
|
const buildResult = build(parsed.root, {
|
|
62811
63158
|
arch,
|
|
62812
|
-
audit
|
|
63159
|
+
audit,
|
|
63160
|
+
optimize: release
|
|
62813
63161
|
});
|
|
62814
63162
|
if (buildResult.errors && buildResult.errors.length) {
|
|
62815
63163
|
result.ok = false;
|
|
@@ -62844,11 +63192,19 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
|
|
|
62844
63192
|
const outfile = path.join(buildDir, path.basename(entry_path, ".nm"));
|
|
62845
63193
|
fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
|
|
62846
63194
|
fs.writeFileSync(codefile, buildResult.code);
|
|
63195
|
+
let companionfile;
|
|
63196
|
+
if (buildResult.companion) {
|
|
63197
|
+
const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
|
|
63198
|
+
companionfile = path.join(buildDir, path.basename(entry_path, ".nm") + "_companion" + comp_ext);
|
|
63199
|
+
fs.writeFileSync(companionfile, buildResult.companion);
|
|
63200
|
+
}
|
|
62847
63201
|
const link_args = [
|
|
62848
63202
|
"-o",
|
|
62849
63203
|
outfile,
|
|
62850
63204
|
codefile
|
|
62851
63205
|
];
|
|
63206
|
+
if (companionfile) link_args.push(companionfile);
|
|
63207
|
+
if (release) link_args.unshift("-O2");
|
|
62852
63208
|
if (audit_obj) link_args.push(audit_obj);
|
|
62853
63209
|
if (platform === "macos" || platform === "ios") link_args.push("-framework", "CoreGraphics", "-framework", "Foundation", "-framework", "AppKit", "-lobjc");
|
|
62854
63210
|
try {
|
|
@@ -62879,7 +63235,8 @@ function run_test_file(entry_path, lib_path, arch, audit = false, audit_runtime)
|
|
|
62879
63235
|
"pipe"
|
|
62880
63236
|
],
|
|
62881
63237
|
timeout: 3e4,
|
|
62882
|
-
maxBuffer: 16777216
|
|
63238
|
+
maxBuffer: 16777216,
|
|
63239
|
+
cwd: cwd ?? process.cwd()
|
|
62883
63240
|
});
|
|
62884
63241
|
} catch (err) {
|
|
62885
63242
|
runStdout = err.stdout ? err.stdout.toString() : "";
|
|
@@ -62961,9 +63318,10 @@ function runTests(root, options = {}) {
|
|
|
62961
63318
|
const files = collect_test_files(root).filter((f) => !options.filter || options.filter.test(f));
|
|
62962
63319
|
console.log(`Found ${files.length} test file(s)\n`);
|
|
62963
63320
|
const startTime = performance.now();
|
|
63321
|
+
const binary_cwd = path.resolve(root);
|
|
62964
63322
|
const results = [];
|
|
62965
63323
|
for (const file of files) {
|
|
62966
|
-
const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime);
|
|
63324
|
+
const result = run_test_file(file, lib, arch, options.audit, options.audit_runtime, options.release, binary_cwd);
|
|
62967
63325
|
report_file(result);
|
|
62968
63326
|
results.push(result);
|
|
62969
63327
|
}
|
|
@@ -63071,7 +63429,8 @@ try {
|
|
|
63071
63429
|
arch: args.arch ?? "aarch64",
|
|
63072
63430
|
filter,
|
|
63073
63431
|
audit: args.audit,
|
|
63074
|
-
audit_runtime: args.audit_runtime
|
|
63432
|
+
audit_runtime: args.audit_runtime,
|
|
63433
|
+
release: args.release
|
|
63075
63434
|
});
|
|
63076
63435
|
process.exit(ok ? 0 : 1);
|
|
63077
63436
|
}
|
package/package.json
CHANGED
package/src/args.ts
CHANGED
|
@@ -160,7 +160,7 @@ export function print_help(): void {
|
|
|
160
160
|
" nomen check --in [file/folder] Parse and check only",
|
|
161
161
|
" nomen format [--in folder] Reformat every .nm file",
|
|
162
162
|
" nomen docs [--in file] Generate markdown documentation",
|
|
163
|
-
" nomen test [--in folder] Discover and run *.test.nm files",
|
|
163
|
+
" nomen test [--in folder] Discover and run *.test.nm files (binary CWD = --in folder)",
|
|
164
164
|
" nomen init <name> Scaffold a new project in ./<name>",
|
|
165
165
|
" nomen lib-path Print the bundled System library path",
|
|
166
166
|
"",
|
package/src/index.ts
CHANGED
package/src/test.ts
CHANGED
|
@@ -278,6 +278,8 @@ export function run_test_file(
|
|
|
278
278
|
arch: string,
|
|
279
279
|
audit = false,
|
|
280
280
|
audit_runtime?: string,
|
|
281
|
+
release = false,
|
|
282
|
+
cwd?: string,
|
|
281
283
|
): TestFileResult {
|
|
282
284
|
const start = performance.now();
|
|
283
285
|
const source_text = fs.readFileSync(entry_path, "utf8");
|
|
@@ -318,6 +320,7 @@ export function run_test_file(
|
|
|
318
320
|
const buildResult = build(parsed.root, {
|
|
319
321
|
arch: arch as "aarch64" | "c",
|
|
320
322
|
audit,
|
|
323
|
+
optimize: release,
|
|
321
324
|
});
|
|
322
325
|
if (buildResult.errors && buildResult.errors.length) {
|
|
323
326
|
result.ok = false;
|
|
@@ -363,11 +366,28 @@ export function run_test_file(
|
|
|
363
366
|
fs.writeFileSync(path.join(buildDir, "main.h"), buildResult.headers ?? "");
|
|
364
367
|
fs.writeFileSync(codefile, buildResult.code);
|
|
365
368
|
|
|
369
|
+
// Companion C (raw-block functions + the pool/fiber/file-scope C infra):
|
|
370
|
+
// `run`/`build` compile and link it next to the main TU. The aarch64 .s
|
|
371
|
+
// references its symbols via `bl` (spawn machinery), so dropping it makes
|
|
372
|
+
// companion-bearing test files fail to link. Same .m/.c rule as the
|
|
373
|
+
// run/build command's companion emission.
|
|
374
|
+
let companionfile: string | undefined;
|
|
375
|
+
if (buildResult.companion) {
|
|
376
|
+
const comp_ext = platform === "macos" || platform === "ios" ? ".m" : ".c";
|
|
377
|
+
companionfile = path.join(buildDir, path.basename(entry_path, ".nm") + "_companion" + comp_ext);
|
|
378
|
+
fs.writeFileSync(companionfile, buildResult.companion);
|
|
379
|
+
}
|
|
380
|
+
|
|
366
381
|
// Link the harness binary. Frameworks are passed on Apple platforms for
|
|
367
382
|
// the same reason as the run/build command: the joined source may reference
|
|
368
383
|
// ObjC runtime symbols even when the test body doesn't, and unused
|
|
369
|
-
// frameworks cost nothing at link time.
|
|
384
|
+
// frameworks cost nothing at link time. Release mode compiles the C (and
|
|
385
|
+
// companion C) with -O2, matching the run/build command; the aarch64 .s is
|
|
386
|
+
// assembled verbatim — its optimizations are the passes enabled via
|
|
387
|
+
// `optimize` above.
|
|
370
388
|
const link_args = ["-o", outfile, codefile];
|
|
389
|
+
if (companionfile) link_args.push(companionfile);
|
|
390
|
+
if (release) link_args.unshift("-O2");
|
|
371
391
|
if (audit_obj) link_args.push(audit_obj);
|
|
372
392
|
if (platform === "macos" || platform === "ios") {
|
|
373
393
|
link_args.push(
|
|
@@ -396,7 +416,11 @@ export function run_test_file(
|
|
|
396
416
|
|
|
397
417
|
// Run the binary and collect the records it streams over stdout. A
|
|
398
418
|
// non-zero exit (crash, abort) still surfaces whatever records were
|
|
399
|
-
// emitted before the crash; a timeout is treated as a crash.
|
|
419
|
+
// emitted before the crash; a timeout is treated as a crash. The
|
|
420
|
+
// documented CWD contract: the binary runs in the `--in` root (the
|
|
421
|
+
// folder `nomen test` scanned for *.test.nm files), so fixture paths in
|
|
422
|
+
// tests resolve against that folder regardless of where the CLI was
|
|
423
|
+
// invoked from.
|
|
400
424
|
let runStdout = "";
|
|
401
425
|
let crashed: string | undefined;
|
|
402
426
|
try {
|
|
@@ -405,6 +429,7 @@ export function run_test_file(
|
|
|
405
429
|
stdio: ["ignore", "pipe", "pipe"],
|
|
406
430
|
timeout: 30_000,
|
|
407
431
|
maxBuffer: 16 * 1024 * 1024,
|
|
432
|
+
cwd: cwd ?? process.cwd(),
|
|
408
433
|
});
|
|
409
434
|
} catch (err: any) {
|
|
410
435
|
runStdout = err.stdout ? err.stdout.toString() : "";
|
|
@@ -519,6 +544,7 @@ export interface RunTestsOptions {
|
|
|
519
544
|
filter?: RegExp;
|
|
520
545
|
audit?: boolean;
|
|
521
546
|
audit_runtime?: string;
|
|
547
|
+
release?: boolean;
|
|
522
548
|
}
|
|
523
549
|
|
|
524
550
|
/**
|
|
@@ -547,10 +573,21 @@ export function runTests(root: string, options: RunTestsOptions = {}): boolean {
|
|
|
547
573
|
console.log(`Found ${files.length} test file(s)\n`);
|
|
548
574
|
|
|
549
575
|
const startTime = performance.now();
|
|
576
|
+
// The CWD contract: test binaries run in the --in root, so relative
|
|
577
|
+
// fixture paths resolve there no matter where the CLI was invoked from.
|
|
578
|
+
const binary_cwd = path.resolve(root);
|
|
550
579
|
|
|
551
580
|
const results: TestFileResult[] = [];
|
|
552
581
|
for (const file of files) {
|
|
553
|
-
const result = run_test_file(
|
|
582
|
+
const result = run_test_file(
|
|
583
|
+
file,
|
|
584
|
+
lib,
|
|
585
|
+
arch,
|
|
586
|
+
options.audit,
|
|
587
|
+
options.audit_runtime,
|
|
588
|
+
options.release,
|
|
589
|
+
binary_cwd,
|
|
590
|
+
);
|
|
554
591
|
report_file(result);
|
|
555
592
|
results.push(result);
|
|
556
593
|
}
|