nomen-lang 0.8.0 → 0.10.0

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.
Files changed (90) hide show
  1. package/audit_runtime.c +12 -2
  2. package/core/System/Arena.nm +14 -5
  3. package/core/System/Array.nm +1 -1
  4. package/core/System/Atomic.nm +3 -3
  5. package/core/System/BigInt.nm +182 -23
  6. package/core/System/Buffer.nm +392 -33
  7. package/core/System/ClassBuffer.nm +145 -13
  8. package/core/System/Controls/Alert.nm +3 -3
  9. package/core/System/Controls/App.nm +17 -17
  10. package/core/System/Controls/Button.nm +2 -2
  11. package/core/System/Controls/CheckBox.nm +3 -3
  12. package/core/System/Controls/ColorPicker.nm +2 -2
  13. package/core/System/Controls/ComboBox.nm +2 -2
  14. package/core/System/Controls/Container.nm +39 -33
  15. package/core/System/Controls/DatePicker.nm +1 -1
  16. package/core/System/Controls/Form.nm +2 -2
  17. package/core/System/Controls/GroupBox.nm +2 -2
  18. package/core/System/Controls/ImageView.nm +2 -2
  19. package/core/System/Controls/Layout.nm +5 -5
  20. package/core/System/Controls/LevelIndicator.nm +2 -2
  21. package/core/System/Controls/ListView.nm +2 -2
  22. package/core/System/Controls/Menu.nm +2 -2
  23. package/core/System/Controls/NumberInput.nm +3 -3
  24. package/core/System/Controls/OtpInput.nm +2 -2
  25. package/core/System/Controls/OutlineView.nm +2 -2
  26. package/core/System/Controls/Pagination.nm +14 -14
  27. package/core/System/Controls/Popover.nm +2 -2
  28. package/core/System/Controls/Progress.nm +2 -2
  29. package/core/System/Controls/Radio.nm +3 -3
  30. package/core/System/Controls/Scrollable.nm +1 -1
  31. package/core/System/Controls/SearchField.nm +2 -2
  32. package/core/System/Controls/SecureField.nm +2 -2
  33. package/core/System/Controls/Segmented.nm +2 -2
  34. package/core/System/Controls/Select.nm +2 -2
  35. package/core/System/Controls/Separator.nm +2 -2
  36. package/core/System/Controls/Sheet.nm +2 -2
  37. package/core/System/Controls/Slider.nm +2 -2
  38. package/core/System/Controls/Spinner.nm +5 -5
  39. package/core/System/Controls/SplitView.nm +3 -3
  40. package/core/System/Controls/Stepper.nm +2 -2
  41. package/core/System/Controls/Switch.nm +2 -2
  42. package/core/System/Controls/Table.nm +2 -2
  43. package/core/System/Controls/Tabs.nm +2 -2
  44. package/core/System/Controls/Text.nm +2 -2
  45. package/core/System/Controls/TextBox.nm +3 -3
  46. package/core/System/Controls/TextView.nm +2 -2
  47. package/core/System/Controls/Toggle.nm +3 -3
  48. package/core/System/Controls/Toolbar.nm +2 -2
  49. package/core/System/Controls/Window.nm +12 -5
  50. package/core/System/Fiber.nm +16 -3
  51. package/core/System/Graph.nm +2 -2
  52. package/core/System/LinkedList.nm +13 -4
  53. package/core/System/List.nm +76 -17
  54. package/core/System/Map.nm +179 -21
  55. package/core/System/Mutex.nm +4 -4
  56. package/core/System/Random.nm +6 -7
  57. package/core/System/Set.nm +99 -23
  58. package/core/System/StableList.nm +371 -0
  59. package/core/System/Stream/Directory.nm +2 -2
  60. package/core/System/Stream/File.nm +18 -18
  61. package/core/System/Stream/Http.nm +5 -5
  62. package/core/System/Stream/Tcp.nm +5 -5
  63. package/core/System/String.nm +10 -5
  64. package/core/System/StringBuilder.nm +223 -12
  65. package/core/System/Task.nm +56 -64
  66. package/core/System/Test.nm +18 -18
  67. package/core/System/Text/CharIndex.nm +9 -9
  68. package/core/System/Text/Chars.nm +1 -1
  69. package/core/System/Text/Json.nm +72 -58
  70. package/core/System/Text/JsonTree.nm +126 -98
  71. package/core/System/Text/Regex.nm +43 -38
  72. package/core/System/Text/Utf8.nm +5 -5
  73. package/core/System/Thread.nm +26 -3
  74. package/core/System/Tree.nm +19 -3
  75. package/core/System/float.nm +36 -22
  76. package/core/System/int.nm +80 -28
  77. package/core/System/int16.nm +11 -0
  78. package/core/System/int32.nm +11 -0
  79. package/core/System/int64.nm +11 -0
  80. package/core/System/int8.nm +11 -0
  81. package/core/System/uint.nm +11 -0
  82. package/core/System/uint16.nm +11 -0
  83. package/core/System/uint32.nm +11 -0
  84. package/core/System/uint64.nm +11 -0
  85. package/core/System/uint8.nm +11 -0
  86. package/core/docs/System/StringBuilder.md +6 -0
  87. package/core/docs/System/Text/JsonTree.md +3 -2
  88. package/dist/index.mjs +37622 -30603
  89. package/package.json +1 -1
  90. package/src/bench_loop.nm +9 -9
