nomen-lang 0.0.16 → 0.0.17

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.
@@ -356,7 +356,11 @@ pub struct Buffer<T> {
356
356
  ```
357
357
  }
358
358
 
359
- func load_T = (self, int i: i >= 0 && i < self.cap, out T) {
359
+ // `inline` splices the (width-folded at monomorphization) raw body at
360
+ // call sites — the load/store pair behind every container `.at`/`set` is
361
+ // the hottest path in data-heavy code, and the naked-inline path keeps it
362
+ // to a handful of instructions with no call overhead.
363
+ inline func load_T = (self, int i: i >= 0 && i < self.cap, out T) {
360
364
  ```
361
365
  #arch: c
362
366
  return ((T*)(unsigned long long)self->data)[i];
@@ -399,7 +403,7 @@ pub struct Buffer<T> {
399
403
  ```
400
404
  }
401
405
 
402
- func store_T = (ref self, int i: i >= 0 && i < self.cap, T val) {
406
+ inline func store_T = (ref self, int i: i >= 0 && i < self.cap, T val) {
403
407
  ```
404
408
  #arch: c
405
409
  ((T*)(unsigned long long)self->data)[i] = val;
@@ -57,6 +57,22 @@ pub struct LinkedList<T>: Enumerable {
57
57
  return self.values.load_T(idx)
58
58
  }
59
59
 
60
+ // Bounds-checked access: `fallback` when idx is outside [0, self.count).
61
+ pub func at_or = (self, int idx, T fallback, out T) {
62
+ if idx >= 0 && idx < self.count {
63
+ return self.at(idx)
64
+ }
65
+ return fallback
66
+ }
67
+
68
+ // Bounds-checked access that traps when idx is outside [0, self.count).
69
+ pub func at_or_panic = (self, int idx, out T) {
70
+ if idx >= 0 && idx < self.count {
71
+ return self.at(idx)
72
+ }
73
+ panic("index out of range")
74
+ }
75
+
60
76
  pub func length = (self, out int) {
61
77
  return self.count
62
78
  }
@@ -31,6 +31,26 @@ pub struct List<T>: Viewable {
31
31
  return self.items.load_T(i)
32
32
  }
33
33
 
34
+ // Bounds-checked access: `fallback` when i is outside [0, self.length) —
35
+ // for indices that can't be proven at compile time and out-of-range is an
36
+ // expected case.
37
+ pub func at_or = (self, int i, T fallback, out T) {
38
+ if i >= 0 && i < self.length {
39
+ return self.at(i)
40
+ }
41
+ return fallback
42
+ }
43
+
44
+ // Bounds-checked access that traps when i is outside [0, self.length) —
45
+ // for indices that can't be proven at compile time and out-of-range is a
46
+ // bug, not a case.
47
+ pub func at_or_panic = (self, int i, out T) {
48
+ if i >= 0 && i < self.length {
49
+ return self.at(i)
50
+ }
51
+ panic("index out of range")
52
+ }
53
+
34
54
  // A non-owning `view T` over [start, end). Delegates to the backing Buffer's
35
55
  // #arch slice primitive — no #arch code here. Borrows from self.
36
56
  pub func slice = (self, int start: start >= 0, int end: end >= start, out view T) {
@@ -1,7 +1,12 @@
1
1
  /**
2
- * An ordered key-value map with `set`/`get`/`has`/`remove`. Keys must be
2
+ * An ordered key-value map with `set`/`get`/`get_or`/`has`/`remove`. Keys must be
3
3
  * `Hashable` (hashed to a `uint` via `key.hash()`) and `Equatable` (compared
4
4
  * with `==`); values may be any type.
5
+ *
6
+ * The capacity is always a power of two (initial 8, doubling on rehash), so
7
+ * bucket indexing is a mask (`hash & (cap - 1)`) instead of a division — and
8
+ * it is correct for negative hashes too: two's-complement `&` already lands
9
+ * in `[0, cap)`, which is why no negative adjustment is needed.
5
10
  **/
6
11
  pub struct Map<TK: Hashable + Equatable, TV> {
7
12
  var length = 0
@@ -53,6 +58,34 @@ pub struct Map<TK: Hashable + Equatable, TV> {
53
58
  return self.values.load_T(idx)
54
59
  }
55
60
 
61
+ // Fused lookup-with-fallback: one probe instead of the `has` + `get`
62
+ // pair (which probes twice) for the common "default when missing" shape.
63
+ pub func get_or = (self, TK key, TV fallback, out TV) {
64
+ var int cap = self.keys.cap
65
+ if cap == 0 {
66
+ return fallback
67
+ }
68
+ var int idx = self.find_slot(key, cap)
69
+ if self.used.load(idx) == 0 {
70
+ return fallback
71
+ }
72
+ return self.values.load_T(idx)
73
+ }
74
+
75
+ // Lookup that traps when the key is absent — for when a missing key is
76
+ // a bug, not a case (and `get`'s 0-for-missing would be ambiguous).
77
+ pub func get_or_panic = (self, TK key, out TV) {
78
+ var int cap = self.keys.cap
79
+ if cap == 0 {
80
+ panic("key not found")
81
+ }
82
+ var int idx = self.find_slot(key, cap)
83
+ if self.used.load(idx) == 0 {
84
+ panic("key not found")
85
+ }
86
+ return self.values.load_T(idx)
87
+ }
88
+
56
89
  pub func has = (self, TK key, out bool) {
57
90
  var int cap = self.keys.cap
58
91
  if cap == 0 {
@@ -91,10 +124,7 @@ pub struct Map<TK: Hashable + Equatable, TV> {
91
124
  if self.used.load(k) == 0 {
92
125
  break
93
126
  }
94
- var int home = (self.keys.load_T(k).hash() as int) % cap
95
- if home < 0 {
96
- home = home + cap
97
- }
127
+ var int home = (self.keys.load_T(k).hash() as int) & (cap - 1)
98
128
  var int dgap = gap - home
99
129
  if dgap < 0 {
100
130
  dgap = dgap + cap
@@ -115,10 +145,9 @@ pub struct Map<TK: Hashable + Equatable, TV> {
115
145
  }
116
146
 
117
147
  func find_slot = (self, TK key, int cap, out int: out >= 0 && out < cap) {
118
- var int idx = (key.hash() as int) % cap
119
- if idx < 0 {
120
- idx = idx + cap
121
- }
148
+ // cap is a power of two, so this is `hash mod cap` via a mask —
149
+ // non-negative for any hash (two's complement), no adjustment.
150
+ var int idx = (key.hash() as int) & (cap - 1)
122
151
  while self.used.load(idx) != 0 {
123
152
  if self.keys.load_T(idx) == key {
124
153
  return idx
@@ -2,6 +2,10 @@
2
2
  * An ordered set of unique values (`add`/`has`/`get`/`remove`). Elements must
3
3
  * be `Hashable` (hashed to a `uint` via `value.hash()`) and `Equatable`
4
4
  * (compared with `==`).
5
+ *
6
+ * The capacity is always a power of two (initial 8, doubling on rehash), so
7
+ * slot indexing is a mask (`hash & (cap - 1)`) — correct for negative hashes
8
+ * too (two's-complement `&` already lands in `[0, cap)`).
5
9
  **/
6
10
  pub struct Set<T: Hashable + Equatable> {
7
11
  var length = 0
@@ -47,6 +51,33 @@ pub struct Set<T: Hashable + Equatable> {
47
51
  return self.slots.load_T(idx)
48
52
  }
49
53
 
54
+ // Membership lookup with a fallback value: `fallback` when the value is
55
+ // not in the set (one probe instead of the `has` + `get` pair).
56
+ pub func get_or = (self, T key, T fallback, out T) {
57
+ var int cap = self.slots.cap
58
+ if cap == 0 {
59
+ return fallback
60
+ }
61
+ var int idx = self.find_slot(key, cap)
62
+ if self.used.load(idx) == 0 {
63
+ return fallback
64
+ }
65
+ return self.slots.load_T(idx)
66
+ }
67
+
68
+ // Membership lookup that traps when the value is not in the set.
69
+ pub func get_or_panic = (self, T key, out T) {
70
+ var int cap = self.slots.cap
71
+ if cap == 0 {
72
+ panic("value not found in set")
73
+ }
74
+ var int idx = self.find_slot(key, cap)
75
+ if self.used.load(idx) == 0 {
76
+ panic("value not found in set")
77
+ }
78
+ return self.slots.load_T(idx)
79
+ }
80
+
50
81
  pub func remove = (ref self, T value) {
51
82
  var int cap = self.slots.cap
52
83
  if cap == 0 {
@@ -76,10 +107,7 @@ pub struct Set<T: Hashable + Equatable> {
76
107
  if self.used.load(k) == 0 {
77
108
  break
78
109
  }
79
- var int home = (self.slots.load_T(k).hash() as int) % cap
80
- if home < 0 {
81
- home = home + cap
82
- }
110
+ var int home = (self.slots.load_T(k).hash() as int) & (cap - 1)
83
111
  var int dgap = gap - home
84
112
  if dgap < 0 {
85
113
  dgap = dgap + cap
@@ -99,10 +127,9 @@ pub struct Set<T: Hashable + Equatable> {
99
127
  }
100
128
 
101
129
  func find_slot = (self, T value, int cap, out int: out >= 0 && out < cap) {
102
- var int idx = (value.hash() as int) % cap
103
- if idx < 0 {
104
- idx = idx + cap
105
- }
130
+ // cap is a power of two: `hash mod cap` via a mask, non-negative
131
+ // for any hash — no adjustment needed.
132
+ var int idx = (value.hash() as int) & (cap - 1)
106
133
  while self.used.load(idx) != 0 {
107
134
  if self.slots.load_T(idx) == value {
108
135
  return idx
@@ -51,6 +51,24 @@ pub struct string: Stringable, Hashable, Equatable {
51
51
  ```
52
52
  }
53
53
 
54
+ // Bounds-checked access: `fallback` when index is outside
55
+ // [0, self.length).
56
+ pub func at_or = (self, int index, char fallback, out char) {
57
+ if index >= 0 && index < self.length {
58
+ return self.at(index)
59
+ }
60
+ return fallback
61
+ }
62
+
63
+ // Bounds-checked access that traps when index is outside
64
+ // [0, self.length) — out-of-range is a bug, not a case.
65
+ pub func at_or_panic = (self, int index, out char) {
66
+ if index >= 0 && index < self.length {
67
+ return self.at(index)
68
+ }
69
+ panic("index out of range")
70
+ }
71
+
54
72
  // A non-owning slice [start, end) into self's buffer. Returns a `view
55
73
  // string` (ptr, len) that borrows from self: it may not outlive self's
56
74
  // scope and is invalidated if self is reassigned. Use .to_string() to
package/dist/index.mjs CHANGED
@@ -11779,7 +11779,7 @@ function extract_aarch64_asm(func, platform) {
11779
11779
  return asm;
11780
11780
  }
11781
11781
  function count_x19_reads(asm) {
11782
- const matches = asm.match(/\bx19\b/g);
11782
+ const matches = asm.split("\n").map((l) => l.replace(/\/\/.*$/, "")).join("\n").match(/\bx19\b/g);
11783
11783
  return matches ? matches.length : 0;
11784
11784
  }
11785
11785
  function build_naked_inline(struct_node, func, status) {
@@ -11787,6 +11787,8 @@ function build_naked_inline(struct_node, func, status) {
11787
11787
  const standalone_return_label = `.return_${struct_node.name}_${func.name.replace(/#/g, "")}`;
11788
11788
  asm = asm.replaceAll(`b ${standalone_return_label}`, "");
11789
11789
  if (count_x19_reads(asm) === 1) asm = asm.replace(/\bx19\b/g, "x0");
11790
+ const site = inline_counter++;
11791
+ asm = asm.replace(/(\.L[A-Za-z0-9_]+)/g, `$1_${site}`);
11790
11792
  status.code += asm + "\n";
11791
11793
  }
11792
11794
  function build_inline_method(struct_node, func, status) {
@@ -11811,9 +11813,11 @@ function build_inline_method(struct_node, func, status) {
11811
11813
  const old_buffer_data_cache = status.buffer_data_cache;
11812
11814
  const old_heap_cleanup_stack = status.heap_cleanup_stack;
11813
11815
  const old_moved = status.moved;
11816
+ const old_outer_scope_declarations = status.outer_scope_declarations;
11814
11817
  const return_label = `.inline_ret_${inline_counter++}`;
11815
11818
  status.function_return_label = return_label;
11816
11819
  status.scoped_declarations = [];
11820
+ status.outer_scope_declarations = [];
11817
11821
  status.function_return_type = void 0;
11818
11822
  status.struct_return_buffer = void 0;
11819
11823
  status.return_buffer_stack_offset = void 0;
@@ -11900,6 +11904,7 @@ function build_inline_method(struct_node, func, status) {
11900
11904
  status.buffer_data_cache = old_buffer_data_cache;
11901
11905
  status.heap_cleanup_stack = old_heap_cleanup_stack;
11902
11906
  status.moved = old_moved;
11907
+ status.outer_scope_declarations = old_outer_scope_declarations;
11903
11908
  }
11904
11909
  let inline_fn_depth = 0;
11905
11910
  const MAX_INLINE_DEPTH = 2;
@@ -11919,9 +11924,11 @@ function build_inline_function(func, status) {
11919
11924
  const old_buffer_data_cache = status.buffer_data_cache;
11920
11925
  const old_heap_cleanup_stack = status.heap_cleanup_stack;
11921
11926
  const old_moved = status.moved;
11927
+ const old_outer_scope_declarations = status.outer_scope_declarations;
11922
11928
  const return_label = `.inline_fn_ret_${inline_counter++}`;
11923
11929
  status.function_return_label = return_label;
11924
11930
  status.scoped_declarations = [];
11931
+ status.outer_scope_declarations = [];
11925
11932
  status.function_return_type = void 0;
11926
11933
  status.struct_return_buffer = void 0;
11927
11934
  status.return_buffer_stack_offset = void 0;
@@ -11996,6 +12003,7 @@ function build_inline_function(func, status) {
11996
12003
  status.buffer_data_cache = old_buffer_data_cache;
11997
12004
  status.heap_cleanup_stack = old_heap_cleanup_stack;
11998
12005
  status.moved = old_moved;
12006
+ status.outer_scope_declarations = old_outer_scope_declarations;
11999
12007
  inline_fn_depth--;
12000
12008
  return true;
12001
12009
  }
@@ -18257,7 +18265,7 @@ function build_value_node(node, status) {
18257
18265
  else value = c_function_name(value);
18258
18266
  if (value.startsWith("\"")) value = escape_c_string(value);
18259
18267
  if (value !== "self" && status.function_ref_params?.has(value) && !status.suppress_dereference) status.code += `*`;
18260
- if (value === "self" && status.function_ref_params?.has("self") && !status.suppress_dereference) status.code += `*`;
18268
+ if (value === "self" && status.function_ref_params?.has("self") && !status.suppress_dereference && status.current_struct?.name !== "string") status.code += `*`;
18261
18269
  if (value !== "self" && status.ref_class_params?.has(value) && !status.suppress_dereference) {
18262
18270
  status.code += `(*${value})`;
18263
18271
  return;
@@ -20386,6 +20394,7 @@ function clone_node(node) {
20386
20394
  c.return_constraint = n.return_constraint ? clone_node(n.return_constraint) : void 0;
20387
20395
  c.returns_mov = n.returns_mov;
20388
20396
  c.is_library = n.is_library;
20397
+ c.is_inline = n.is_inline;
20389
20398
  return c;
20390
20399
  }
20391
20400
  case "param": {
@@ -20954,6 +20963,40 @@ function snapshot_bounds(name, status) {
20954
20963
  lower_bound_inclusive_exprs: decl.lower_bound_inclusive_exprs?.slice()
20955
20964
  };
20956
20965
  }
20966
+ function snapshot_all_bounds(status) {
20967
+ const snap = /* @__PURE__ */ new Map();
20968
+ for (const v of status.values) snap.set(v, {
20969
+ range_lower: v.range_lower,
20970
+ range_upper: v.range_upper,
20971
+ upper_bound_exprs: v.upper_bound_exprs?.slice(),
20972
+ lower_bound_exprs: v.lower_bound_exprs?.slice(),
20973
+ upper_bound_inclusive_exprs: v.upper_bound_inclusive_exprs?.slice(),
20974
+ lower_bound_inclusive_exprs: v.lower_bound_inclusive_exprs?.slice(),
20975
+ upper_bound_expr: v.upper_bound_expr,
20976
+ lower_bound_expr: v.lower_bound_expr,
20977
+ alias_of: v.alias_of,
20978
+ known_length: v.known_length,
20979
+ path_bounds: v.path_bounds ? new Map(v.path_bounds) : void 0
20980
+ });
20981
+ return snap;
20982
+ }
20983
+ function restore_all_bounds(status, snap) {
20984
+ for (const v of status.values) {
20985
+ const s = snap.get(v);
20986
+ if (!s) continue;
20987
+ v.range_lower = s.range_lower;
20988
+ v.range_upper = s.range_upper;
20989
+ v.upper_bound_exprs = s.upper_bound_exprs;
20990
+ v.lower_bound_exprs = s.lower_bound_exprs;
20991
+ v.upper_bound_inclusive_exprs = s.upper_bound_inclusive_exprs;
20992
+ v.lower_bound_inclusive_exprs = s.lower_bound_inclusive_exprs;
20993
+ v.upper_bound_expr = s.upper_bound_expr;
20994
+ v.lower_bound_expr = s.lower_bound_expr;
20995
+ v.alias_of = s.alias_of;
20996
+ v.known_length = s.known_length;
20997
+ v.path_bounds = s.path_bounds;
20998
+ }
20999
+ }
20957
21000
  /**
20958
21001
  * Serialize an AST expression to a canonical string for comparison.
20959
21002
  * E.g. `list.length` → "list.length", `self.length` → resolves alias to actual var.
@@ -20963,7 +21006,7 @@ function expr_to_string(node, status) {
20963
21006
  const vn = node;
20964
21007
  if (status) {
20965
21008
  const decl = status.values.findLast((v) => v.name === vn.value);
20966
- if (decl?.alias_of) return decl.alias_of;
21009
+ if (decl?.alias_of) return canonicalize_length_path(decl.alias_of, status);
20967
21010
  }
20968
21011
  return vn.value;
20969
21012
  }
@@ -20972,7 +21015,10 @@ function expr_to_string(node, status) {
20972
21015
  if (access.access.node_type === "access_field") {
20973
21016
  const target = expr_to_string(access.target, status);
20974
21017
  const field = access.access.name;
20975
- if (target) return `${target}.${field}`;
21018
+ if (target) {
21019
+ const path = `${target}.${field}`;
21020
+ return status ? canonicalize_length_path(path, status) : path;
21021
+ }
20976
21022
  } else if (access.access.node_type === "access_func") {
20977
21023
  const target = expr_to_string(access.target, status);
20978
21024
  const method = access.access.name;
@@ -20981,9 +21027,88 @@ function expr_to_string(node, status) {
20981
21027
  "size",
20982
21028
  "count",
20983
21029
  "cap"
20984
- ].includes(method)) return `${target}.${method}`;
20985
- }
20986
- }
21030
+ ].includes(method)) {
21031
+ const path = `${target}.${method}`;
21032
+ return status ? canonicalize_length_path(path, status) : path;
21033
+ }
21034
+ }
21035
+ }
21036
+ }
21037
+ /**
21038
+ * Rewrite a `<base>.<field>` path through the assumed parallel-length
21039
+ * equalities (`a.length == b.length` param contracts) to a canonical
21040
+ * representative, so a bound phrased against either container's length
21041
+ * (`i < xs.length`) verifies a constraint on the other (`hash.length`).
21042
+ * Each equation's `a` side is the representative; following `b → a` edges
21043
+ * to a fixed point makes the rewrite idempotent and confluent for chains
21044
+ * (a==b, b==c all canonicalize to the chain head). Only touches paths whose
21045
+ * field has a recorded equation.
21046
+ */
21047
+ function canonicalize_length_path(path, status) {
21048
+ const eqs = status.equal_lengths;
21049
+ if (!eqs?.length) return path;
21050
+ const m = /^(\w+)\.(length|count|size|cap)$/.exec(path);
21051
+ if (!m) return path;
21052
+ const field = m[2];
21053
+ let base = m[1];
21054
+ for (let step = 0; step < eqs.length; step++) {
21055
+ const eq = eqs.find((e) => e.field === field && (e.a === base || e.b === base));
21056
+ if (!eq || eq.a === base) break;
21057
+ base = eq.a;
21058
+ }
21059
+ return `${base}.${field}`;
21060
+ }
21061
+ /**
21062
+ * Remove `X.<f> == Y.<f>` clauses (same container-size field on two params,
21063
+ * one of them `param_name`) from a parameter constraint, recording each as an
21064
+ * ASSUMED parallel-length equality on `status.equal_lengths`. Such a clause
21065
+ * relates two runtime lengths, which the verifier can never prove at a call
21066
+ * site — instead it is trusted there (stripped from the contract) and made
21067
+ * available to the body's bounds machinery via expr_to_string
21068
+ * canonicalization. Returns the rewritten constraint, or undefined when
21069
+ * everything was stripped. Nodes are only rebuilt when something changed.
21070
+ */
21071
+ function strip_length_equalities(constraint, param_name, status) {
21072
+ if (!constraint) return void 0;
21073
+ if (constraint.node_type !== "op") return constraint;
21074
+ const op = constraint;
21075
+ if (op.op === "&&") {
21076
+ const left = strip_length_equalities(op.left_value, param_name, status);
21077
+ const right = strip_length_equalities(op.right_value, param_name, status);
21078
+ if (left && right) {
21079
+ if (left === op.left_value && right === op.right_value) return constraint;
21080
+ return new OperationNode(op.start, "&&", left, right, op.type);
21081
+ }
21082
+ return left ?? right;
21083
+ }
21084
+ if (op.op !== "==") return constraint;
21085
+ const extract = (side) => {
21086
+ if (side.node_type !== "access") return void 0;
21087
+ const access = side;
21088
+ let field;
21089
+ if (access.access.node_type === "access_field") field = access.access.name;
21090
+ else if (access.access.node_type === "access_func") field = access.access.name;
21091
+ if (!field || !NON_NEGATIVE_FIELDS$1.has(field)) return void 0;
21092
+ if (access.target.node_type !== "value") return void 0;
21093
+ const base = access.target.value;
21094
+ if (base !== param_name && !status.values.some((v) => v.name === base)) return void 0;
21095
+ return {
21096
+ base,
21097
+ field
21098
+ };
21099
+ };
21100
+ const lhs = extract(op.left_value);
21101
+ const rhs = extract(op.right_value);
21102
+ if (!lhs || !rhs || lhs.field !== rhs.field) return constraint;
21103
+ if (lhs.base === rhs.base) return constraint;
21104
+ if (!status.equal_lengths) status.equal_lengths = [];
21105
+ const other = lhs.base;
21106
+ const paired = rhs.base;
21107
+ if (!status.equal_lengths.some((e) => e.field === lhs.field && (e.a === other && e.b === paired || e.a === paired && e.b === other))) status.equal_lengths.push({
21108
+ a: other,
21109
+ b: paired,
21110
+ field: lhs.field
21111
+ });
20987
21112
  }
20988
21113
  /**
20989
21114
  * Extract flow-sensitive bounds from a comparison condition.
@@ -21079,32 +21204,59 @@ function apply_bounds(condition, status, is_loop = false) {
21079
21204
  var_decl = status.values.findLast((v) => v.name === base);
21080
21205
  }
21081
21206
  if (!var_decl) return;
21207
+ let path_entry;
21208
+ if (bound.var_name.includes(".") && bound.var_name !== var_decl.name) {
21209
+ if (!var_decl.path_bounds) var_decl.path_bounds = /* @__PURE__ */ new Map();
21210
+ path_entry = var_decl.path_bounds.get(bound.var_name);
21211
+ if (!path_entry) {
21212
+ path_entry = {};
21213
+ var_decl.path_bounds.set(bound.var_name, path_entry);
21214
+ }
21215
+ }
21216
+ const push_unique = (arr, expr) => {
21217
+ const out = arr ?? [];
21218
+ if (!out.includes(expr)) out.push(expr);
21219
+ return out;
21220
+ };
21082
21221
  if (bound.op === "<" || bound.op === "<=") {
21083
21222
  const inclusive = bound.op === "<=";
21084
21223
  if (!var_decl.upper_bound_exprs) var_decl.upper_bound_exprs = [];
21085
21224
  if (!var_decl.upper_bound_inclusive_exprs) var_decl.upper_bound_inclusive_exprs = [];
21086
21225
  if (inclusive) {
21087
- if (!var_decl.upper_bound_inclusive_exprs.includes(bound.expr)) var_decl.upper_bound_inclusive_exprs.push(bound.expr);
21088
- } else if (!var_decl.upper_bound_exprs.includes(bound.expr)) var_decl.upper_bound_exprs.push(bound.expr);
21226
+ var_decl.upper_bound_inclusive_exprs = push_unique(var_decl.upper_bound_inclusive_exprs, bound.expr);
21227
+ if (path_entry) path_entry.upper_inclusive = push_unique(path_entry.upper_inclusive, bound.expr);
21228
+ } else {
21229
+ var_decl.upper_bound_exprs = push_unique(var_decl.upper_bound_exprs, bound.expr);
21230
+ if (path_entry) path_entry.upper = push_unique(path_entry.upper, bound.expr);
21231
+ }
21089
21232
  var_decl.upper_bound_expr = bound.expr;
21090
21233
  const num = numeric_interval(string_to_node(bound.expr), status);
21091
21234
  if (num) {
21092
21235
  const hi = bound.op === "<=" ? num.upper : num.lower;
21093
21236
  if (is_loop) var_decl.range_upper = hi;
21094
21237
  else if (var_decl.range_upper === void 0 || hi < var_decl.range_upper) var_decl.range_upper = hi;
21238
+ if (path_entry) {
21239
+ if (is_loop) path_entry.range_upper = hi;
21240
+ else if (path_entry.range_upper === void 0 || hi < path_entry.range_upper) path_entry.range_upper = hi;
21241
+ }
21095
21242
  }
21096
21243
  } else if (bound.op === ">" || bound.op === ">=") {
21097
21244
  const inclusive = bound.op === ">=";
21098
21245
  if (!var_decl.lower_bound_exprs) var_decl.lower_bound_exprs = [];
21099
21246
  if (!var_decl.lower_bound_inclusive_exprs) var_decl.lower_bound_inclusive_exprs = [];
21100
21247
  if (inclusive) {
21101
- if (!var_decl.lower_bound_inclusive_exprs.includes(bound.expr)) var_decl.lower_bound_inclusive_exprs.push(bound.expr);
21102
- } else if (!var_decl.lower_bound_exprs.includes(bound.expr)) var_decl.lower_bound_exprs.push(bound.expr);
21248
+ var_decl.lower_bound_inclusive_exprs = push_unique(var_decl.lower_bound_inclusive_exprs, bound.expr);
21249
+ if (path_entry) path_entry.lower_inclusive = push_unique(path_entry.lower_inclusive, bound.expr);
21250
+ } else {
21251
+ var_decl.lower_bound_exprs = push_unique(var_decl.lower_bound_exprs, bound.expr);
21252
+ if (path_entry) path_entry.lower = push_unique(path_entry.lower, bound.expr);
21253
+ }
21103
21254
  var_decl.lower_bound_expr = bound.expr;
21104
21255
  const num = numeric_interval(string_to_node(bound.expr), status);
21105
21256
  if (num) {
21106
21257
  const lo = bound.op === ">=" ? num.lower : num.upper;
21107
21258
  if (var_decl.range_lower === void 0 || lo > var_decl.range_lower) var_decl.range_lower = lo;
21259
+ if (path_entry && (path_entry.range_lower === void 0 || lo > path_entry.range_lower)) path_entry.range_lower = lo;
21108
21260
  }
21109
21261
  }
21110
21262
  apply_inverse_numeric_bound(bound, var_decl, status);
@@ -21259,6 +21411,31 @@ function substitute_constraint(node, lhs_name, param_to_arg, visited = /* @__PUR
21259
21411
  }
21260
21412
  return node;
21261
21413
  }
21414
+ /** Extract `return_bounds` from a value node that is a call (or wraps one). */
21415
+ function call_return_bounds(value) {
21416
+ if (!value) return void 0;
21417
+ if (value.node_type === "func_call" || value.node_type === "access_func") return value.return_bounds;
21418
+ if (value.node_type === "access" && value.access.node_type === "access_func") return value.access.return_bounds;
21419
+ }
21420
+ /**
21421
+ * Transfer a call's return-contract bounds onto the variable the call result
21422
+ * is bound to (`const int m = mid(xs)` / `m = mid(xs)`). Without this, only
21423
+ * the nested form `xs.at(mid(xs))` carried the contract (via the call node's
21424
+ * `return_bounds` decoration); binding to a variable dropped it and forced
21425
+ * unreadable nesting. Uses the same funneled semantics as the nested path
21426
+ * (collect_return_bounds: `<=` lands in the strict `upper` array), so both
21427
+ * forms verify identically.
21428
+ */
21429
+ function apply_return_bounds_to_var(name, rb, status) {
21430
+ if (!rb) return;
21431
+ const decl = status.values.findLast((v) => v.name === name);
21432
+ if (!decl) return;
21433
+ const push = (arr, add) => arr ? (arr.push(...add.filter((e) => !arr.includes(e))), arr) : add.slice();
21434
+ if (rb.upper.length) decl.upper_bound_exprs = push(decl.upper_bound_exprs, rb.upper);
21435
+ if (rb.lower.length) decl.lower_bound_exprs = push(decl.lower_bound_exprs, rb.lower);
21436
+ if (rb.upper_inclusive.length) decl.upper_bound_inclusive_exprs = push(decl.upper_bound_inclusive_exprs, rb.upper_inclusive);
21437
+ if (rb.lower_inclusive.length) decl.lower_bound_inclusive_exprs = push(decl.lower_bound_inclusive_exprs, rb.lower_inclusive);
21438
+ }
21262
21439
  /**
21263
21440
  * Build an expression node tree from a dotted path string (e.g. "list",
21264
21441
  * "self.items") for use in return-contract substitution. Used to map `self`
@@ -22474,6 +22651,18 @@ function check_function_call(node, status, func, target_type, self_value, self_p
22474
22651
  if (access.access.node_type === "access_field") field_name = access.access.name;
22475
22652
  else if (access.access.node_type === "access_func") field_name = access.access.name;
22476
22653
  if (field_name && NON_NEGATIVE_FIELDS.has(field_name)) alias_of = expr_to_string(param, status);
22654
+ const access_path = expr_to_string(param, status);
22655
+ if (access_path && access_path.includes(".")) {
22656
+ const path_bounds = status.values.findLast((v) => v.name === access_path.split(".")[0])?.path_bounds?.get(access_path);
22657
+ if (path_bounds) {
22658
+ upper_bound_exprs = path_bounds.upper?.slice();
22659
+ lower_bound_exprs = path_bounds.lower?.slice();
22660
+ upper_bound_inclusive_exprs = path_bounds.upper_inclusive?.slice();
22661
+ lower_bound_inclusive_exprs = path_bounds.lower_inclusive?.slice();
22662
+ range_lower = path_bounds.range_lower;
22663
+ range_upper = path_bounds.range_upper;
22664
+ }
22665
+ }
22477
22666
  }
22478
22667
  let nested_rb;
22479
22668
  if (param.node_type === "func_call" || param.node_type === "access_func") nested_rb = param.return_bounds;
@@ -22692,6 +22881,222 @@ function receiver_is_const(target_type, self_value, status) {
22692
22881
  return !!binding && binding.declaration === "const" && !binding.type.is_ref;
22693
22882
  }
22694
22883
  //#endregion
22884
+ //#region ../src/build_common/fold_asm_constants.ts
22885
+ /**
22886
+ * A tiny constant-propagation peephole over monomorphized raw aarch64
22887
+ * assembly (`#arch: aarch64` blocks after T → concrete-type substitution).
22888
+ *
22889
+ * Raw library code is written generically: `mov x3, #T_SIZE` followed by a
22890
+ * runtime width dispatch (`cmp x3, #8 / b.gt … / cmp x3, #1 / b.eq …`).
22891
+ * After substitution T_SIZE is a literal, so every dispatched branch has a
22892
+ * compile-time outcome — but as TEXT, so nothing folds it. This pass tracks
22893
+ * `mov <reg>, #<imm>` definitions per basic block and:
22894
+ *
22895
+ * - folds `cmp <reg>, #<imm>` + following `b.<cond> <label>` when the
22896
+ * comparison's outcome is known: never-taken branches are dropped,
22897
+ * always-taken ones become unconditional `b <label>`,
22898
+ * - rewrites `madd <d>, <n>, <m>, xzr` / `mul <d>, <n>, <m>` as
22899
+ * `lsl <d>, <n>, #<log2>` when `<m>` is a known power-of-two constant
22900
+ * (the element-stride multiply in every load_T/store_T body).
22901
+ *
22902
+ * Soundness: a register's definition is only tracked from its immediate
22903
+ * `mov` to its next write; the map is cleared at labels (jump targets may
22904
+ * arrive with different values), at unconditional branches, at calls
22905
+ * (`bl`/`blr` clobber the caller-saved set), and at any instruction form we
22906
+ * do not model. Dead code left behind dropped branches (e.g. the
22907
+ * width-matched tails and the memcpy copy path) stays in place — it is
22908
+ * unreachable, and removing it would require a full reachability pass.
22909
+ * Comments (`// …`) are ignored.
22910
+ */
22911
+ const REG = /^[wx][0-9]+$/;
22912
+ const COND_OPS = /* @__PURE__ */ new Set([
22913
+ "eq",
22914
+ "ne",
22915
+ "gt",
22916
+ "ge",
22917
+ "lt",
22918
+ "le",
22919
+ "hi",
22920
+ "hs",
22921
+ "lo",
22922
+ "ls",
22923
+ "al"
22924
+ ]);
22925
+ function split_comment(line) {
22926
+ return line.replace(/\/\/.*$/, "");
22927
+ }
22928
+ /** Does `a <cond> b` hold? (signed semantics; immediates here are small.) */
22929
+ function cond_holds(cond, a, b) {
22930
+ switch (cond) {
22931
+ case "eq": return a === b;
22932
+ case "ne": return a !== b;
22933
+ case "gt": return a > b;
22934
+ case "ge": return a >= b;
22935
+ case "lt": return a < b;
22936
+ case "le": return a <= b;
22937
+ case "hi": return a > b;
22938
+ case "hs": return a >= b;
22939
+ case "lo": return a < b;
22940
+ case "ls": return a <= b;
22941
+ case "al": return true;
22942
+ default: return false;
22943
+ }
22944
+ }
22945
+ function is_pow2(n) {
22946
+ if (n > 0 && (n & n - 1) === 0) return Math.log2(n);
22947
+ }
22948
+ function fold_asm_constants(asm) {
22949
+ const lines = asm.split("\n");
22950
+ const out = [];
22951
+ const defs = /* @__PURE__ */ new Map();
22952
+ const clear = () => defs.clear();
22953
+ /** Invalidate the destination register of a general instruction. */
22954
+ const invalidate_dest = (ops) => {
22955
+ const first = ops.split(",")[0]?.trim();
22956
+ if (first && REG.test(first)) defs.delete(first);
22957
+ };
22958
+ let i = 0;
22959
+ while (i < lines.length) {
22960
+ const raw_line = lines[i];
22961
+ const line = split_comment(raw_line).trim();
22962
+ if (!line) {
22963
+ out.push(raw_line);
22964
+ i++;
22965
+ continue;
22966
+ }
22967
+ if (/^\.[\w$]+:$/.test(line) || line.startsWith(".")) {
22968
+ clear();
22969
+ out.push(raw_line);
22970
+ i++;
22971
+ continue;
22972
+ }
22973
+ const m = /^(\w+)\s*(.*)$/.exec(line);
22974
+ if (!m) {
22975
+ clear();
22976
+ out.push(raw_line);
22977
+ i++;
22978
+ continue;
22979
+ }
22980
+ const mn = m[1];
22981
+ const ops = m[2].trim();
22982
+ if (mn === "bl" || mn === "blr" || mn === "b" || mn === "br" || mn === "ret") {
22983
+ clear();
22984
+ out.push(raw_line);
22985
+ i++;
22986
+ continue;
22987
+ }
22988
+ if (mn === "mov") {
22989
+ const mm = /^([wx][0-9]+)\s*,\s*#(-?\d+)$/.exec(ops);
22990
+ if (mm) {
22991
+ defs.set(mm[1], parseInt(mm[2], 10));
22992
+ out.push(raw_line);
22993
+ i++;
22994
+ continue;
22995
+ }
22996
+ invalidate_dest(ops);
22997
+ out.push(raw_line);
22998
+ i++;
22999
+ continue;
23000
+ }
23001
+ if (mn === "cmp") {
23002
+ const cm = /^([wx][0-9]+)\s*,\s*#(-?\d+)$/.exec(ops);
23003
+ if (cm && defs.has(cm[1])) {
23004
+ const a = defs.get(cm[1]);
23005
+ const b = parseInt(cm[2], 10);
23006
+ const run = [];
23007
+ let j = i + 1;
23008
+ while (j < lines.length) {
23009
+ const t = split_comment(lines[j]).trim();
23010
+ if (!t) {
23011
+ j++;
23012
+ continue;
23013
+ }
23014
+ const bm = /^b\.(\w+)\s+(\S+)$/.exec(t);
23015
+ if (bm && COND_OPS.has(bm[1])) {
23016
+ run.push({
23017
+ cond: bm[1],
23018
+ label: bm[2],
23019
+ line_idx: j
23020
+ });
23021
+ j++;
23022
+ continue;
23023
+ }
23024
+ break;
23025
+ }
23026
+ if (run.length > 0) {
23027
+ let follower = "";
23028
+ for (let k = run[run.length - 1].line_idx + 1; k < lines.length; k++) {
23029
+ const t = split_comment(lines[k]).trim();
23030
+ if (!t) continue;
23031
+ follower = t;
23032
+ break;
23033
+ }
23034
+ const follower_mn = /^(\S+)/.exec(follower)?.[1] ?? "";
23035
+ if (follower === "" || follower.startsWith(".") || [
23036
+ "cmp",
23037
+ "tst",
23038
+ "cmn",
23039
+ "b",
23040
+ "bl",
23041
+ "ret",
23042
+ "cbz",
23043
+ "cbnz"
23044
+ ].includes(follower_mn)) {
23045
+ for (const r of run) if (cond_holds(r.cond, a, b)) {
23046
+ out.push(`b ${r.label}`);
23047
+ break;
23048
+ }
23049
+ i = run[run.length - 1].line_idx + 1;
23050
+ continue;
23051
+ }
23052
+ }
23053
+ }
23054
+ out.push(raw_line);
23055
+ i++;
23056
+ continue;
23057
+ }
23058
+ if (mn === "madd" || mn === "mul") {
23059
+ const parts = ops.split(",").map((p) => p.trim());
23060
+ if (parts.length === (mn === "madd" ? 4 : 3)) {
23061
+ const dest = parts[0];
23062
+ const srcs = [parts[1], parts[2]];
23063
+ const addend = mn === "madd" ? parts[3] : void 0;
23064
+ const addend_zero = mn === "madd" ? /^(x|w)zr$/.test(addend ?? "") : true;
23065
+ let folded = false;
23066
+ if (addend_zero && REG.test(dest)) for (const [n_reg, m_reg] of [[srcs[0], srcs[1]], [srcs[1], srcs[0]]]) {
23067
+ const c = m_reg !== void 0 && REG.test(m_reg) ? defs.get(m_reg) : void 0;
23068
+ const shift = c !== void 0 ? is_pow2(c) : void 0;
23069
+ if (shift !== void 0 && REG.test(n_reg) && !defs.has(n_reg)) {
23070
+ out.push(`lsl ${dest}, ${n_reg}, #${shift}`);
23071
+ defs.delete(dest);
23072
+ i++;
23073
+ folded = true;
23074
+ break;
23075
+ }
23076
+ }
23077
+ if (folded) continue;
23078
+ }
23079
+ invalidate_dest(ops);
23080
+ out.push(raw_line);
23081
+ i++;
23082
+ continue;
23083
+ }
23084
+ if (![
23085
+ "str",
23086
+ "stp",
23087
+ "cmp",
23088
+ "tst",
23089
+ "cbz",
23090
+ "cbnz",
23091
+ "tbz",
23092
+ "tbnz"
23093
+ ].includes(mn)) invalidate_dest(ops);
23094
+ out.push(raw_line);
23095
+ i++;
23096
+ }
23097
+ return out.join("\n");
23098
+ }
23099
+ //#endregion
22695
23100
  //#region ../src/nodes/ExtendNode.ts
22696
23101
  /**
22697
23102
  * An `extend struct Name { ... }` / `extend class Name { ... }` declaration.
@@ -23089,6 +23494,7 @@ function check_declaration_node(decl, status) {
23089
23494
  status.pending_return_bounds.delete(decl.name);
23090
23495
  }
23091
23496
  }
23497
+ if (decl.value) apply_return_bounds_to_var(decl.name, call_return_bounds(decl.value), status);
23092
23498
  }
23093
23499
  }
23094
23500
  /**
@@ -23822,6 +24228,11 @@ function check_function_parameter_node(param, status) {
23822
24228
  is_null: param.type.is_nullable ? true : void 0
23823
24229
  });
23824
24230
  if (param.constraint) {
24231
+ const stripped = strip_length_equalities(param.constraint, param.name, status);
24232
+ if (stripped !== param.constraint) {
24233
+ param.constraint = stripped;
24234
+ if (!param.constraint) return;
24235
+ }
23825
24236
  check_node(param.constraint, status);
23826
24237
  const constraint_type = type_from_value_node(param.constraint, status);
23827
24238
  if (constraint_type.name && constraint_type.name !== "bool") add_error(status, `Constraint must be a boolean expression, got ${constraint_type.name}`, param.constraint.start);
@@ -23844,7 +24255,15 @@ function clone_status(status) {
23844
24255
  upper_bound_exprs: v.upper_bound_exprs?.slice(),
23845
24256
  lower_bound_exprs: v.lower_bound_exprs?.slice(),
23846
24257
  upper_bound_inclusive_exprs: v.upper_bound_inclusive_exprs?.slice(),
23847
- lower_bound_inclusive_exprs: v.lower_bound_inclusive_exprs?.slice()
24258
+ lower_bound_inclusive_exprs: v.lower_bound_inclusive_exprs?.slice(),
24259
+ path_bounds: v.path_bounds ? new Map([...v.path_bounds].map(([k, b]) => [k, {
24260
+ upper: b.upper?.slice(),
24261
+ lower: b.lower?.slice(),
24262
+ upper_inclusive: b.upper_inclusive?.slice(),
24263
+ lower_inclusive: b.lower_inclusive?.slice(),
24264
+ range_lower: b.range_lower,
24265
+ range_upper: b.range_upper
24266
+ }])) : void 0
23848
24267
  })),
23849
24268
  function_value_base: status.function_value_base,
23850
24269
  structs: status.structs.slice(),
@@ -23857,6 +24276,7 @@ function clone_status(status) {
23857
24276
  type_params: status.type_params,
23858
24277
  errors: status.errors,
23859
24278
  buffer_caps: status.buffer_caps,
24279
+ equal_lengths: status.equal_lengths?.slice(),
23860
24280
  mutated_local_names: status.mutated_local_names
23861
24281
  };
23862
24282
  }
@@ -23875,6 +24295,7 @@ function check_function_node(func, status) {
23875
24295
  }
23876
24296
  let function_status = clone_status(status);
23877
24297
  function_status.function_value_base = function_status.values.length;
24298
+ function_status.equal_lengths = [];
23878
24299
  const structs_before = function_status.structs.length;
23879
24300
  const enums_before = function_status.enums.length;
23880
24301
  const types_before = function_status.types.length;
@@ -24812,6 +25233,7 @@ function substitute_raw_in_node(node, substitution, structs, deref_params = /* @
24812
25233
  value = value.replace(new RegExp(`\\b${param}_NEEDS_STRDUP\\b`, "g"), type === "string" ? "1" : "0");
24813
25234
  }
24814
25235
  for (const pname of deref_params) value = value.replace(new RegExp(`(?<![&*.>\\w])\\b${pname}\\b(?![\\w])`, "g"), `(*${pname})`);
25236
+ if (raw_block_is_pure_asm(value)) value = fold_asm_constants(value);
24815
25237
  raw.value = value;
24816
25238
  return;
24817
25239
  }
@@ -26037,13 +26459,17 @@ function check_assignment_node(assign, status) {
26037
26459
  left_value.lower_bound_exprs = void 0;
26038
26460
  left_value.upper_bound_inclusive_exprs = void 0;
26039
26461
  left_value.lower_bound_inclusive_exprs = void 0;
26462
+ left_value.path_bounds = void 0;
26040
26463
  left_value.alias_of = void 0;
26041
26464
  left_value.class_alias_of = void 0;
26042
26465
  left_value.type.length = void 0;
26043
26466
  if (assign.left_value.node_type === "value") invalidate_view_borrows_of(status, left_value.name);
26044
26467
  let synthetic_rhs;
26045
26468
  if (is_trackable_compound) synthetic_rhs = new OperationNode(assign.right_value.start, compound_op, assign.left_value, assign.right_value, left_value.type);
26046
- if ((!is_compound || synthetic_rhs) && left_value_name === left_value.name) track_assignment_bounds(left_value.name, synthetic_rhs ?? assign.right_value, status, self_snapshot);
26469
+ if ((!is_compound || synthetic_rhs) && left_value_name === left_value.name) {
26470
+ track_assignment_bounds(left_value.name, synthetic_rhs ?? assign.right_value, status, self_snapshot);
26471
+ if (!is_compound) apply_return_bounds_to_var(left_value.name, call_return_bounds(assign.right_value), status);
26472
+ }
26047
26473
  if (!is_compound && left_value.type.name === "string" && assign.right_value.node_type === "value" && assign.right_value.value.startsWith("\"") && assign.right_value.value.endsWith("\"")) {
26048
26474
  const len = assign.right_value.value.length - 2;
26049
26475
  left_value.type.length = new ValueNode(assign.right_value.start, len.toString(), new Type("int"));
@@ -26701,7 +27127,11 @@ function check_operation_node(op, status) {
26701
27127
  var_obj.is_null = false;
26702
27128
  }
26703
27129
  }
26704
- if (!check_node(op.right_value, status)) return false;
27130
+ const saved_bounds = snapshot_all_bounds(status);
27131
+ apply_bounds(op.left_value, status);
27132
+ const right_ok = check_node(op.right_value, status);
27133
+ restore_all_bounds(status, saved_bounds);
27134
+ if (!right_ok) return false;
26705
27135
  if (saved_null !== void 0 && left_check) {
26706
27136
  const var_obj = status.values.findLast((v) => v.name === left_check.name);
26707
27137
  if (var_obj) var_obj.is_null = saved_null;
@@ -26709,6 +27139,16 @@ function check_operation_node(op, status) {
26709
27139
  op.type = new Type("bool");
26710
27140
  return true;
26711
27141
  }
27142
+ if (op.op === "||") {
27143
+ if (!check_node(op.left_value, status)) return false;
27144
+ const saved_bounds = snapshot_all_bounds(status);
27145
+ apply_negated_bounds(op.left_value, status);
27146
+ const right_ok = check_node(op.right_value, status);
27147
+ restore_all_bounds(status, saved_bounds);
27148
+ if (!right_ok) return false;
27149
+ op.type = new Type("bool");
27150
+ return true;
27151
+ }
26712
27152
  const is_equality = op.op === "==" || op.op === "!=";
26713
27153
  const is_null_coalesce = op.op === "??";
26714
27154
  const old_allow_null = status.allow_null_value;
@@ -26786,7 +27226,6 @@ function check_operation_node(op, status) {
26786
27226
  case ">=":
26787
27227
  case "<":
26788
27228
  case "<=":
26789
- case "||":
26790
27229
  op.type = new Type("bool");
26791
27230
  break;
26792
27231
  case "??": {
@@ -29168,7 +29607,8 @@ function parse_visibility(visibility, status) {
29168
29607
  break;
29169
29608
  case "inline":
29170
29609
  consume(status);
29171
- if (peek_next(status) === "func") parse_function(visibility, status, void 0, true);
29610
+ if (peek_current(status) === "inline") consume(status);
29611
+ if (peek_current(status) === "func") parse_function(visibility, status, void 0, true);
29172
29612
  else add_error(status, "Expected func after inline", get_index(status));
29173
29613
  break;
29174
29614
  case "#": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nomen-lang",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "The CLI for the Nomen programming language.",
5
5
  "keywords": [],
6
6
  "license": "ISC",