nomen-lang 0.10.0 → 0.11.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.
- package/core/System/Format.nm +751 -0
- package/dist/index.mjs +395 -12
- package/package.json +1 -1
- package/src/docs.ts +10 -1
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time-checked rendering helpers behind interpolation format specs
|
|
3
|
+
* (`\{expr:spec}` — SPEC, String Interpolation). The checker validates each
|
|
4
|
+
* spec against the argument's type and lowers the argument into one of these
|
|
5
|
+
* calls, so the interpolation helper keeps receiving an ordinary owned
|
|
6
|
+
* string. Every function is also usable directly.
|
|
7
|
+
*
|
|
8
|
+
* Widths are signed: positive right-aligns, negative left-aligns, 0 means
|
|
9
|
+
* "no padding". `zero` pads with '0' between the sign and the digits (the
|
|
10
|
+
* printf `0` flag); alignment and zero-padding never combine (`<`/`>` and
|
|
11
|
+
* `0` are alternative fills in the spec grammar).
|
|
12
|
+
**/
|
|
13
|
+
pub struct Format {
|
|
14
|
+
// Render a float with a fixed number of decimal places. `spec` is the
|
|
15
|
+
// full printf directive the checker built from the format spec (e.g.
|
|
16
|
+
// "%.2f", "%08.2f", "%-10.3f") — the body formats nothing itself, it
|
|
17
|
+
// only hands the directive to snprintf (twice: size, then render, so
|
|
18
|
+
// any width is safe).
|
|
19
|
+
pub func float_fixed = (float value, string spec, mv out string) {
|
|
20
|
+
```
|
|
21
|
+
#arch: c
|
|
22
|
+
int need = snprintf(NULL, 0, spec.ptr, value);
|
|
23
|
+
char* str = (char*)malloc((size_t)need + 1);
|
|
24
|
+
snprintf(str, (size_t)need + 1, spec.ptr, value);
|
|
25
|
+
str[need] = 0;
|
|
26
|
+
return (nomen_string){ str, need };
|
|
27
|
+
```
|
|
28
|
+
```
|
|
29
|
+
#arch: aarch64
|
|
30
|
+
// x0 = value (double bits — GP param), x1/x2 = the spec's (ptr, len)
|
|
31
|
+
// pair. The spec IS the directive. The variadic double rides the
|
|
32
|
+
// stack (Darwin AAPCS64 varargs); [sp] is re-armed before each call.
|
|
33
|
+
stp x19, x20, [sp, #-16]!
|
|
34
|
+
stp x21, x22, [sp, #-16]!
|
|
35
|
+
mov x19, x0 // value bits
|
|
36
|
+
mov x20, x1 // spec ptr
|
|
37
|
+
sub sp, sp, #16
|
|
38
|
+
fmov d0, x19
|
|
39
|
+
str d0, [sp] // variadic double slot
|
|
40
|
+
mov x0, #0 // NULL — sizing pass
|
|
41
|
+
mov x1, #0
|
|
42
|
+
mov x2, x20
|
|
43
|
+
bl _snprintf
|
|
44
|
+
mov x21, x0 // need
|
|
45
|
+
add x0, x21, #1
|
|
46
|
+
bl _malloc
|
|
47
|
+
mov x22, x0 // str
|
|
48
|
+
fmov d0, x19
|
|
49
|
+
str d0, [sp] // re-arm the slot
|
|
50
|
+
mov x0, x22
|
|
51
|
+
add x1, x21, #1
|
|
52
|
+
mov x2, x20
|
|
53
|
+
bl _snprintf // writes need bytes + NUL at str[need]
|
|
54
|
+
mov x0, x22
|
|
55
|
+
mov x1, x21
|
|
56
|
+
add sp, sp, #16
|
|
57
|
+
ldp x21, x22, [sp], #16
|
|
58
|
+
ldp x19, x20, [sp], #16
|
|
59
|
+
```
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Grouped fixed-point rendering — the `\{value:.2n}` / `\{value:n}` spec
|
|
63
|
+
// (C#'s "N" standard format): render with %.Nf (2 decimals when the spec
|
|
64
|
+
// omits a precision, the C# default), then comma the integer part every
|
|
65
|
+
// three digits. Width and alignment compose at the call site through
|
|
66
|
+
// `Format.pad`; zero-padding and grouping never combine (checked).
|
|
67
|
+
pub func float_grouped = (float value, int places: places >= 0, mv out string) {
|
|
68
|
+
var sb = StringBuilder()
|
|
69
|
+
sb.append_char('%')
|
|
70
|
+
sb.append_char('.')
|
|
71
|
+
sb.append_int(places)
|
|
72
|
+
sb.append_char('f')
|
|
73
|
+
var string rendered = Format.float_fixed(value, sb.to_string())
|
|
74
|
+
return Format.group_int_part(rendered)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Insert ',' every three digits of the integer part of an already
|
|
78
|
+
// rendered number ("1234.50" → "1,234.50", "-987654" → "-987,654").
|
|
79
|
+
// A sign and any fractional part pass through untouched.
|
|
80
|
+
internal func group_int_part = (string text, mv out string) { var int len = text.length
|
|
81
|
+
var int dot = len
|
|
82
|
+
var int i = 0
|
|
83
|
+
while i < len; i += 1 {
|
|
84
|
+
if text.at(i) == '.' {
|
|
85
|
+
dot = i
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
var int digits = 0
|
|
89
|
+
i = 0
|
|
90
|
+
while i < dot; i += 1 {
|
|
91
|
+
var char c = text.at(i)
|
|
92
|
+
if c != '-' && c != '+' {
|
|
93
|
+
digits += 1
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
var sb = StringBuilder()
|
|
97
|
+
var int seen = 0
|
|
98
|
+
i = 0
|
|
99
|
+
while i < len; i += 1 {
|
|
100
|
+
var char c = text.at(i)
|
|
101
|
+
sb.append_char(c)
|
|
102
|
+
if i < dot && c != '-' && c != '+' {
|
|
103
|
+
seen += 1
|
|
104
|
+
var int remaining = digits - seen
|
|
105
|
+
if remaining > 0 && remaining % 3 == 0 {
|
|
106
|
+
sb.append_char(',')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return sb.to_string()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Render a signed integer in base 2/8/10/16. A negative value renders
|
|
114
|
+
// its magnitude with a leading '-' in every base (not two's complement).
|
|
115
|
+
// `group` inserts thousands separators every three digits — intended for
|
|
116
|
+
// base 10 (the `\{value:n}` spec); a grouped other base is the caller's
|
|
117
|
+
// choice and commas every three digits regardless.
|
|
118
|
+
pub func int_base = (int64 value, int base: base >= 2 && base <= 16, bool upper, int width, bool zero, bool group, mv out string) {
|
|
119
|
+
const string digits = "0123456789abcdef"
|
|
120
|
+
const string upper_digits = "0123456789ABCDEF"
|
|
121
|
+
var int w = width
|
|
122
|
+
var bool left = false
|
|
123
|
+
if w < 0 {
|
|
124
|
+
left = true
|
|
125
|
+
w = 0 - w
|
|
126
|
+
}
|
|
127
|
+
var bool neg = value < 0
|
|
128
|
+
var uint64 mag = value as uint64
|
|
129
|
+
if neg {
|
|
130
|
+
// Two's-complement negate on the UNSIGNED magnitude (wraps
|
|
131
|
+
// correctly for INT64_MIN — no signed-overflow UB).
|
|
132
|
+
var uint64 zero = 0
|
|
133
|
+
mag = zero - mag
|
|
134
|
+
}
|
|
135
|
+
var uint64 b = base as uint64
|
|
136
|
+
// Digits arrive least-significant first; the emit walks the scratch
|
|
137
|
+
// backwards. 72 slots covers binary's 64 digits plus the sign.
|
|
138
|
+
var Array<char> chars = Array<char>.with('0', 72)
|
|
139
|
+
var int count = 0
|
|
140
|
+
if mag == 0 {
|
|
141
|
+
chars.set(0, '0')
|
|
142
|
+
count = 1
|
|
143
|
+
} else {
|
|
144
|
+
while mag > 0 {
|
|
145
|
+
var uint64 d = mag % b
|
|
146
|
+
mag = mag / b
|
|
147
|
+
var int di = d as int
|
|
148
|
+
if di < 10 {
|
|
149
|
+
chars.set(count, digits.at(di))
|
|
150
|
+
}
|
|
151
|
+
if di >= 10 && upper {
|
|
152
|
+
chars.set(count, upper_digits.at(di))
|
|
153
|
+
}
|
|
154
|
+
if di >= 10 && !upper {
|
|
155
|
+
chars.set(count, digits.at(di))
|
|
156
|
+
}
|
|
157
|
+
count += 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
var int total = count
|
|
161
|
+
if neg {
|
|
162
|
+
total += 1
|
|
163
|
+
}
|
|
164
|
+
var int padding = 0
|
|
165
|
+
if w > total {
|
|
166
|
+
padding = w - total
|
|
167
|
+
}
|
|
168
|
+
var sb = StringBuilder()
|
|
169
|
+
var int i = 0
|
|
170
|
+
if left {
|
|
171
|
+
// sign, digits, spaces
|
|
172
|
+
if neg {
|
|
173
|
+
sb.append_char('-')
|
|
174
|
+
}
|
|
175
|
+
while i < count; i += 1 {
|
|
176
|
+
sb.append_char(chars.at(count - 1 - i))
|
|
177
|
+
if group && base == 10 && count - 1 - i > 0 && (count - 1 - i) % 3 == 0 {
|
|
178
|
+
sb.append_char(',')
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
i = 0
|
|
182
|
+
while i < padding; i += 1 {
|
|
183
|
+
sb.append_char(' ')
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
if zero {
|
|
187
|
+
// sign, zeros, digits (printf's `0` flag)
|
|
188
|
+
if neg {
|
|
189
|
+
sb.append_char('-')
|
|
190
|
+
}
|
|
191
|
+
while i < padding; i += 1 {
|
|
192
|
+
sb.append_char('0')
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
// spaces, sign, digits
|
|
196
|
+
while i < padding; i += 1 {
|
|
197
|
+
sb.append_char(' ')
|
|
198
|
+
}
|
|
199
|
+
if neg {
|
|
200
|
+
sb.append_char('-')
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
i = 0
|
|
204
|
+
while i < count; i += 1 {
|
|
205
|
+
sb.append_char(chars.at(count - 1 - i))
|
|
206
|
+
if group && base == 10 && count - 1 - i > 0 && (count - 1 - i) % 3 == 0 {
|
|
207
|
+
sb.append_char(',')
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return sb.to_string()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Unsigned twin of int_base — the value argument keeps the uint64 family
|
|
215
|
+
// (uint8..uint64) so no value is reinterpreted through a signed width.
|
|
216
|
+
pub func uint_base = (uint64 value, int base: base >= 2 && base <= 16, bool upper, int width, bool zero, bool group, mv out string) {
|
|
217
|
+
const string digits = "0123456789abcdef"
|
|
218
|
+
const string upper_digits = "0123456789ABCDEF"
|
|
219
|
+
var int w = width
|
|
220
|
+
var bool left = false
|
|
221
|
+
if w < 0 {
|
|
222
|
+
left = true
|
|
223
|
+
w = 0 - w
|
|
224
|
+
}
|
|
225
|
+
var uint64 mag = value
|
|
226
|
+
var uint64 b = base as uint64
|
|
227
|
+
var Array<char> chars = Array<char>.with('0', 72)
|
|
228
|
+
var int count = 0
|
|
229
|
+
if mag == 0 {
|
|
230
|
+
chars.set(0, '0')
|
|
231
|
+
count = 1
|
|
232
|
+
} else {
|
|
233
|
+
while mag > 0 {
|
|
234
|
+
var uint64 d = mag % b
|
|
235
|
+
mag = mag / b
|
|
236
|
+
var int di = d as int
|
|
237
|
+
if di < 10 {
|
|
238
|
+
chars.set(count, digits.at(di))
|
|
239
|
+
}
|
|
240
|
+
if di >= 10 && upper {
|
|
241
|
+
chars.set(count, upper_digits.at(di))
|
|
242
|
+
}
|
|
243
|
+
if di >= 10 && !upper {
|
|
244
|
+
chars.set(count, digits.at(di))
|
|
245
|
+
}
|
|
246
|
+
count += 1
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
var int padding = 0
|
|
250
|
+
if w > count {
|
|
251
|
+
padding = w - count
|
|
252
|
+
}
|
|
253
|
+
var sb = StringBuilder()
|
|
254
|
+
var int i = 0
|
|
255
|
+
if left {
|
|
256
|
+
while i < count; i += 1 {
|
|
257
|
+
sb.append_char(chars.at(count - 1 - i))
|
|
258
|
+
if group && base == 10 && count - 1 - i > 0 && (count - 1 - i) % 3 == 0 {
|
|
259
|
+
sb.append_char(',')
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
i = 0
|
|
263
|
+
while i < padding; i += 1 {
|
|
264
|
+
sb.append_char(' ')
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
while i < padding; i += 1 {
|
|
268
|
+
if zero {
|
|
269
|
+
sb.append_char('0')
|
|
270
|
+
} else {
|
|
271
|
+
sb.append_char(' ')
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
i = 0
|
|
275
|
+
while i < count; i += 1 {
|
|
276
|
+
sb.append_char(chars.at(count - 1 - i))
|
|
277
|
+
if group && base == 10 && count - 1 - i > 0 && (count - 1 - i) % 3 == 0 {
|
|
278
|
+
sb.append_char(',')
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return sb.to_string()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Align an already-rendered text in a field of `|width|` spaces. The
|
|
286
|
+
// text is borrowed; the result is always a fresh owned string.
|
|
287
|
+
pub func pad = (string text, int width, mv out string) {
|
|
288
|
+
var int w = width
|
|
289
|
+
var bool left = false
|
|
290
|
+
if w < 0 {
|
|
291
|
+
left = true
|
|
292
|
+
w = 0 - w
|
|
293
|
+
}
|
|
294
|
+
if w <= text.length {
|
|
295
|
+
return text.copy()
|
|
296
|
+
}
|
|
297
|
+
var int padding = w - text.length
|
|
298
|
+
var sb = StringBuilder()
|
|
299
|
+
if left {
|
|
300
|
+
// text, then spaces
|
|
301
|
+
sb.append(text)
|
|
302
|
+
var int i = 0
|
|
303
|
+
while i < padding; i += 1 {
|
|
304
|
+
sb.append_char(' ')
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
// spaces, then text
|
|
308
|
+
var int i = 0
|
|
309
|
+
while i < padding; i += 1 {
|
|
310
|
+
sb.append_char(' ')
|
|
311
|
+
}
|
|
312
|
+
sb.append(text)
|
|
313
|
+
}
|
|
314
|
+
return sb.to_string()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Custom patterns (the `\{value:#,##0.00}` mini-language) ------------
|
|
318
|
+
//
|
|
319
|
+
// Subset, validated at compile time (SPEC, String Interpolation):
|
|
320
|
+
// 0 digit slot, always renders (zero-pads)
|
|
321
|
+
// # digit slot, renders only when a digit exists
|
|
322
|
+
// . decimal point; trailing '#' fraction slots trim zeros
|
|
323
|
+
// , group separator between digit slots; the rightmost comma
|
|
324
|
+
// sets the group size for all groups
|
|
325
|
+
// % value scaled x100; '%' emitted at its position
|
|
326
|
+
// '...' quoted literal; any other character passes through
|
|
327
|
+
// ; at most two sections (positive;negative) — the negative
|
|
328
|
+
// section renders the absolute value, sign and all
|
|
329
|
+
// Not in the language: exponential patterns (use the `:e` spec), the
|
|
330
|
+
// per-mille sign, trailing-comma scaling, three-section forms.
|
|
331
|
+
//
|
|
332
|
+
// The scanners below share one shape: snapshot `was_quoted`, toggle the
|
|
333
|
+
// quote state on a quote character, then run each structural check as
|
|
334
|
+
// its own top-level `if` gated on "this character is structural" —
|
|
335
|
+
// Nomen has no `else if`.
|
|
336
|
+
|
|
337
|
+
// The section of `pattern` that applies to `negative` values.
|
|
338
|
+
internal func pat_section = (string pattern, bool negative, mv out string) {
|
|
339
|
+
var int split = -1
|
|
340
|
+
var bool quoted = false
|
|
341
|
+
var int i = 0
|
|
342
|
+
while i < pattern.length; i += 1 {
|
|
343
|
+
var char c = pattern.at(i)
|
|
344
|
+
var bool was_quoted = quoted
|
|
345
|
+
if c == '\'' {
|
|
346
|
+
if was_quoted {
|
|
347
|
+
quoted = false
|
|
348
|
+
}
|
|
349
|
+
if !was_quoted {
|
|
350
|
+
quoted = true
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if !was_quoted && !quoted && c == ';' {
|
|
354
|
+
split = i
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
var sb = StringBuilder()
|
|
358
|
+
if split == -1 {
|
|
359
|
+
return pattern.copy()
|
|
360
|
+
}
|
|
361
|
+
var int from = 0
|
|
362
|
+
if negative {
|
|
363
|
+
from = split + 1
|
|
364
|
+
}
|
|
365
|
+
i = from
|
|
366
|
+
while i < pattern.length; i += 1 {
|
|
367
|
+
sb.append_char(pattern.at(i))
|
|
368
|
+
}
|
|
369
|
+
return sb.to_string()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// True when the pattern carries two ';'-separated sections.
|
|
373
|
+
internal func pat_has2 = (string pattern, out bool) {
|
|
374
|
+
var bool quoted = false
|
|
375
|
+
var bool found = false
|
|
376
|
+
var int i = 0
|
|
377
|
+
while i < pattern.length; i += 1 {
|
|
378
|
+
var char c = pattern.at(i)
|
|
379
|
+
var bool was_quoted = quoted
|
|
380
|
+
if c == '\'' {
|
|
381
|
+
if was_quoted {
|
|
382
|
+
quoted = false
|
|
383
|
+
}
|
|
384
|
+
if !was_quoted {
|
|
385
|
+
quoted = true
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if !was_quoted && !quoted && c == ';' {
|
|
389
|
+
found = true
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return found
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// True when the pattern carries a '%' anywhere (unquoted).
|
|
396
|
+
internal func pat_has_percent = (string pattern, out bool) {
|
|
397
|
+
var bool quoted = false
|
|
398
|
+
var bool found = false
|
|
399
|
+
var int i = 0
|
|
400
|
+
while i < pattern.length; i += 1 {
|
|
401
|
+
var char c = pattern.at(i)
|
|
402
|
+
var bool was_quoted = quoted
|
|
403
|
+
if c == '\'' {
|
|
404
|
+
if was_quoted {
|
|
405
|
+
quoted = false
|
|
406
|
+
}
|
|
407
|
+
if !was_quoted {
|
|
408
|
+
quoted = true
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if !was_quoted && !quoted && c == '%' {
|
|
412
|
+
found = true
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return found
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// The widest fraction-slot count across both sections — the snprintf
|
|
419
|
+
// pass renders at least this many digits; shaping trims to the applied
|
|
420
|
+
// section's own slots.
|
|
421
|
+
internal func pat_max_frac = (string pattern, out int) {
|
|
422
|
+
var bool quoted = false
|
|
423
|
+
var bool in_frac = false
|
|
424
|
+
var int count = 0
|
|
425
|
+
var int best = 0
|
|
426
|
+
var int i = 0
|
|
427
|
+
while i < pattern.length; i += 1 {
|
|
428
|
+
var char c = pattern.at(i)
|
|
429
|
+
var bool was_quoted = quoted
|
|
430
|
+
if c == '\'' {
|
|
431
|
+
if was_quoted {
|
|
432
|
+
quoted = false
|
|
433
|
+
}
|
|
434
|
+
if !was_quoted {
|
|
435
|
+
quoted = true
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
var bool structural = !was_quoted && !quoted
|
|
439
|
+
if structural && c == '.' {
|
|
440
|
+
in_frac = true
|
|
441
|
+
}
|
|
442
|
+
if structural && (c == '0' || c == '#') {
|
|
443
|
+
if in_frac {
|
|
444
|
+
count += 1
|
|
445
|
+
if count > best {
|
|
446
|
+
best = count
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if structural && c == ';' {
|
|
451
|
+
in_frac = false
|
|
452
|
+
count = 0
|
|
453
|
+
}
|
|
454
|
+
if structural && c != '.' && c != '0' && c != '#' {
|
|
455
|
+
// any other literal ends the fraction run
|
|
456
|
+
in_frac = false
|
|
457
|
+
count = 0
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return best
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Decimal digits of a magnitude, most-significant first, no sign.
|
|
464
|
+
internal func pat_digits10 = (uint64 mag, mv out string) {
|
|
465
|
+
var sb = StringBuilder()
|
|
466
|
+
if mag == 0 {
|
|
467
|
+
sb.append_char('0')
|
|
468
|
+
return sb.to_string()
|
|
469
|
+
}
|
|
470
|
+
var Array<char> chars = Array<char>.with('0', 20)
|
|
471
|
+
var int count = 0
|
|
472
|
+
var uint64 m = mag
|
|
473
|
+
var string dig = "0123456789"
|
|
474
|
+
while m > 0 {
|
|
475
|
+
var uint64 d = m % 10
|
|
476
|
+
m = m / 10
|
|
477
|
+
chars.set(count, dig.at(d as int))
|
|
478
|
+
count += 1
|
|
479
|
+
}
|
|
480
|
+
var int j = count - 1
|
|
481
|
+
while j >= 0; j -= 1 {
|
|
482
|
+
sb.append_char(chars.at(j))
|
|
483
|
+
}
|
|
484
|
+
return sb.to_string()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Shape digit strings against one pattern section. `int_digits` is the
|
|
488
|
+
// unsigned integer part; `frac_raw` the rendered fraction digits at the
|
|
489
|
+
// section's full precision ("" for integers — zeros fill up to the
|
|
490
|
+
// section's minimum). Emits prefix literals, the grouped integer part,
|
|
491
|
+
// the trimmed fraction, and the suffix. Every helper call result lands
|
|
492
|
+
// in a single-binding local: no owned string is ever reassigned (the
|
|
493
|
+
// displaced-free pass does not cover every conditional reassignment
|
|
494
|
+
// shape — FOLLOWUP.md).
|
|
495
|
+
internal func pat_render = (string section, string int_digits, string frac_raw, bool negative, mv out string) {
|
|
496
|
+
// Pass 1 — locate the regions: prefix (before the first integer
|
|
497
|
+
// slot), the integer slot run, the fraction slots, and the suffix
|
|
498
|
+
// (after the last slot).
|
|
499
|
+
var bool quoted = false
|
|
500
|
+
var int first_slot = -1
|
|
501
|
+
var int last_slot = -1
|
|
502
|
+
var int dot = -1
|
|
503
|
+
var int pad_slots = 0
|
|
504
|
+
var int frac_slots = 0
|
|
505
|
+
var int frac_min = 0
|
|
506
|
+
var bool has_comma = false
|
|
507
|
+
var int slots_after_comma = 0
|
|
508
|
+
var int i = 0
|
|
509
|
+
while i < section.length; i += 1 {
|
|
510
|
+
var char c = section.at(i)
|
|
511
|
+
var bool was_quoted = quoted
|
|
512
|
+
if c == '\'' {
|
|
513
|
+
if was_quoted {
|
|
514
|
+
quoted = false
|
|
515
|
+
}
|
|
516
|
+
if !was_quoted {
|
|
517
|
+
quoted = true
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
var bool structural = !was_quoted && !quoted
|
|
521
|
+
if structural && c == '.' {
|
|
522
|
+
if dot == -1 {
|
|
523
|
+
dot = i
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if structural && (c == '0' || c == '#') {
|
|
527
|
+
last_slot = i
|
|
528
|
+
if dot == -1 {
|
|
529
|
+
// an integer slot — never counted before the dot
|
|
530
|
+
if first_slot == -1 {
|
|
531
|
+
first_slot = i
|
|
532
|
+
}
|
|
533
|
+
slots_after_comma += 1
|
|
534
|
+
if c == '0' {
|
|
535
|
+
// only '0' slots force zero-padding ('#' drops out)
|
|
536
|
+
pad_slots += 1
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
frac_slots += 1
|
|
540
|
+
if c == '0' {
|
|
541
|
+
frac_min = frac_slots
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if structural && c == ',' && dot == -1 {
|
|
546
|
+
has_comma = true
|
|
547
|
+
slots_after_comma = 0
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Pass 2 — emit.
|
|
551
|
+
var sb = StringBuilder()
|
|
552
|
+
if negative {
|
|
553
|
+
sb.append_char('-')
|
|
554
|
+
}
|
|
555
|
+
// prefix: [0, first_slot), quotes stripped
|
|
556
|
+
i = 0
|
|
557
|
+
quoted = false
|
|
558
|
+
while i < first_slot; i += 1 {
|
|
559
|
+
var char c = section.at(i)
|
|
560
|
+
var bool was_quoted = quoted
|
|
561
|
+
if c == '\'' {
|
|
562
|
+
if was_quoted {
|
|
563
|
+
quoted = false
|
|
564
|
+
}
|
|
565
|
+
if !was_quoted {
|
|
566
|
+
quoted = true
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if was_quoted && c != '\'' {
|
|
570
|
+
sb.append_char(c)
|
|
571
|
+
}
|
|
572
|
+
if !was_quoted && c != '\'' {
|
|
573
|
+
sb.append_char(c)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// integer part: zero-pad to the '0' slots, group per the rightmost
|
|
577
|
+
// comma's run, drop a lone zero when every slot is '#' (the shape
|
|
578
|
+
// helper's own empty case). The shape result lands in a local — an
|
|
579
|
+
// inline call argument to append_string would never be anchored.
|
|
580
|
+
sb.append_string(Format.pat_shape_int(int_digits, pad_slots, has_comma, slots_after_comma))
|
|
581
|
+
// fraction: trim trailing zeros down to the section minimum, then
|
|
582
|
+
// zero-fill up to it (integer values arrive with no fraction at all).
|
|
583
|
+
var string kept = Format.pat_shape_frac(frac_raw, frac_slots, frac_min)
|
|
584
|
+
if kept.length > 0 {
|
|
585
|
+
sb.append_char('.')
|
|
586
|
+
sb.append(kept)
|
|
587
|
+
}
|
|
588
|
+
// suffix: (last_slot, end), quotes stripped, structural dot dropped
|
|
589
|
+
i = last_slot + 1
|
|
590
|
+
quoted = false
|
|
591
|
+
while i < section.length; i += 1 {
|
|
592
|
+
var char c = section.at(i)
|
|
593
|
+
var bool was_quoted = quoted
|
|
594
|
+
if c == '\'' {
|
|
595
|
+
if was_quoted {
|
|
596
|
+
quoted = false
|
|
597
|
+
}
|
|
598
|
+
if !was_quoted {
|
|
599
|
+
quoted = true
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if was_quoted && c != '\'' {
|
|
603
|
+
sb.append_char(c)
|
|
604
|
+
}
|
|
605
|
+
if !was_quoted && c != '\'' && i != dot {
|
|
606
|
+
sb.append_char(c)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return sb.to_string()
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// The integer part of a pattern: zero-pad to the '0' slots, then insert
|
|
613
|
+
// group separators every `group_size` digits. A value of "0" with no '0'
|
|
614
|
+
// slots renders empty (the `#`-only shapes).
|
|
615
|
+
internal func pat_shape_int = (string int_digits, int pad_slots, bool has_comma, int group_size, mv out string) {
|
|
616
|
+
var sb = StringBuilder()
|
|
617
|
+
if int_digits == "0" && pad_slots == 0 {
|
|
618
|
+
return sb.to_string()
|
|
619
|
+
}
|
|
620
|
+
var int total = int_digits.length
|
|
621
|
+
if total < pad_slots {
|
|
622
|
+
total = pad_slots
|
|
623
|
+
}
|
|
624
|
+
var int pad_count = total - int_digits.length
|
|
625
|
+
var int k = 0
|
|
626
|
+
while k < total; k += 1 {
|
|
627
|
+
if k < pad_count {
|
|
628
|
+
sb.append_char('0')
|
|
629
|
+
} else {
|
|
630
|
+
sb.append_char(int_digits.at(k - pad_count))
|
|
631
|
+
}
|
|
632
|
+
var int remaining = total - k - 1
|
|
633
|
+
if has_comma && group_size > 0 && remaining > 0 && remaining % group_size == 0 {
|
|
634
|
+
sb.append_char(',')
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return sb.to_string()
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The fraction part of a pattern: clip to the slot count, trim trailing
|
|
641
|
+
// zeros down to the minimum ('0' slots), then zero-fill back up to it.
|
|
642
|
+
internal func pat_shape_frac = (string frac_raw, int frac_slots, int frac_min, mv out string) {
|
|
643
|
+
var sb = StringBuilder()
|
|
644
|
+
var int n = frac_raw.length
|
|
645
|
+
if n > frac_slots {
|
|
646
|
+
n = frac_slots
|
|
647
|
+
}
|
|
648
|
+
var bool trimming = true
|
|
649
|
+
while trimming && n > frac_min {
|
|
650
|
+
var char last = frac_raw.at(n - 1)
|
|
651
|
+
if last == '0' {
|
|
652
|
+
n -= 1
|
|
653
|
+
}
|
|
654
|
+
if last != '0' {
|
|
655
|
+
trimming = false
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
var int i = 0
|
|
659
|
+
while i < n; i += 1 {
|
|
660
|
+
sb.append_char(frac_raw.at(i))
|
|
661
|
+
}
|
|
662
|
+
while n < frac_min; n += 1 {
|
|
663
|
+
sb.append_char('0')
|
|
664
|
+
}
|
|
665
|
+
return sb.to_string()
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Pick the sign's section, render, and prepend '-' when a negative value
|
|
669
|
+
// has no dedicated section.
|
|
670
|
+
internal func pat_render_all = (string pattern, bool negative, string int_digits, string frac_raw, mv out string) {
|
|
671
|
+
var string section = Format.pat_section(pattern, negative)
|
|
672
|
+
var bool has2 = Format.pat_has2(pattern)
|
|
673
|
+
return Format.pat_render(section, int_digits, frac_raw, negative && !has2)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// `#,##0`-style rendering of a signed integer.
|
|
677
|
+
pub func int_pattern = (int64 value, string pattern, mv out string) {
|
|
678
|
+
var bool neg = value < 0
|
|
679
|
+
var uint64 mag = value as uint64
|
|
680
|
+
if neg {
|
|
681
|
+
var uint64 zero = 0
|
|
682
|
+
mag = zero - mag
|
|
683
|
+
}
|
|
684
|
+
if Format.pat_has_percent(pattern) {
|
|
685
|
+
mag = mag * 100
|
|
686
|
+
}
|
|
687
|
+
// The digit string stays a named local on purpose: a plain `string`
|
|
688
|
+
// parameter is a borrow, so this local must free at scope exit, and
|
|
689
|
+
// the per-function ownership sets must isolate it from the
|
|
690
|
+
// same-named `const string digits` locals in int_base/uint_base
|
|
691
|
+
// (name-keyed marks bleeding across library methods suppressed this
|
|
692
|
+
// free — FOLLOWUP receipt; displaced_free.test.ts pins the class).
|
|
693
|
+
var string digits = Format.pat_digits10(mag)
|
|
694
|
+
var string rendered = Format.pat_render_all(pattern, neg, digits, "")
|
|
695
|
+
return rendered
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Unsigned twin of int_pattern — no sign, no reinterpretation.
|
|
699
|
+
pub func uint_pattern = (uint64 value, string pattern, mv out string) {
|
|
700
|
+
var uint64 mag = value
|
|
701
|
+
if Format.pat_has_percent(pattern) {
|
|
702
|
+
mag = mag * 100
|
|
703
|
+
}
|
|
704
|
+
return Format.pat_render_all(pattern, false, Format.pat_digits10(mag), "")
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Fixed-point pattern rendering of a float: snprintf renders the
|
|
708
|
+
// fraction at the pattern's widest precision, then the shaping pass
|
|
709
|
+
// trims to the applied section's slots.
|
|
710
|
+
pub func float_pattern = (float value, string pattern, mv out string) {
|
|
711
|
+
var bool neg = value < 0
|
|
712
|
+
var float v = value
|
|
713
|
+
if neg {
|
|
714
|
+
var float zero = 0
|
|
715
|
+
v = zero - v
|
|
716
|
+
}
|
|
717
|
+
if Format.pat_has_percent(pattern) {
|
|
718
|
+
v = v * (100 as float)
|
|
719
|
+
}
|
|
720
|
+
var int max_frac = Format.pat_max_frac(pattern)
|
|
721
|
+
var spec_sb = StringBuilder()
|
|
722
|
+
spec_sb.append_char('%')
|
|
723
|
+
spec_sb.append_char('.')
|
|
724
|
+
spec_sb.append_int(max_frac)
|
|
725
|
+
spec_sb.append_char('f')
|
|
726
|
+
var string rendered = Format.float_fixed(v, spec_sb.to_string())
|
|
727
|
+
// rendered = digits '.' frac — split them
|
|
728
|
+
var int dot = rendered.length
|
|
729
|
+
var int i = 0
|
|
730
|
+
while i < rendered.length; i += 1 {
|
|
731
|
+
if rendered.at(i) == '.' {
|
|
732
|
+
dot = i
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
var int_sb = StringBuilder()
|
|
736
|
+
var frac_sb = StringBuilder()
|
|
737
|
+
var int s = 0
|
|
738
|
+
while s < rendered.length; s += 1 {
|
|
739
|
+
if s < dot {
|
|
740
|
+
int_sb.append_char(rendered.at(s))
|
|
741
|
+
}
|
|
742
|
+
if s > dot {
|
|
743
|
+
frac_sb.append_char(rendered.at(s))
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Both digit strings pass as call results (not locals at their last
|
|
747
|
+
// use) so the boundary keeps them borrows the call site anchors.
|
|
748
|
+
return Format.pat_render_all(pattern, neg, int_sb.to_string(), frac_sb.to_string())
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
}
|
package/dist/index.mjs
CHANGED
|
@@ -7257,6 +7257,15 @@ var FunctionCallNode = class extends BaseNode {
|
|
|
7257
7257
|
*/
|
|
7258
7258
|
in_place_payload_indices;
|
|
7259
7259
|
/**
|
|
7260
|
+
* Per-argument format specs for a `_string_interpolate_N` call
|
|
7261
|
+
* (`\{expr:spec}`): entry `i - 1` corresponds to param `i` (index 0 is
|
|
7262
|
+
* the pattern literal). Null entry = no spec for that argument. Set by
|
|
7263
|
+
* the parser; the checker validates each spec against the argument's
|
|
7264
|
+
* type and rewrites the argument into a `Format.*` rendering call (see
|
|
7265
|
+
* check/utils/format_spec_arg.ts).
|
|
7266
|
+
*/
|
|
7267
|
+
format_specs;
|
|
7268
|
+
/**
|
|
7260
7269
|
* The checker-stamped mangled label for a call resolved to an overloaded
|
|
7261
7270
|
* callee (`Struct_method_int`). For struct constructor calls resolved to
|
|
7262
7271
|
* an overloaded `#init`, the backends emit/bl the call through this label
|
|
@@ -15835,7 +15844,11 @@ function is_fresh_heap_string(node) {
|
|
|
15835
15844
|
if (!node) return false;
|
|
15836
15845
|
if (node.node_type === "access") {
|
|
15837
15846
|
const access = node.access;
|
|
15838
|
-
if (access.node_type === "access_func"
|
|
15847
|
+
if (access.node_type === "access_func") {
|
|
15848
|
+
const fn = access;
|
|
15849
|
+
if (fn.name === "to_string") return true;
|
|
15850
|
+
if (fn.owned_return) return true;
|
|
15851
|
+
}
|
|
15839
15852
|
}
|
|
15840
15853
|
if (node.type?.name !== "string") return false;
|
|
15841
15854
|
if (node.node_type === "op") return true;
|
|
@@ -15843,8 +15856,7 @@ function is_fresh_heap_string(node) {
|
|
|
15843
15856
|
if (node.node_type === "access") {
|
|
15844
15857
|
const access = node.access;
|
|
15845
15858
|
if (access.node_type !== "access_func") return false;
|
|
15846
|
-
|
|
15847
|
-
return !(!fn.owned_return && is_call_site_borrow_accessor(fn.name));
|
|
15859
|
+
return !is_call_site_borrow_accessor(access.name);
|
|
15848
15860
|
}
|
|
15849
15861
|
return false;
|
|
15850
15862
|
}
|
|
@@ -36405,6 +36417,7 @@ function clone_node(node) {
|
|
|
36405
36417
|
c.swap_params = n.swap_params ? new Map([...n.swap_params].map(([k, v]) => [k, clone_node(v)])) : void 0;
|
|
36406
36418
|
c.variadic_param_name = n.variadic_param_name;
|
|
36407
36419
|
c.variadic_param_index = n.variadic_param_index;
|
|
36420
|
+
c.format_specs = n.format_specs?.slice();
|
|
36408
36421
|
c.is_enum_shorthand = n.is_enum_shorthand;
|
|
36409
36422
|
c.is_thread_ctor = n.is_thread_ctor;
|
|
36410
36423
|
c.is_fiber_ctor = n.is_fiber_ctor;
|
|
@@ -47524,6 +47537,116 @@ function promote_allocations(node, status) {
|
|
|
47524
47537
|
}
|
|
47525
47538
|
}
|
|
47526
47539
|
//#endregion
|
|
47540
|
+
//#region ../src/check/utils/format_spec_arg.ts
|
|
47541
|
+
/**
|
|
47542
|
+
* Compile-time-checked format specs (`\{expr:spec}` — SPEC, String
|
|
47543
|
+
* Interpolation). The parse layer splits the spec text off the interpolation
|
|
47544
|
+
* argument and attaches it to the call (`FunctionCallNode.format_specs`);
|
|
47545
|
+
* this module validates the spec against the argument's TYPE — the
|
|
47546
|
+
* compile-time half of the feature — and lowers the argument into a call of
|
|
47547
|
+
* the matching System `Format` function, so the interpolation helper keeps
|
|
47548
|
+
* receiving an ordinary owned string.
|
|
47549
|
+
*
|
|
47550
|
+
* \{f:.2} / \{f:08.2} / \{f:<10.3} → Format.float_fixed(v, "<directive>")
|
|
47551
|
+
* The checker builds the full printf directive (it knows the spec is
|
|
47552
|
+
* constant), so the raw body is one snprintf with no flag logic.
|
|
47553
|
+
* \{i:03} / \{i:x} / \{u:08X} / \{i:b} → Format.int_base / Format.uint_base
|
|
47554
|
+
* Signed arguments widen to int64, unsigned to uint64 (both accepted
|
|
47555
|
+
* families under the implicit-int coercion ranking), then the library's
|
|
47556
|
+
* digit loop renders base/width/zero/alignment.
|
|
47557
|
+
* \{s:>8} / anything with only width/align → Format.pad(rendered, width)
|
|
47558
|
+
* The argument keeps its `.to_string()` wrap; pad aligns the text.
|
|
47559
|
+
*
|
|
47560
|
+
* Type mismatches are REJECTED here (precision on a non-float, base on a
|
|
47561
|
+
* non-integer, zero-padding on a non-numeric) with the spec text in the
|
|
47562
|
+
* message — an invalid `\{expr:spec}` never reaches codegen.
|
|
47563
|
+
*/
|
|
47564
|
+
/** Lower one spec'd interpolation argument. Returns the replacement node, or
|
|
47565
|
+
* undefined when the spec was rejected (error already reported) or the
|
|
47566
|
+
* argument shape is unexpected (keep the plain wrap). */
|
|
47567
|
+
function check_format_spec_arg(param, spec, status) {
|
|
47568
|
+
const wrap = param.node_type === "access" && param.access.node_type === "access_func" && param.access.name === "to_string" ? param : void 0;
|
|
47569
|
+
const target = wrap ? param.target : param;
|
|
47570
|
+
const t = type_from_value_node(target, status);
|
|
47571
|
+
const info = !t.is_array && !t.is_view && !t.is_ref && !t.is_pointer ? get_built_in_type(t.name) : void 0;
|
|
47572
|
+
const kind = info?.kind;
|
|
47573
|
+
const is_float = kind === "float";
|
|
47574
|
+
const is_int = kind === "int";
|
|
47575
|
+
if (spec.precision !== void 0 && !is_float) {
|
|
47576
|
+
add_error(status, `format spec ':${spec.text}': precision requires a float, but the value is ${t.name || "value"}`, spec.start);
|
|
47577
|
+
return;
|
|
47578
|
+
}
|
|
47579
|
+
if (spec.base && "xXbo".includes(spec.base) && !is_int) {
|
|
47580
|
+
add_error(status, `format spec ':${spec.text}': base requires an integer, but the value is ${t.name || "value"}`, spec.start);
|
|
47581
|
+
return;
|
|
47582
|
+
}
|
|
47583
|
+
if (spec.base && "nN".includes(spec.base) && !is_int && !is_float) {
|
|
47584
|
+
add_error(status, `format spec ':${spec.text}': grouping requires an integer or float, but the value is ${t.name || "value"}`, spec.start);
|
|
47585
|
+
return;
|
|
47586
|
+
}
|
|
47587
|
+
if (spec.pattern !== void 0 && !is_int && !is_float) {
|
|
47588
|
+
add_error(status, `format spec ':${spec.text}': pattern requires an integer or float, but the value is ${t.name || "value"}`, spec.start);
|
|
47589
|
+
return;
|
|
47590
|
+
}
|
|
47591
|
+
if (spec.base && "eE".includes(spec.base) && !is_float) {
|
|
47592
|
+
add_error(status, `format spec ':${spec.text}': exponential requires a float, but the value is ${t.name || "value"}`, spec.start);
|
|
47593
|
+
return;
|
|
47594
|
+
}
|
|
47595
|
+
if (spec.zero && !is_float && !is_int) {
|
|
47596
|
+
add_error(status, `format spec ':${spec.text}': zero-padding requires an integer or float, but the value is ${t.name || "value"}`, spec.start);
|
|
47597
|
+
return;
|
|
47598
|
+
}
|
|
47599
|
+
const signed_width = (spec.align === "left" ? -1 : 1) * (spec.width ?? 0);
|
|
47600
|
+
const is_exponential = spec.base === "e" || spec.base === "E";
|
|
47601
|
+
const is_grouped = spec.base === "n" || spec.base === "N";
|
|
47602
|
+
if (spec.pattern !== void 0) {
|
|
47603
|
+
const fname = is_float ? "float_pattern" : info?.signed ? "int_pattern" : "uint_pattern";
|
|
47604
|
+
const rendered = format_call(spec.start, fname, [new CastNode(target.start, target, new Type(is_float ? "float" : info?.signed ? "int64" : "uint64")), new ValueNode(target.start, `"${escape_pattern(spec.pattern)}"`, new Type("string", true))]);
|
|
47605
|
+
if (signed_width === 0) return rendered;
|
|
47606
|
+
return format_call(spec.start, "pad", [rendered, new ValueNode(target.start, String(signed_width), new Type("int", true))]);
|
|
47607
|
+
}
|
|
47608
|
+
if (is_grouped && is_float) {
|
|
47609
|
+
const places = spec.precision ?? 2;
|
|
47610
|
+
const grouped = format_call(spec.start, "float_grouped", [new CastNode(target.start, target, new Type("float")), new ValueNode(target.start, String(places), new Type("int", true))]);
|
|
47611
|
+
if (signed_width === 0) return grouped;
|
|
47612
|
+
return format_call(spec.start, "pad", [grouped, new ValueNode(target.start, String(signed_width), new Type("int", true))]);
|
|
47613
|
+
}
|
|
47614
|
+
if (is_float && (spec.precision !== void 0 || is_exponential)) {
|
|
47615
|
+
let directive = "%";
|
|
47616
|
+
if (spec.zero && signed_width > 0) directive += "0";
|
|
47617
|
+
if (spec.align === "left") directive += "-";
|
|
47618
|
+
if (signed_width !== 0) directive += String(signed_width);
|
|
47619
|
+
directive += `.${spec.precision ?? 6}`;
|
|
47620
|
+
directive += is_exponential ? spec.base : "f";
|
|
47621
|
+
return format_call(spec.start, "float_fixed", [new CastNode(target.start, target, new Type("float")), new ValueNode(target.start, `"${directive}"`, new Type("string", true))]);
|
|
47622
|
+
}
|
|
47623
|
+
if (is_int && (spec.base || spec.zero)) {
|
|
47624
|
+
const grouped = is_grouped;
|
|
47625
|
+
const base = spec.base === "x" || spec.base === "X" ? 16 : spec.base === "b" ? 2 : spec.base === "o" ? 8 : 10;
|
|
47626
|
+
const fname = info?.signed ? "int_base" : "uint_base";
|
|
47627
|
+
const render_width = grouped ? 0 : signed_width;
|
|
47628
|
+
const rendered = format_call(spec.start, fname, [
|
|
47629
|
+
new CastNode(target.start, target, new Type(info?.signed ? "int64" : "uint64")),
|
|
47630
|
+
new ValueNode(target.start, String(base), new Type("int", true)),
|
|
47631
|
+
new ValueNode(target.start, spec.base === "X" ? "true" : "false", new Type("bool", true)),
|
|
47632
|
+
new ValueNode(target.start, String(render_width), new Type("int", true)),
|
|
47633
|
+
new ValueNode(target.start, spec.zero && !grouped ? "true" : "false", new Type("bool", true)),
|
|
47634
|
+
new ValueNode(target.start, grouped ? "true" : "false", new Type("bool", true))
|
|
47635
|
+
]);
|
|
47636
|
+
if (grouped && signed_width !== 0) return format_call(spec.start, "pad", [rendered, new ValueNode(target.start, String(signed_width), new Type("int", true))]);
|
|
47637
|
+
return rendered;
|
|
47638
|
+
}
|
|
47639
|
+
return format_call(spec.start, "pad", [wrap ?? param, new ValueNode(target.start, String(signed_width), new Type("int", true))]);
|
|
47640
|
+
}
|
|
47641
|
+
function format_call(start, fname, params) {
|
|
47642
|
+
return new AccessNode(start, new ValueNode(start, "Format"), new AccessFunctionCallNode(start, fname, void 0, params));
|
|
47643
|
+
}
|
|
47644
|
+
/** The pattern rides a Nomen string literal — escape the two characters
|
|
47645
|
+
* that would terminate or escape it. */
|
|
47646
|
+
function escape_pattern(pattern) {
|
|
47647
|
+
return pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
47648
|
+
}
|
|
47649
|
+
//#endregion
|
|
47527
47650
|
//#region ../src/check/utils/regex_pattern_lint.ts
|
|
47528
47651
|
/**
|
|
47529
47652
|
* Compile-time lint for Regex pattern literals (FOLLOWUP "Regex pattern
|
|
@@ -47949,6 +48072,15 @@ function check_function_call(node, status, func, target_type, self_value, self_p
|
|
|
47949
48072
|
status.expected_type = old_expected_type;
|
|
47950
48073
|
status.allow_null_value = old_allow_null;
|
|
47951
48074
|
status.checking_variadic_arg = old_variadic_arg;
|
|
48075
|
+
const spec = i > 0 && is_interp_name(node.name) && node.node_type === "func_call" ? node.format_specs?.[i - 1] : void 0;
|
|
48076
|
+
if (spec) {
|
|
48077
|
+
const lowered = check_format_spec_arg(param, spec, status);
|
|
48078
|
+
if (lowered) {
|
|
48079
|
+
node.params[i] = lowered;
|
|
48080
|
+
param = lowered;
|
|
48081
|
+
check_node(lowered, status);
|
|
48082
|
+
}
|
|
48083
|
+
}
|
|
47952
48084
|
if (i > 0 && is_interp_name(node.name) && parse_interp_name(node.name).kinds === null) {
|
|
47953
48085
|
const rewrite = interp_arg_rewrite(param, status);
|
|
47954
48086
|
if (rewrite.kind && rewrite.target) {
|
|
@@ -55356,6 +55488,12 @@ function build_struct_functions(node, status, skip_init = false) {
|
|
|
55356
55488
|
status.ref_class_param_types = /* @__PURE__ */ new Map();
|
|
55357
55489
|
status.scoped_declarations = enter_c_scope(status);
|
|
55358
55490
|
status.c_borrow_only_strings = scan_borrow_only_strings(func);
|
|
55491
|
+
const old_string_borrow_vars = status.string_borrow_vars;
|
|
55492
|
+
status.string_borrow_vars = /* @__PURE__ */ new Set();
|
|
55493
|
+
const old_moved_string_vars = status.moved_string_vars;
|
|
55494
|
+
status.moved_string_vars = /* @__PURE__ */ new Set();
|
|
55495
|
+
const old_owned_string_vars = status.owned_string_vars;
|
|
55496
|
+
status.owned_string_vars = /* @__PURE__ */ new Set();
|
|
55359
55497
|
status.force_heap_strings = scan_force_heap_strings(func.statements ?? [], status.structs);
|
|
55360
55498
|
status.function_return_type = func.return_type;
|
|
55361
55499
|
const old_nullable_ret_has = status.nullable_ret_has_param;
|
|
@@ -55480,6 +55618,9 @@ function build_struct_functions(node, status, skip_init = false) {
|
|
|
55480
55618
|
status.ref_local_vars = old_ref_locals;
|
|
55481
55619
|
status.ref_slot_vars = old_slot_locals;
|
|
55482
55620
|
status.variable_types = old_variable_types;
|
|
55621
|
+
status.string_borrow_vars = old_string_borrow_vars;
|
|
55622
|
+
status.moved_string_vars = old_moved_string_vars;
|
|
55623
|
+
status.owned_string_vars = old_owned_string_vars;
|
|
55483
55624
|
}
|
|
55484
55625
|
status.current_struct = old_current_struct;
|
|
55485
55626
|
for (let traitName of node.traits) {
|
|
@@ -71760,7 +71901,10 @@ const PREFIX_POSITIONS = /* @__PURE__ */ new Set([
|
|
|
71760
71901
|
function join$2(pieces) {
|
|
71761
71902
|
let out = "";
|
|
71762
71903
|
for (let i = 0; i < pieces.length; i++) {
|
|
71763
|
-
if (i > 0
|
|
71904
|
+
if (i > 0) {
|
|
71905
|
+
if (pieces[i].kind === "comment") out += pieces[i].gap_before ?? (pieces[i].space_before ? " " : "");
|
|
71906
|
+
else if (needs_space(pieces, i)) out += " ";
|
|
71907
|
+
}
|
|
71764
71908
|
out += pieces[i].text;
|
|
71765
71909
|
}
|
|
71766
71910
|
return out;
|
|
@@ -71770,13 +71914,12 @@ function needs_space(pieces, i) {
|
|
|
71770
71914
|
const current = pieces[i];
|
|
71771
71915
|
if (KEEP_SPACING.has(current.text) || KEEP_SPACING.has(previous.text)) return current.space_before;
|
|
71772
71916
|
if (NO_SPACE_BEFORE.has(current.text)) return false;
|
|
71773
|
-
if (current.text === "}") return
|
|
71917
|
+
if (current.text === "}") return previous.text !== "{";
|
|
71774
71918
|
if (NO_SPACE_AFTER.has(previous.text)) return false;
|
|
71775
71919
|
if (previous.text === "out") return current.text !== ".";
|
|
71776
71920
|
if (is_sign(pieces, i - 1)) return false;
|
|
71777
71921
|
if (current.text === ".") return !is_value_end(pieces, i - 1);
|
|
71778
71922
|
if (current.text === "(" || current.text === "[") return !is_value_end(pieces, i - 1) && !CALL_KEYWORDS.has(previous.text);
|
|
71779
|
-
if (current.kind === "comment") return current.space_before;
|
|
71780
71923
|
return true;
|
|
71781
71924
|
}
|
|
71782
71925
|
/** Is the piece at `i` a sign (`-5`) rather than an operator (`a - 5`)? */
|
|
@@ -72000,11 +72143,21 @@ function consume_string(input, status) {
|
|
|
72000
72143
|
});
|
|
72001
72144
|
let end = consume_interpolated_expression(input, status.i);
|
|
72002
72145
|
let fragment = input.substring(status.i + 2, end);
|
|
72146
|
+
const spec_at = find_format_spec_split(fragment);
|
|
72147
|
+
let spec_token;
|
|
72148
|
+
if (spec_at !== -1) {
|
|
72149
|
+
spec_token = {
|
|
72150
|
+
value: fragment.substring(spec_at),
|
|
72151
|
+
i: status.i + 2 + spec_at
|
|
72152
|
+
};
|
|
72153
|
+
fragment = fragment.substring(0, spec_at);
|
|
72154
|
+
}
|
|
72003
72155
|
status.i = status.i + 2;
|
|
72004
72156
|
for (let subtoken of tokenize(fragment)) {
|
|
72005
72157
|
subtoken.i += status.i;
|
|
72006
72158
|
status.tokens.push(subtoken);
|
|
72007
72159
|
}
|
|
72160
|
+
if (spec_token) status.tokens.push(spec_token);
|
|
72008
72161
|
status.i = end;
|
|
72009
72162
|
status.tokens.push({
|
|
72010
72163
|
value: "}",
|
|
@@ -72031,6 +72184,35 @@ function consume_interpolated_expression(input, i) {
|
|
|
72031
72184
|
}
|
|
72032
72185
|
return input.length - 1;
|
|
72033
72186
|
}
|
|
72187
|
+
/** Index of the `:` starting a format spec in an interpolation body, or -1.
|
|
72188
|
+
* The spec (if any) always runs to the end of the fragment, so the
|
|
72189
|
+
* RIGHTMOST top-level `:` wins; colons inside string/char literals and
|
|
72190
|
+
* parentheses/brackets are skipped. The parse layer validates the spec
|
|
72191
|
+
* grammar — a top-level `:` has no other meaning in an expression, so any
|
|
72192
|
+
* one found here is treated as a spec attempt and reported there. */
|
|
72193
|
+
function find_format_spec_split(fragment) {
|
|
72194
|
+
let depth = 0;
|
|
72195
|
+
let candidate = -1;
|
|
72196
|
+
let in_string = false;
|
|
72197
|
+
let in_char = false;
|
|
72198
|
+
for (let j = 0; j < fragment.length; j++) {
|
|
72199
|
+
const c = fragment[j];
|
|
72200
|
+
if (in_string || in_char) {
|
|
72201
|
+
if (c === "\\") j += 1;
|
|
72202
|
+
else if (in_string && c === "\"") in_string = false;
|
|
72203
|
+
else if (in_char && c === "'") in_char = false;
|
|
72204
|
+
continue;
|
|
72205
|
+
}
|
|
72206
|
+
if (c === "\"") in_string = true;
|
|
72207
|
+
else if (c === "'") in_char = true;
|
|
72208
|
+
else if (c === "(" || c === "[") depth += 1;
|
|
72209
|
+
else if (c === ")" || c === "]") depth -= 1;
|
|
72210
|
+
else if (c === ":" && depth === 0) {
|
|
72211
|
+
if (fragment[j + 1] !== ":" && fragment[j - 1] !== ":") candidate = j;
|
|
72212
|
+
}
|
|
72213
|
+
}
|
|
72214
|
+
return candidate;
|
|
72215
|
+
}
|
|
72034
72216
|
function consume_comment(input, status) {
|
|
72035
72217
|
for (let j = status.i + 1; j < input.length; j++) if (input[j] === "\n") return j;
|
|
72036
72218
|
return input.length - 1;
|
|
@@ -72125,12 +72307,12 @@ function scan_line(text, state) {
|
|
|
72125
72307
|
return verbatim(text);
|
|
72126
72308
|
}
|
|
72127
72309
|
const pieces = [];
|
|
72128
|
-
let
|
|
72310
|
+
let gap = "";
|
|
72129
72311
|
let i = 0;
|
|
72130
72312
|
while (i < text.length) {
|
|
72131
72313
|
const c = text[i];
|
|
72132
72314
|
if (is_space(c)) {
|
|
72133
|
-
|
|
72315
|
+
gap += c;
|
|
72134
72316
|
i++;
|
|
72135
72317
|
continue;
|
|
72136
72318
|
}
|
|
@@ -72164,9 +72346,10 @@ function scan_line(text, state) {
|
|
|
72164
72346
|
pieces.push({
|
|
72165
72347
|
kind,
|
|
72166
72348
|
text: text.slice(start, i),
|
|
72167
|
-
space_before
|
|
72349
|
+
space_before: gap.length > 0,
|
|
72350
|
+
gap_before: gap
|
|
72168
72351
|
});
|
|
72169
|
-
|
|
72352
|
+
gap = "";
|
|
72170
72353
|
}
|
|
72171
72354
|
if (!pieces.length) return {
|
|
72172
72355
|
kind: "blank",
|
|
@@ -72415,6 +72598,11 @@ function join_import(pieces) {
|
|
|
72415
72598
|
out = piece.text;
|
|
72416
72599
|
continue;
|
|
72417
72600
|
}
|
|
72601
|
+
if (piece.kind === "comment") {
|
|
72602
|
+
out += piece.gap_before ?? (piece.space_before ? " " : "");
|
|
72603
|
+
out += piece.text;
|
|
72604
|
+
continue;
|
|
72605
|
+
}
|
|
72418
72606
|
if (!(piece.text === "::" || piece.text === "." || piece.text === "-" || pieces[i - 1].text === "::" || pieces[i - 1].text === "." || pieces[i - 1].text === "-")) out += " ";
|
|
72419
72607
|
out += piece.text;
|
|
72420
72608
|
}
|
|
@@ -74437,17 +74625,202 @@ function parse_match_branch(status) {
|
|
|
74437
74625
|
}
|
|
74438
74626
|
//#endregion
|
|
74439
74627
|
//#region ../src/parse/parse_string_interpolation.ts
|
|
74628
|
+
/** A format spec token emitted by the tokenizer: the `:` plus the raw spec
|
|
74629
|
+
* text (`:.2`, `:>8`, `:08x` — or invalid text, which the strict grammar
|
|
74630
|
+
* below reports). The tokenizer splits on ANY top-level `:` (none can occur
|
|
74631
|
+
* in an ordinary expression; `::` namespace separators are excluded), so
|
|
74632
|
+
* this accepts any colon-leading token, including a bare `:`. */
|
|
74633
|
+
const FORMAT_SPEC_TOKEN = /^:/;
|
|
74634
|
+
/** Strict grammar walk over the raw spec text (without the `:`). A spec
|
|
74635
|
+
* containing `#` or `,` tries the simple grammar first, then the custom
|
|
74636
|
+
* pattern grammar; everything else reports the simple-grammar failure. */
|
|
74637
|
+
function parse_format_spec(text, start, status) {
|
|
74638
|
+
const simple = parse_simple_spec(text, start);
|
|
74639
|
+
if (simple.spec) return simple.spec;
|
|
74640
|
+
if (!simple.structural) {
|
|
74641
|
+
add_error(status, simple.error ?? "empty format spec", start);
|
|
74642
|
+
return;
|
|
74643
|
+
}
|
|
74644
|
+
const pattern = parse_pattern_spec(text, start);
|
|
74645
|
+
if (pattern.spec) return pattern.spec;
|
|
74646
|
+
if (/[#,;%'"]/.test(text) || text.includes("E")) add_error(status, pattern.error ?? "empty format spec", start);
|
|
74647
|
+
else add_error(status, simple.error ?? "empty format spec", start);
|
|
74648
|
+
}
|
|
74649
|
+
/** The simple spec grammar: `[fill][width][.precision][mode]`. Returns the
|
|
74650
|
+
* spec, or an error plus whether the failure was STRUCTURAL (the text
|
|
74651
|
+
* isn't simple-shaped at all — a custom pattern may still apply) versus a
|
|
74652
|
+
* combination rule the simple grammar rejects outright. */
|
|
74653
|
+
function parse_simple_spec(text, start) {
|
|
74654
|
+
const spec = {
|
|
74655
|
+
text,
|
|
74656
|
+
start
|
|
74657
|
+
};
|
|
74658
|
+
let i = 0;
|
|
74659
|
+
let filled = false;
|
|
74660
|
+
const read_digits = () => {
|
|
74661
|
+
const begin = i;
|
|
74662
|
+
while (i < text.length && text[i] >= "0" && text[i] <= "9") i += 1;
|
|
74663
|
+
if (i === begin) return void 0;
|
|
74664
|
+
return parseInt(text.substring(begin, i), 10);
|
|
74665
|
+
};
|
|
74666
|
+
if (text[i] === "<" || text[i] === ">") {
|
|
74667
|
+
spec.align = text[i] === "<" ? "left" : "right";
|
|
74668
|
+
i += 1;
|
|
74669
|
+
filled = true;
|
|
74670
|
+
const width = read_digits();
|
|
74671
|
+
if (width === void 0) return { error: `invalid format spec ':${text}': expected width digits after '${spec.align === "left" ? "<" : ">"}'` };
|
|
74672
|
+
spec.width = width;
|
|
74673
|
+
} else if (text[i] === "0") {
|
|
74674
|
+
spec.zero = true;
|
|
74675
|
+
i += 1;
|
|
74676
|
+
filled = true;
|
|
74677
|
+
const width = read_digits();
|
|
74678
|
+
if (width === void 0) return {
|
|
74679
|
+
error: `invalid format spec ':${text}': expected width digits after '0'`,
|
|
74680
|
+
structural: true
|
|
74681
|
+
};
|
|
74682
|
+
spec.width = width;
|
|
74683
|
+
}
|
|
74684
|
+
if (spec.width === void 0) {
|
|
74685
|
+
const width = read_digits();
|
|
74686
|
+
if (width !== void 0) {
|
|
74687
|
+
spec.width = width;
|
|
74688
|
+
filled = true;
|
|
74689
|
+
}
|
|
74690
|
+
}
|
|
74691
|
+
if (text[i] === ".") {
|
|
74692
|
+
i += 1;
|
|
74693
|
+
const precision = read_digits();
|
|
74694
|
+
if (precision === void 0) return {
|
|
74695
|
+
error: `invalid format spec ':${text}': expected digits after '.'`,
|
|
74696
|
+
structural: true
|
|
74697
|
+
};
|
|
74698
|
+
spec.precision = precision;
|
|
74699
|
+
filled = true;
|
|
74700
|
+
}
|
|
74701
|
+
const BASE_LETTERS = "xXbo";
|
|
74702
|
+
if (i < text.length && "xXbonNeE".includes(text[i]) && text.length - i === 1) {
|
|
74703
|
+
spec.base = text[i];
|
|
74704
|
+
i += 1;
|
|
74705
|
+
filled = true;
|
|
74706
|
+
}
|
|
74707
|
+
if (i < text.length) return {
|
|
74708
|
+
error: `invalid format spec ':${text}': unexpected character '${text[i]}'`,
|
|
74709
|
+
structural: true
|
|
74710
|
+
};
|
|
74711
|
+
if (!filled) return {
|
|
74712
|
+
error: "empty format spec",
|
|
74713
|
+
structural: true
|
|
74714
|
+
};
|
|
74715
|
+
if (spec.precision !== void 0 && spec.base && BASE_LETTERS.includes(spec.base)) return { error: `invalid format spec ':${text}': precision and base cannot combine` };
|
|
74716
|
+
if (spec.zero && spec.base && (spec.base === "n" || spec.base === "N")) return { error: `invalid format spec ':${text}': zero-padding cannot combine with grouping` };
|
|
74717
|
+
if (spec.width === 0) return { error: `invalid format spec ':${text}': width must be at least 1` };
|
|
74718
|
+
return { spec };
|
|
74719
|
+
}
|
|
74720
|
+
/** The custom-pattern grammar (C#-style subset — see FormatSpec). An
|
|
74721
|
+
* optional `<N`/`>N` alignment prefix composes; the rest is the pattern,
|
|
74722
|
+
* validated section by section. Returns the spec or the error message. */
|
|
74723
|
+
function parse_pattern_spec(text, start) {
|
|
74724
|
+
const spec = {
|
|
74725
|
+
text,
|
|
74726
|
+
start
|
|
74727
|
+
};
|
|
74728
|
+
let i = 0;
|
|
74729
|
+
if (text[i] === "<" || text[i] === ">") {
|
|
74730
|
+
spec.align = text[i] === "<" ? "left" : "right";
|
|
74731
|
+
i += 1;
|
|
74732
|
+
const begin = i;
|
|
74733
|
+
while (i < text.length && text[i] >= "0" && text[i] <= "9") i += 1;
|
|
74734
|
+
if (i === begin) return { error: `invalid format spec ':${text}': expected width digits after '${spec.align === "left" ? "<" : ">"}'` };
|
|
74735
|
+
spec.width = parseInt(text.substring(begin, i), 10);
|
|
74736
|
+
if (spec.width === 0) return { error: `invalid format spec ':${text}': width must be at least 1` };
|
|
74737
|
+
}
|
|
74738
|
+
const pattern = text.substring(i);
|
|
74739
|
+
if (pattern.length === 0) return { error: "empty format spec" };
|
|
74740
|
+
const failf = (message) => {
|
|
74741
|
+
return { error: `invalid format spec ':${text}': ${message}` };
|
|
74742
|
+
};
|
|
74743
|
+
let quoted = false;
|
|
74744
|
+
let sections = 1;
|
|
74745
|
+
let section_has_slot = false;
|
|
74746
|
+
let section_dot = false;
|
|
74747
|
+
let slots_since_comma = -1;
|
|
74748
|
+
for (const c of pattern) {
|
|
74749
|
+
if (quoted) {
|
|
74750
|
+
if (c === "'") quoted = false;
|
|
74751
|
+
continue;
|
|
74752
|
+
}
|
|
74753
|
+
if (c === "'") {
|
|
74754
|
+
quoted = true;
|
|
74755
|
+
continue;
|
|
74756
|
+
}
|
|
74757
|
+
if (c === ";") {
|
|
74758
|
+
if (!section_has_slot) return failf("each section needs at least one digit placeholder ('0' or '#')");
|
|
74759
|
+
if (slots_since_comma === 0) return failf("group commas must sit between digit placeholders");
|
|
74760
|
+
sections += 1;
|
|
74761
|
+
if (sections > 2) return failf("at most two ';'-separated sections");
|
|
74762
|
+
section_has_slot = false;
|
|
74763
|
+
section_dot = false;
|
|
74764
|
+
slots_since_comma = -1;
|
|
74765
|
+
continue;
|
|
74766
|
+
}
|
|
74767
|
+
if (c === "E" || c === "e") return failf("exponential patterns are not supported — use the ':e' spec");
|
|
74768
|
+
if (c === ".") {
|
|
74769
|
+
if (section_dot) return failf("at most one '.' per section");
|
|
74770
|
+
section_dot = true;
|
|
74771
|
+
slots_since_comma = -1;
|
|
74772
|
+
continue;
|
|
74773
|
+
}
|
|
74774
|
+
if (c === "0" || c === "#") {
|
|
74775
|
+
section_has_slot = true;
|
|
74776
|
+
if (slots_since_comma >= 0) slots_since_comma += 1;
|
|
74777
|
+
continue;
|
|
74778
|
+
}
|
|
74779
|
+
if (c === ",") {
|
|
74780
|
+
if (!section_has_slot || section_dot || slots_since_comma === 0) return failf("group commas must sit between digit placeholders");
|
|
74781
|
+
slots_since_comma = 0;
|
|
74782
|
+
continue;
|
|
74783
|
+
}
|
|
74784
|
+
}
|
|
74785
|
+
if (quoted) return failf("unbalanced pattern quotes");
|
|
74786
|
+
if (!section_has_slot) return failf("each section needs at least one digit placeholder ('0' or '#')");
|
|
74787
|
+
if (slots_since_comma === 0) return failf("group commas must sit between digit placeholders");
|
|
74788
|
+
spec.pattern = pattern;
|
|
74789
|
+
return { spec };
|
|
74790
|
+
}
|
|
74440
74791
|
function parse_string_interpolation(status) {
|
|
74441
74792
|
const start = get_index(status);
|
|
74442
74793
|
let pattern = consume(status);
|
|
74443
74794
|
let values = [];
|
|
74795
|
+
let specs = [];
|
|
74796
|
+
let has_spec = false;
|
|
74444
74797
|
while (true) {
|
|
74445
74798
|
const value = consume(status);
|
|
74446
74799
|
if (value === "\\{") {
|
|
74447
74800
|
pattern += "\\{}";
|
|
74801
|
+
const peeked = status.tokens[status.i];
|
|
74802
|
+
if (peeked && FORMAT_SPEC_TOKEN.test(peeked.value)) {
|
|
74803
|
+
add_error(status, `format spec '${peeked.value}' requires an expression before ':'`, peeked.i);
|
|
74804
|
+
while (status.i < status.tokens.length && status.tokens[status.i].value !== "}") status.i += 1;
|
|
74805
|
+
accept("}", status);
|
|
74806
|
+
values.push(new ValueNode(start, "\"\"", new Type("string", true)));
|
|
74807
|
+
specs.push(null);
|
|
74808
|
+
continue;
|
|
74809
|
+
}
|
|
74448
74810
|
let param = parse_expression(status);
|
|
74811
|
+
let spec = null;
|
|
74812
|
+
const spec_token = status.tokens[status.i];
|
|
74813
|
+
if (spec_token && FORMAT_SPEC_TOKEN.test(spec_token.value)) {
|
|
74814
|
+
status.i += 1;
|
|
74815
|
+
const parsed = parse_format_spec(spec_token.value.substring(1), spec_token.i, status);
|
|
74816
|
+
if (parsed) {
|
|
74817
|
+
spec = parsed;
|
|
74818
|
+
has_spec = true;
|
|
74819
|
+
}
|
|
74820
|
+
}
|
|
74449
74821
|
if (!(param.node_type === "access" && param.access.node_type === "access_func" && param.access.name === "to_string")) param = new AccessNode(start, param, new AccessFunctionCallNode(start, "to_string"));
|
|
74450
74822
|
values.push(param);
|
|
74823
|
+
specs.push(spec);
|
|
74451
74824
|
accept("}", status);
|
|
74452
74825
|
} else if (value.endsWith("\"")) {
|
|
74453
74826
|
pattern += value;
|
|
@@ -74455,7 +74828,9 @@ function parse_string_interpolation(status) {
|
|
|
74455
74828
|
} else pattern += value;
|
|
74456
74829
|
}
|
|
74457
74830
|
pattern = pattern.replaceAll("\\{}", "%s");
|
|
74458
|
-
|
|
74831
|
+
const call = new FunctionCallNode(start, `_string_interpolate_${values.length}`, new Type("string"), [new ValueNode(0, pattern, new Type("string", true)), ...values]);
|
|
74832
|
+
if (has_spec) call.format_specs = specs;
|
|
74833
|
+
return call;
|
|
74459
74834
|
}
|
|
74460
74835
|
//#endregion
|
|
74461
74836
|
//#region ../src/parse/parse_switch.ts
|
|
@@ -76188,6 +76563,7 @@ function resolve_linked_types(source, library, file_path) {
|
|
|
76188
76563
|
if (user_defined.has(token.value)) continue;
|
|
76189
76564
|
if (library.types.has(token.value) && !BASE_TYPES.includes(token.value)) needed.add(token.value);
|
|
76190
76565
|
else if (library.functions.has(token.value) && !user_declared_funcs.has(token.value)) needed.add(token.value);
|
|
76566
|
+
else if (token.value.length > 1 && token.value.startsWith(":") && !token.value.startsWith("::")) needed.add("Format");
|
|
76191
76567
|
}
|
|
76192
76568
|
if (tokens.some((t) => t.value === "async")) needed.add("Nursery");
|
|
76193
76569
|
const resolved = resolve_types_with_deps(needed, library, is_library_file ? file_path : void 0);
|
|
@@ -76599,11 +76975,18 @@ function heading_of(item) {
|
|
|
76599
76975
|
return `${item.kind} ${item.name}${generic}`;
|
|
76600
76976
|
}
|
|
76601
76977
|
function signature_of(fn) {
|
|
76602
|
-
const params = fn.params.filter((p) => !p.is_self_param).map((p) => `${render_type(p.type)}${p.name ? " " + p.name : ""}`);
|
|
76978
|
+
const params = fn.params.filter((p) => !p.is_self_param).map((p) => `${render_type(p.type)}${p.name ? " " + p.name : ""}${field_set_of(p)}`);
|
|
76603
76979
|
let sig = `${fn.name}(${params.join(", ")})`;
|
|
76604
76980
|
if (fn.return_type?.name) sig += ` -> ${render_type(fn.return_type)}`;
|
|
76605
76981
|
return sig;
|
|
76606
76982
|
}
|
|
76983
|
+
/** The declared field set suffix (FIELD_EFFECTS.md): `{}` for "displaces
|
|
76984
|
+
* nothing", `{ a, b }` for a named set, nothing for a wildcard. */
|
|
76985
|
+
function field_set_of(param) {
|
|
76986
|
+
if (param.field_set === void 0) return "";
|
|
76987
|
+
if (param.field_set.length === 0) return " {}";
|
|
76988
|
+
return ` { ${param.field_set.join(", ")} }`;
|
|
76989
|
+
}
|
|
76607
76990
|
function members_of(node) {
|
|
76608
76991
|
if (node.node_type === "struct" || node.node_type === "trait") return node.functions.filter((f) => f.visibility === "pub" && !f.name.startsWith("#"));
|
|
76609
76992
|
return [];
|
package/package.json
CHANGED
package/src/docs.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type BaseNode from "../../src/nodes/BaseNode.ts";
|
|
|
6
6
|
import type BitsetNode from "../../src/nodes/BitsetNode.ts";
|
|
7
7
|
import type EnumNode from "../../src/nodes/EnumNode.ts";
|
|
8
8
|
import type FunctionNode from "../../src/nodes/FunctionNode.ts";
|
|
9
|
+
import type ParameterNode from "../../src/nodes/ParameterNode.ts";
|
|
9
10
|
import type StructNode from "../../src/nodes/StructNode.ts";
|
|
10
11
|
import type TraitNode from "../../src/nodes/TraitNode.ts";
|
|
11
12
|
import type Type from "../../src/nodes/Type.ts";
|
|
@@ -231,12 +232,20 @@ function heading_of(item: DocItem): string {
|
|
|
231
232
|
function signature_of(fn: FunctionNode): string {
|
|
232
233
|
const params = fn.params
|
|
233
234
|
.filter((p) => !p.is_self_param)
|
|
234
|
-
.map((p) => `${render_type(p.type)}${p.name ? " " + p.name : ""}`);
|
|
235
|
+
.map((p) => `${render_type(p.type)}${p.name ? " " + p.name : ""}${field_set_of(p)}`);
|
|
235
236
|
let sig = `${fn.name}(${params.join(", ")})`;
|
|
236
237
|
if (fn.return_type?.name) sig += ` -> ${render_type(fn.return_type)}`;
|
|
237
238
|
return sig;
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/** The declared field set suffix (FIELD_EFFECTS.md): `{}` for "displaces
|
|
242
|
+
* nothing", `{ a, b }` for a named set, nothing for a wildcard. */
|
|
243
|
+
function field_set_of(param: ParameterNode): string {
|
|
244
|
+
if (param.field_set === undefined) return "";
|
|
245
|
+
if (param.field_set.length === 0) return " {}";
|
|
246
|
+
return ` { ${param.field_set.join(", ")} }`;
|
|
247
|
+
}
|
|
248
|
+
|
|
240
249
|
function members_of(node: BaseNode): FunctionNode[] {
|
|
241
250
|
if (node.node_type === "struct" || node.node_type === "trait") {
|
|
242
251
|
const n = node as unknown as { functions: FunctionNode[] };
|