@@ -1,156 +1,184 @@
1
- // JsonTree: a flat pool of JSON tree nodes backed by a Buffer<JsonNode> slab.
2
- // Each node is a value struct stored at index N in the buffer; grow_T/store_T/
3
- // load_T handle the slab arithmetic. The caller creates a JsonTree once and
4
- // calls reset() per document to reuse the slab without reallocation.
1
+ // JsonTree: a flat pool of JSON tree nodes backed by a StableList<JsonNode>.
2
+ // Each node is a value struct appended to the list; node indices are list
3
+ // indices, and list slots never move. The caller creates a JsonTree once and
4
+ // calls reset() per document. The pool owns its node texts end to end —
5
+ // set_text/take_text's interior field write frees the displaced text and the
6
+ // list's own destroy reclaims every node — so there is no manual text
7
+ // lifecycle. (The pre-StableList version kept a raw Buffer slab with a
8
+ // hand-encoded element layout and a strdup/free dance; see the stable grow-only
9
+ // arrays".)
5
10
  //
6
- // set_text is the one exception to the load/store pattern: it calls strdup
7
- // directly into the slab via raw code, because routing the strdup'd pointer
8
- // through an Nomen local would subject it to auto-free at scope exit, leaving
9
- // the slab's text pointer dangling.
11
+ // Accessors read and write through the slot alias (`ref JsonNode n =
12
+ // self.nodes.at(idx)`), the checked interior-mutation form: the alias binds
13
+ // the slot itself, so field writes land in the list and reads copy nothing.
10
14
 
11
15
  // A node in a parsed JSON tree, stored by value in the pool.
12
16
  // kind: 0=null, 1=true, 2=number, 3=string, 4=array, 5=object, 6=false
13
17
  // child/next/val are node indices, -1 = none.
18
+ //
19
+ // `text` is nullable and seeds as `null` (the in-band zero pair): the auto
20
+ // constructor stores no pointer for it, so a fresh node costs NO allocation
21
+ // — every parsed node used to pay a strdup-of-"" seed plus the free when
22
+ // take_text displaced it (or at destroy for kinds that never carry text).
23
+ // set_text/take_text install the owned value; until then the field holds
24
+ // null and every read (get_text/get_text_view) answers the empty string.
14
25
  mv struct JsonNode {
15
26
  var kind = 0
16
- var text = ""
27
+ var string? text = null
17
28
  var int child = -1
18
29
  var int next = -1
19
30
  var int val = -1
20
31
  }
21
32
 
22
33
  /**
23
- * A flat, buffer-backed pool of JSON tree nodes — the parsed representation `Json.parse` produces
34
+ * A flat, pool-backed set of JSON tree nodes — the parsed representation `Json.parse` produces
24
35
  **/
25
36
  pub mv struct JsonTree {
26
- var nodes = Buffer<JsonNode>()
27
- var count = 0
28
-
29
- func alloc_node = (ref self, out int) {
30
- var int idx = self.count
31
- self.nodes.grow(idx + 1)
32
- self.count = idx + 1
33
- var default = JsonNode()
34
- self.nodes.store(idx, default)
35
- // store_T deep-copies the mv `text` field (strdup of the shared
36
- // empty-string literal) into the slot. JsonTree manages node texts
37
- // manually (set_text strdup's into the slab; free_text/release frees
38
- // them; NULL means "no text"), so the slot must start NULL — free the
39
- // strdup'd default and NULL it, or reset() would leak it. The slab
40
- // slot layout: 56-byte JsonNode elements — 7 words — with the text
41
- // pair at words 2 and 3 of each element.
42
- unsafe {
43
- var ptr uint64 slots = self.nodes.data as ptr uint64
44
- var int t = idx * 7 + 2
45
- var uint64 old = slots[t]
46
- if old != 0 {
47
- free(old)
48
- }
49
- slots[t] = 0
50
- slots[t + 1] = 0
51
- }
52
- return idx
53
- }
37
+ var nodes = StableList<JsonNode>()
54
38
 
55
- // Free a node's strdup'd text pointer (set_text writes a heap pointer via
56
- // raw strdup; this mirrors it). idx is x1, following set_text's layout.
57
- func free_text = (ref self, int idx) {
58
- unsafe {
59
- var ptr uint64 slots = self.nodes.data as ptr uint64
60
- var int t = idx * 7 + 2
61
- var uint64 p = slots[t]
62
- if p != 0 {
63
- free(p)
64
- slots[t] = 0
65
- slots[t + 1] = 0
66
- }
67
- }
39
+ // Append a fresh node and return its index.
40
+ func alloc_node = (ref self { nodes }, out int) {
41
+ self.nodes.push(JsonNode())
42
+ return self.nodes.length - 1
68
43
  }
69
44
 
70
- // Free every strdup'd text pointer before reusing the slab, otherwise
71
- // re-parsing a document leaks the previous document's string/number texts.
72
- func reset = (ref self) {
73
- var i = 0
74
- while i < self.count; i += 1 {
75
- self.free_text(i)
76
- }
77
- self.count = 0
45
+ // Drop the current document: the fresh StableList displaces the old one,
46
+ // whose destroy frees every node (texts included) and its slabs. The
47
+ // next parse starts from a clean pool.
48
+ func reset = (ref self { nodes }) {
49
+ self.nodes = StableList<JsonNode>()
78
50
  }
79
51
 
80
- // Runs when the tree instance is destroyed (before its `nodes` buffer field
81
- // is freed): release the current document's strdup'd node texts, which live
82
- // raw in the slab and are otherwise invisible to the auto-destroy pass.
83
- func #destroy = () {
84
- var i = 0
85
- while i < self.count; i += 1 {
86
- self.free_text(i)
52
+ // Install node `idx`'s text, TAKING ownership of the argument (the D3
53
+ // mv convention): `set_text(n, mv s)` transfers the caller's buffer
54
+ // into the pool with no copy anywhere — the field store moves the pair
55
+ // raw and the caller's variable is invalidated. (Pass `.copy()` for the
56
+ // keep-the-original spelling: `set_text(n, s.copy())`.) The pool owns
57
+ // its node texts end to end, so the transferred buffer is freed exactly
58
+ // once, by the list's destroy or the next text store. For strings
59
+ // produced by a StringBuilder, `take_text` installs the builder's
60
+ // buffer with no copy at all.
61
+ func set_text = (ref self {}, int idx, mv string text) {
62
+ if idx < 0 || idx >= self.nodes.length {
63
+ panic("index out of range")
87
64
  }
88
- self.count = 0
65
+ ref JsonNode n = self.nodes.at(idx)
66
+ n.text = mv text
89
67
  }
90
68
 
91
- // strdup directly into the slab — see file header for why this can't
92
- // go through load_T/store_T. (Under the fat-string ABI the C body is
93
- // emitted behind the _raw_ adapter, so `text` arrives as a thin char*;
94
- // aarch64 receives the pair x2 = ptr, x3 = len.)
95
- func set_text = (ref self, int idx, string text) {
96
- unsafe {
97
- var int len = text.length
98
- var uint64 buf = malloc(len + 1)
99
- memcpy(buf, (text as ptr char) as uint64, len)
100
- memset(buf + len, 0, 1)
101
- var ptr uint64 slots = self.nodes.data as ptr uint64
102
- var int t = idx * 7 + 2
103
- slots[t] = buf
104
- slots[t + 1] = len
69
+ // Install node `idx`'s text by TAKING the builder's buffer — the parse
70
+ // path's zero-copy store. `to_string` is a take (the builder hands its
71
+ // heap buffer over and resets), and a fresh-call RHS stores into the
72
+ // field directly, so per parsed string/key the only allocation in the
73
+ // whole node lifecycle is the builder's own buffer — the node's null
74
+ // seed costs nothing and there is no copy malloc, no seed free, no
75
+ // temp free. (A `mv string` parameter cannot do this: the string-arg
76
+ // ownership convention makes mv string arguments caller-owned
77
+ // borrows, forcing the callee-side copy this method exists to skip.)
78
+ func take_text = (ref self {}, int idx, ref StringBuilder sb {}) {
79
+ if idx < 0 || idx >= self.nodes.length {
80
+ panic("index out of range")
105
81
  }
82
+ ref JsonNode n = self.nodes.at(idx)
83
+ n.text = sb.to_string()
106
84
  }
107
85
 
108
- func set_kind = (ref self, int idx, int val) {
109
- var JsonNode n = self.nodes.load(idx)
86
+ func set_kind = (ref self {}, int idx, int val) {
87
+ if idx < 0 || idx >= self.nodes.length {
88
+ panic("index out of range")
89
+ }
90
+ ref JsonNode n = self.nodes.at(idx)
110
91
  n.kind = val
111
- self.nodes.store(idx, n)
112
92
  }
113
93
 
114
- func set_child = (ref self, int idx, int val) {
115
- var JsonNode n = self.nodes.load(idx)
94
+ func set_child = (ref self {}, int idx, int val) {
95
+ if idx < 0 || idx >= self.nodes.length {
96
+ panic("index out of range")
97
+ }
98
+ ref JsonNode n = self.nodes.at(idx)
116
99
  n.child = val
117
- self.nodes.store(idx, n)
118
100
  }
119
101
 
120
- func set_next = (ref self, int idx, int val) {
121
- var JsonNode n = self.nodes.load(idx)
102
+ func set_next = (ref self {}, int idx, int val) {
103
+ if idx < 0 || idx >= self.nodes.length {
104
+ panic("index out of range")
105
+ }
106
+ ref JsonNode n = self.nodes.at(idx)
122
107
  n.next = val
123
- self.nodes.store(idx, n)
124
108
  }
125
109
 
126
- func set_val = (ref self, int idx, int val) {
127
- var JsonNode n = self.nodes.load(idx)
110
+ func set_val = (ref self {}, int idx, int val) {
111
+ if idx < 0 || idx >= self.nodes.length {
112
+ panic("index out of range")
113
+ }
114
+ ref JsonNode n = self.nodes.at(idx)
128
115
  n.val = val
129
- self.nodes.store(idx, n)
130
116
  }
131
117
 
132
118
  func get_kind = (self, int idx, out int) {
133
- var JsonNode n = self.nodes.load(idx)
119
+ if idx < 0 || idx >= self.nodes.length {
120
+ panic("index out of range")
121
+ }
122
+ ref JsonNode n = self.nodes.at(idx)
134
123
  return n.kind
135
124
  }
136
125
 
137
126
  func get_text = (self, int idx, out string) {
138
- var JsonNode n = self.nodes.load(idx)
127
+ if idx < 0 || idx >= self.nodes.length {
128
+ panic("index out of range")
129
+ }
130
+ ref JsonNode n = self.nodes.at(idx)
131
+ // The aarch64 `??` yields the left pair AS-IS (no dup), so the
132
+ // coalesce spelling would return a borrow of the node's text — the
133
+ // caller's free would take the pool's buffer. Spell the owned copy:
134
+ // a null text answers a fresh "" and a non-null one takes
135
+ // `.copy()` (the return contract — the caller owns the result —
136
+ // is the `.copy()` shape the method always had).
137
+ if n.text == null {
138
+ return ""
139
+ }
139
140
  return n.text.copy()
140
141
  }
141
142
 
143
+ // A non-mv (ptr, len) view over node `idx`'s text — the zero-copy read.
144
+ // The borrow roots at `self` and re-roots at the call-site receiver
145
+ // (the `slice` convention), so it is invalidated by `reset()`/re-parse
146
+ // like any pool borrow. Callers that immediately append, hash, or
147
+ // inspect the text take this instead of paying `get_text`'s per-read
148
+ // copy allocation. A node that never had text installed views the
149
+ // empty string (a rodata-rooted empty borrow).
150
+ func get_text_view = (self, int idx, out view string) {
151
+ if idx < 0 || idx >= self.nodes.length {
152
+ panic("index out of range")
153
+ }
154
+ ref JsonNode n = self.nodes.at(idx)
155
+ if n.text == null {
156
+ return "".slice(0, 0)
157
+ }
158
+ return n.text.slice(0, n.text.length)
159
+ }
160
+
142
161
  func get_child = (self, int idx, out int) {
143
- var JsonNode n = self.nodes.load(idx)
162
+ if idx < 0 || idx >= self.nodes.length {
163
+ panic("index out of range")
164
+ }
165
+ ref JsonNode n = self.nodes.at(idx)
144
166
  return n.child
145
167
  }
146
168
 
147
169
  func get_next = (self, int idx, out int) {
148
- var JsonNode n = self.nodes.load(idx)
170
+ if idx < 0 || idx >= self.nodes.length {
171
+ panic("index out of range")
172
+ }
173
+ ref JsonNode n = self.nodes.at(idx)
149
174
  return n.next
150
175
  }
151
176
 
152
177
  func get_val = (self, int idx, out int) {
153
- var JsonNode n = self.nodes.load(idx)
178
+ if idx < 0 || idx >= self.nodes.length {
179
+ panic("index out of range")
180
+ }
181
+ ref JsonNode n = self.nodes.at(idx)
154
182
  return n.val
155
183
  }
156
184
  }
@@ -33,10 +33,9 @@ pub struct Regex {
33
33
  }
34
34
 
35
35
  // Return the first substring of input matching pattern, or "" if none.
36
- // `mv out string`: the built match (or the copied empty literal) is
37
- // handed to the caller as an owned value.
36
+ // `mv out string`: the matched span (or the empty literal) is handed to
37
+ // the caller as an owned value.
38
38
  pub func match = (string pattern, string input, mv out string) {
39
- var match_result = ""
40
39
  var int pat_len = pattern.length
41
40
  var int len = input.length
42
41
  var string charset = first_byte_set(pattern, pat_len)
@@ -52,14 +51,14 @@ pub struct Regex {
52
51
  var caps = List<int>()
53
52
  var int end = match_here(pattern, 0, pat_len, input, i, len, ref caps)
54
53
  if end >= 0 {
55
- var int j = i
56
- while j < end; j += 1 {
57
- match_result = match_result + "\{input.at(j)}"
58
- }
59
- break
54
+ // One owned substring copy. The old accumulator
55
+ // (`match_result = match_result + "\{input.at(j)}"` per
56
+ // matched byte) paid an interpolation malloc PLUS a
57
+ // whole-accumulator re-copy per byte — O(n²) in the span.
58
+ return input.substring(i, end)
60
59
  }
61
60
  }
62
- return match_result
61
+ return ""
63
62
  }
64
63
 
65
64
  // Try to match pattern anywhere in input. On a match, append the whole
@@ -70,7 +69,7 @@ pub struct Regex {
70
69
  pub func captures = (
71
70
  string pattern,
72
71
  string input,
73
- ref List<string> dst,
72
+ ref List<string> dst { items },
74
73
  out bool
75
74
  ) { var int pat_len = pattern.length
76
75
  var int len = input.length
@@ -118,7 +117,7 @@ pub struct Regex {
118
117
  // first, so a reused RegexMatch carries nothing over. Scans every
119
118
  // position: the first-byte prefilter would skip nullable patterns'
120
119
  // empty matches at skipped positions, reporting a wrong .start.
121
- pub func find = (string pattern, string input, ref RegexMatch dst) {
120
+ pub func find = (string pattern, string input, ref RegexMatch dst { text }) {
122
121
  dst.found = false
123
122
  dst.start = 0
124
123
  dst.end = 0
@@ -173,25 +172,27 @@ pub struct Regex {
173
172
  }
174
173
 
175
174
  // Replace all non-overlapping occurrences of pattern in input with replacement.
175
+ // Literal bytes between matches are copied in bulk runs (one
176
+ // `append_string_view` per run) — the old loop appended per byte.
176
177
  pub func replace_all = (
177
178
  string pattern,
178
179
  string input,
179
180
  string replacement,
180
181
  out string
181
- ) { var sb = StringBuilder()
182
+ ) {
183
+ var sb = StringBuilder()
182
184
  var int pat_len = pattern.length
183
185
  var int len = input.length
184
186
  var string charset = first_byte_set(pattern, pat_len)
185
187
  var int clen = charset.length
186
188
  var pos = 0
189
+ var run_start = 0
187
190
  while pos < len {
188
191
  if clen > 0 {
189
- var int next = find_next_byte(input, pos, len, charset)
190
- // Copy the bytes we're skipping over.
191
- while pos < next {
192
- sb.append_char(input.at(pos))
193
- pos += 1
194
- }
192
+ // Prefilter: jump to the next candidate byte. The skipped
193
+ // run stays pending — it is copied in bulk at the next
194
+ // match, or by the tail copy below.
195
+ pos = find_next_byte(input, pos, len, charset)
195
196
  if pos >= len {
196
197
  break
197
198
  }
@@ -199,18 +200,22 @@ pub struct Regex {
199
200
  var caps = List<int>()
200
201
  var int end = match_here(pattern, 0, pat_len, input, pos, len, ref caps)
201
202
  if end >= 0 {
203
+ sb.append_string_view(input.slice(run_start, pos))
202
204
  sb.append_string(replacement)
203
205
  if end == pos {
204
- sb.append_char(input.at(pos))
206
+ // Empty match: the byte at pos is not consumed — it
207
+ // starts the next literal run.
208
+ run_start = pos
205
209
  pos += 1
206
210
  } else {
207
211
  pos = end
212
+ run_start = pos
208
213
  }
209
214
  } else {
210
- sb.append_char(input.at(pos))
211
215
  pos += 1
212
216
  }
213
217
  }
218
+ sb.append_string_view(input.slice(run_start, len))
214
219
  return sb.to_string()
215
220
  }
216
221
 
@@ -227,13 +232,13 @@ pub struct Regex {
227
232
  }
228
233
 
229
234
  // find, ignoring ASCII case.
230
- pub func find_ci = (string pattern, string input, ref RegexMatch dst) {
235
+ pub func find_ci = (string pattern, string input, ref RegexMatch dst { text }) {
231
236
  var string folded = fold_pattern(pattern)
232
237
  Regex.find(folded, input, ref dst)
233
238
  }
234
239
 
235
240
  // captures, ignoring ASCII case.
236
- pub func captures_ci = (string pattern, string input, ref List<string> dst, out bool) {
241
+ pub func captures_ci = (string pattern, string input, ref List<string> dst { items }, out bool) {
237
242
  var string folded = fold_pattern(pattern)
238
243
  return Regex.captures(folded, input, ref dst)
239
244
  }
@@ -303,7 +308,7 @@ func fold_pattern = (string pattern, mv out string) {
303
308
 
304
309
  // fold a character class [cs..ce): copy its contents, adding the case
305
310
  // counterpart of every letter member and mirroring letter ranges.
306
- func fold_class = (string pattern, int cs, int ce, ref StringBuilder dst) {
311
+ func fold_class = (string pattern, int cs, int ce, ref StringBuilder dst {}) {
307
312
  dst.append_char('[')
308
313
  var int i = cs + 1
309
314
  if i < ce - 1 {
@@ -430,7 +435,7 @@ func collect_first_bytes = (
430
435
  string pattern,
431
436
  int pat_pos,
432
437
  int pat_end,
433
- ref StringBuilder sb,
438
+ ref StringBuilder sb {},
434
439
  out int
435
440
  ) {
436
441
  var depth = 0
@@ -477,7 +482,7 @@ func collect_first_bytes = (
477
482
  // atom — literal, escaped, class, or group — quantified with '?'/'*' is
478
483
  // nullable: what FOLLOWS it can also be first, so the scan continues past it.
479
484
  // A wildcard '.' or an empty alternative makes the set indeterminable.
480
- func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb, out int) {
485
+ func alt_first_bytes = (string pattern, int start, int end, ref StringBuilder sb {}, out int) {
481
486
  var int i = start
482
487
  while i < end {
483
488
  var char c = pattern.at(i)
@@ -717,7 +722,7 @@ func add_class_chars = (
717
722
  string pattern,
718
723
  int start,
719
724
  int end,
720
- ref StringBuilder sb
725
+ ref StringBuilder sb {}
721
726
  ) { var int cs = start
722
727
  if cs < end {
723
728
  if pattern.at(cs) == '^' {
@@ -772,7 +777,7 @@ func match_here = (
772
777
  string input,
773
778
  int in_pos,
774
779
  int in_len,
775
- ref List<int> caps,
780
+ ref List<int> caps { items },
776
781
  out int: out <= in_len
777
782
  ) {
778
783
  // Handle top-level alternation: a '|' at bracket/paren depth 0 splits the
@@ -1031,7 +1036,7 @@ func match_group_then_rest = (
1031
1036
  string input,
1032
1037
  int in_pos,
1033
1038
  int in_len,
1034
- ref List<int> caps,
1039
+ ref List<int> caps { items },
1035
1040
  out int
1036
1041
  ) {
1037
1042
  var int g = group_index_at(pattern, atom_start)
@@ -1122,7 +1127,7 @@ func match_single_atom = (
1122
1127
  string input,
1123
1128
  int in_pos,
1124
1129
  int in_len,
1125
- ref List<int> caps,
1130
+ ref List<int> caps { items },
1126
1131
  out int
1127
1132
  ) {
1128
1133
  var char pc = pattern.at(atom_start)
@@ -1304,7 +1309,7 @@ func match_counted = (
1304
1309
  int lazy,
1305
1310
  int count,
1306
1311
  int in_len,
1307
- ref List<int> caps,
1312
+ ref List<int> caps { items },
1308
1313
  out int
1309
1314
  ) {
1310
1315
  var can_stop = count >= min_count
@@ -1402,7 +1407,7 @@ func match_star = (
1402
1407
  int min_count,
1403
1408
  int lazy,
1404
1409
  int in_len,
1405
- ref List<int> caps,
1410
+ ref List<int> caps { items },
1406
1411
  out int
1407
1412
  ) {
1408
1413
  var char pc = pattern.at(atom_start)
@@ -1485,7 +1490,7 @@ func match_star_group = (
1485
1490
  int min_count,
1486
1491
  int lazy,
1487
1492
  int in_len,
1488
- ref List<int> caps,
1493
+ ref List<int> caps { items },
1489
1494
  out int
1490
1495
  ) {
1491
1496
  if lazy > 0 {
@@ -1554,7 +1559,7 @@ func match_group_iteration = (
1554
1559
  int min_count,
1555
1560
  int lazy,
1556
1561
  int in_len,
1557
- ref List<int> caps,
1562
+ ref List<int> caps { items },
1558
1563
  out int
1559
1564
  ) {
1560
1565
  var int g = group_index_at(pattern, atom_start)
@@ -1621,7 +1626,7 @@ func match_optional = (
1621
1626
  int in_pos,
1622
1627
  int lazy,
1623
1628
  int in_len,
1624
- ref List<int> caps,
1629
+ ref List<int> caps { items },
1625
1630
  out int
1626
1631
  ) {
1627
1632
  if lazy > 0 {
@@ -1894,7 +1899,7 @@ func group_index_at = (string pattern, int open_pos, out int) {
1894
1899
  // group_pos. A later span for the same group supersedes the earlier one, so
1895
1900
  // a repeated group reports its last iteration; a failed group records
1896
1901
  // (-1, -1) so stale spans from an abandoned attempt never surface.
1897
- func record_capture = (ref List<int> caps, int group, int start, int end) {
1902
+ func record_capture = (ref List<int> caps { items }, int group, int start, int end) {
1898
1903
  if group <= 0 {
1899
1904
  return
1900
1905
  }
@@ -1953,7 +1958,7 @@ func match_backref = (
1953
1958
  int group,
1954
1959
  int in_pos,
1955
1960
  int in_len,
1956
- ref List<int> caps,
1961
+ ref List<int> caps {},
1957
1962
  out int
1958
1963
  ) {
1959
1964
  var int at = last_capture_index(caps, group)
@@ -1989,7 +1994,7 @@ func match_star_backref = (
1989
1994
  int min_count,
1990
1995
  int lazy,
1991
1996
  int in_len,
1992
- ref List<int> caps,
1997
+ ref List<int> caps { items },
1993
1998
  out int
1994
1999
  ) {
1995
2000
  var int group = backref_group(pattern.at(atom_start + 1))
@@ -2111,7 +2116,7 @@ func matches_shorthand = (char c, char b, out bool) {
2111
2116
  // first-byte charset. Complement shorthands (S/D/W) match "anything else" —
2112
2117
  // not expressible as an inclusion set — so they return -1, which disables
2113
2118
  // the caller's prefilter.
2114
- func shorthand_members = (char c, ref StringBuilder sb, out int) {
2119
+ func shorthand_members = (char c, ref StringBuilder sb {}, out int) {
2115
2120
  var int kind = c as int
2116
2121
  if kind == 83 || kind == 68 || kind == 87 {
2117
2122
  return -1
@@ -126,11 +126,11 @@ pub struct Utf8 {
126
126
  sb.append_char(((0x80 | (code & 0x3F)) as char))
127
127
  }
128
128
  } else {
129
- sb.append_char(((0xF0 | (code >> 18)) as char))
130
- sb.append_char(((0x80 | ((code >> 12) & 0x3F)) as char))
131
- sb.append_char(((0x80 | ((code >> 6) & 0x3F)) as char))
132
- sb.append_char(((0x80 | (code & 0x3F)) as char))
133
- }
129
+ sb.append_char(((0xF0 | (code >> 18)) as char))
130
+ sb.append_char(((0x80 | ((code >> 12) & 0x3F)) as char))
131
+ sb.append_char(((0x80 | ((code >> 6) & 0x3F)) as char))
132
+ sb.append_char(((0x80 | (code & 0x3F)) as char))
133
+ }
134
134
  return sb.to_string()
135
135
  }
136
136
 
@@ -82,9 +82,13 @@ pub mv struct Thread<T> : Spawnable<T> {
82
82
  // fields, so #destroy becomes a no-op afterwards. The future's refs
83
83
  // account for the trampoline, the returned handle, and the nursery (when
84
84
  // captured).
85
- pub func start = (ref self, mv out Task<T>) {
85
+ pub func start = (ref self {}, mv out Task<T>) {
86
86
  ```
87
87
  #arch: c
88
+ // Single-launch contract: a consumed construction (started) owns
89
+ // nothing — the handles below are zeroed, and writing the ref count
90
+ // through the null future would fault. Abort with the reason.
91
+ if (self->started) __nomen_spawn_double_start_abort();
88
92
  struct nomen_future *_f = (struct nomen_future *)self->future;
89
93
  _f->refs = self->nursery_futures ? 3 : 2;
90
94
  __nomen_pool_submit((struct nomen_closure *)self->task);
@@ -94,7 +98,12 @@ pub mv struct Thread<T> : Spawnable<T> {
94
98
  ```
95
99
  ```
96
100
  #arch: aarch64
97
- // x19 = self. f = self->future (#32); nursery_futures at #48.
101
+ // x19 = self. started at #40; f = self->future (#32); nursery_futures
102
+ // at #48. Single-launch contract first: a consumed construction has
103
+ // zeroed handles, and the ref-store below would write through null.
104
+ ldrb w9, [x19, #40]
105
+ cbnz w9, .Lnomen_start_double
106
+ // f = self->future (#32); nursery_futures at #48.
98
107
  ldr x0, [x19, #32]
99
108
  ldr x1, [x19, #48]
100
109
  mov w2, #2
@@ -113,6 +122,10 @@ pub mv struct Thread<T> : Spawnable<T> {
113
122
  ldr x3, [x19, #32]
114
123
  bl ___nomen_nursery_track
115
124
  .Lnomen_start_notrack:
125
+ b .Lnomen_start_done
126
+ .Lnomen_start_double:
127
+ bl ___nomen_spawn_double_start_abort
128
+ .Lnomen_start_done:
116
129
  ```
117
130
  // The handle is a fresh heap Task built through the internal #init;
118
131
  // the caller owns it (a discarded statement form is reclaimed at
@@ -136,9 +149,12 @@ pub mv struct Thread<T> : Spawnable<T> {
136
149
  // mid-execution BY DESIGN. The unused future (a Thread always carries
137
150
  // the full machinery) is released — first detaching the closure from it,
138
151
  // so the daemon runner owns the closure now.
139
- pub func detach = (ref self) {
152
+ pub func detach = (ref self {}) {
140
153
  ```
141
154
  #arch: c
155
+ // Single-launch contract (see .start): a consumed construction has
156
+ // zeroed handles, and detaching the null closure would crash a worker.
157
+ if (self->started) __nomen_spawn_double_start_abort();
142
158
  struct nomen_future *_uf = (struct nomen_future *)self->future;
143
159
  if (_uf) {
144
160
  _uf->owner_args = NULL;
@@ -148,6 +164,9 @@ pub mv struct Thread<T> : Spawnable<T> {
148
164
  ```
149
165
  ```
150
166
  #arch: aarch64
167
+ // Single-launch contract first (see .start): started at #40.
168
+ ldrb w9, [x19, #40]
169
+ cbnz w9, .Lnomen_detach_double
151
170
  // if (self->future) { owner_args = NULL; release it }
152
171
  ldr x0, [x19, #32]
153
172
  cbz x0, .Lnomen_detach_nofut
@@ -157,6 +176,10 @@ pub mv struct Thread<T> : Spawnable<T> {
157
176
  // __nomen_task_detach(self->task)
158
177
  ldr x0, [x19, #8]
159
178
  bl ___nomen_task_detach
179
+ b .Lnomen_detach_done
180
+ .Lnomen_detach_double:
181
+ bl ___nomen_spawn_double_start_abort
182
+ .Lnomen_detach_done:
160
183
  ```
161
184
  self.task = 0
162
185
  self.future = 0