Skip to content
← Back

src/syntax/process/numeric_literal_classifier.ghul

1
namespace Syntax.Process is
2
use System.Reflection.Metadata.ILOpCode
3
use Logging
4
use Source.LOCATION
5
6
use Semantic.Lookups.InnateSymbolLookup
7
use Semantic.Types.Type
8
9
use IR.Values
10
11
// Classify a parsed integer literal: pick the target type from
12
// any trailing type-suffix (`b`/`s`/`i`/`l`/`w`/`c`, optionally
13
// prefixed by `u` for unsigned or `s` for sign-without-width),
14
// strip the suffix from the value string, and range-check the
15
// result against the chosen type. The lexer accepts arbitrarily
16
// long digit strings and any suffix combination; this is where
17
// suffix → type and value → fits-in-type get decided.
18
//
19
// Bounds errors and "character literals cannot be unsigned"
20
// structural errors are logged via the constructor-supplied
21
// logger. Callers receive a non-null `Literal.NUMBER` on success
22
// and null on either kind of failure.
23
//
24
// The `fits` / `parse_hex` / `max_unsigned_for` helpers are
25
// exposed for unit testing in isolation from the suffix path.
26
class NUMERIC_LITERAL_CLASSIFIER is
27
_logger: Logger
28
_innate_symbol_lookup: InnateSymbolLookup
29
30
init(logger: Logger, innate_symbol_lookup: InnateSymbolLookup) is
31
super.init()
32
33
_logger = logger
34
_innate_symbol_lookup = innate_symbol_lookup
35
si
36
37
// How many trailing characters of an integer literal are its
38
// type suffix.
39
//
40
// The digits are scanned maximally in the literal's own radix,
41
// and the suffix is whatever is left. That is what keeps `b`
42
// and `c` - hex digits as well as size selectors - reading as
43
// digits in a hex literal, so `0x20AC` is 8364 rather than
44
// 0x20A as a char. A backtick attaches a suffix that would
45
// otherwise be read as a digit, and is counted as part of the
46
// suffix so that stripping it yields the value.
47
//
48
// Every reader of a given token has to resolve this the same
49
// way, so it is the one place the rule lives.
50
suffix_length(value_string: string) -> int static is
51
if value_string.length == 0 then
52
return 0
53
fi
54
55
let tick = value_string.index_of('`')
56
57
if tick >= 0 then
58
return value_string.length - tick
59
fi
60
61
// A sign belongs to the value rather than to either end of
62
// it, and a caller may hand the text over with one still
63
// attached.
64
let signed = value_string[0] == '-' \/ value_string[0] == '+'
65
let start = if signed then 1 else 0 fi
66
67
let is_hex =
68
value_string.length > start + 1 /\
69
value_string[start] == '0' /\
70
(value_string[start + 1] == 'x' \/ value_string[start + 1] == 'X')
71
72
let first_digit = if is_hex then start + 2 else start fi
73
let index mut = first_digit
74
75
while index < value_string.length /\ is_digit(value_string[index], is_hex) do
76
index = index + 1
77
od
78
79
return value_string.length - index
80
si
81
82
// Whether `c` is a digit of the literal's radix. `_` never
83
// reaches here - the lexer drops digit-group separators before
84
// the token text is built.
85
is_digit(c: char, is_hex: bool) -> bool static =>
86
(c >= '0' /\ c <= '9') \/
87
(is_hex /\ ((c >= 'a' /\ c <= 'f') \/ (c >= 'A' /\ c <= 'F')))
88
89
// The literal with its type suffix removed, which is the value
90
// it denotes and the string every back end should read.
91
strip_suffix(value_string: string) -> string static =>
92
value_string.substring(0, value_string.length - suffix_length(value_string))
93
94
// The value an integer literal denotes. Every scalar suffix
95
// yields a `Literal.NUMBER`; `n` yields a `Literal.BIGINT`,
96
// which has no constant to load and is built at run time.
97
classify(location: LOCATION, value_string: string) -> Value? =>
98
if is_bigint(value_string) then
99
classify_bigint(location, value_string)
100
else
101
classify_integer(location, value_string)
102
fi
103
104
// Whether the literal carries the `n` suffix. The size selector
105
// is always the last character of the suffix, so this is the
106
// whole of the test.
107
is_bigint(value_string: string) -> bool static is
108
if suffix_length(value_string) == 0 then
109
return false
110
fi
111
112
let last = value_string[value_string.length - 1]
113
114
return last == 'n' \/ last == 'N'
115
si
116
117
// The selectors of a suffixed literal, with any attaching
118
// backtick dropped - it marks the boundary and says nothing
119
// about the type. Empty when the backtick had nothing after it,
120
// which the lexer has already reported.
121
selectors(value_string: string) -> string static is
122
let cutoff = value_string.length - suffix_length(value_string)
123
124
if value_string[cutoff] == '`' then
125
return value_string.substring(cutoff + 1, value_string.length - cutoff - 1)
126
fi
127
128
return value_string.substring(cutoff, value_string.length - cutoff)
129
si
130
131
classify_bigint(location: LOCATION, value_string: string) -> Literal.BIGINT? is
132
let selected = selectors(value_string)
133
134
if selected.length == 0 then
135
return null
136
fi
137
138
if "uU".contains(selected[0]) then
139
_logger.error(location, "bigint literal cannot be unsigned")
140
return null
141
fi
142
143
let working = strip_suffix(value_string)
144
145
let (ok, digits) = decimal_magnitude(working)
146
147
if !ok then
148
_logger.error(location, "invalid bigint literal {working}")
149
return null
150
fi
151
152
let as_long: long mut = 0l
153
let fits_long = fits_in_long(digits) /\ long.try_parse(digits, as_long ref)
154
155
return Literal.BIGINT(digits, _innate_symbol_lookup.get_bigint_type(), as_long, fits_long)
156
si
157
158
// Largest magnitude a `long` holds, as the digit string
159
// `fits_in_long` compares against.
160
LONG_MAX_DIGITS: string static => "9223372036854775807"
161
162
// A bigint literal has no width to overflow, so its magnitude is
163
// carried as digits rather than range-checked. `long` is only
164
// the shorter of the two ways to build one at run time, so
165
// whether the value fits is a question about which instructions
166
// to emit rather than about whether the literal is legal.
167
fits_in_long(digits: string) -> bool =>
168
digits.length < LONG_MAX_DIGITS.length \/
169
(digits.length == LONG_MAX_DIGITS.length /\ string.compare_ordinal(digits, LONG_MAX_DIGITS) <= 0)
170
171
// The literal's magnitude in decimal digits, with no leading
172
// zeros. A decimal literal already is that, once its zeros are
173
// dropped; a hex one is accumulated a digit at a time, because
174
// the value has no bound to parse it into.
175
decimal_magnitude(value_string: string) -> (ok: bool, digits: string) is
176
let is_hex = value_string.starts_with("0x") \/ value_string.starts_with("0X")
177
let source = if is_hex then value_string.substring(2, value_string.length - 2) else value_string fi
178
179
if source.length == 0 then
180
return (false, "0")
181
fi
182
183
// Least significant first, one decimal digit each, so a
184
// carry appends rather than shifting the whole number.
185
let accumulator = Collections.LIST[int]([0])
186
let radix = if is_hex then 16 else 10 fi
187
188
for c in source do
189
let digit: int mut
190
191
if c >= '0' /\ c <= '9' then
192
digit = cast int(c) - cast int('0')
193
elif is_hex /\ c >= 'a' /\ c <= 'f' then
194
digit = cast int(c) - cast int('a') + 10
195
elif is_hex /\ c >= 'A' /\ c <= 'F' then
196
digit = cast int(c) - cast int('A') + 10
197
else
198
return (false, "0")
199
fi
200
201
let carry mut = digit
202
203
for i in 0..accumulator.count do
204
let product = accumulator[i] * radix + carry
205
accumulator[i] = product % 10
206
carry = product / 10
207
od
208
209
while carry > 0 do
210
accumulator.add(carry % 10)
211
carry = carry / 10
212
od
213
od
214
215
let buffer = System.Text.StringBuilder(accumulator.count)
216
let top mut = accumulator.count - 1
217
218
while top > 0 /\ accumulator[top] == 0 do
219
top = top - 1
220
od
221
222
for i in 0::top do
223
buffer.append(cast char(cast int('0') + accumulator[top - i]))
224
od
225
226
return (true, buffer.to_string())
227
si
228
229
classify_integer(location: LOCATION, value_string: string) -> Literal.NUMBER? is
230
let type: Type? mut = null
231
let is_wide mut = false
232
let conv: ILOpCode? mut = null
233
let working mut = value_string
234
235
let suffix = suffix_length(working)
236
237
if suffix > 0 then
238
let cutoff = working.length - suffix
239
240
// The selectors themselves, with any attaching backtick
241
// dropped - it marks the boundary and says nothing about
242
// the type.
243
let selectors =
244
if working[cutoff] == '`' then
245
working.substring(cutoff + 1, suffix - 1)
246
else
247
working.substring(cutoff, suffix)
248
fi
249
250
// A backtick with nothing after it. The lexer has already
251
// reported it; failing quietly here keeps it to one
252
// diagnostic.
253
if selectors.length == 0 then
254
return null
255
fi
256
257
let last_char = selectors[selectors.length - 1]
258
let is_unsigned = "uU".contains(selectors[0])
259
260
case last_char
261
when 'b', 'B' then
262
if is_unsigned then
263
type = _innate_symbol_lookup.get_ubyte_type()
264
else
265
type = _innate_symbol_lookup.get_byte_type()
266
fi
267
268
when 'c', 'C' then
269
if is_unsigned then
270
_logger.error(location, "numeric character literal cannot be unsigned")
271
else
272
type = _innate_symbol_lookup.get_char_type()
273
fi
274
275
when 's', 'S' then
276
if is_unsigned then
277
type = _innate_symbol_lookup.get_ushort_type()
278
else
279
type = _innate_symbol_lookup.get_short_type()
280
fi
281
282
when 'i', 'I' then
283
if is_unsigned then
284
type = _innate_symbol_lookup.get_uint_type()
285
else
286
type = _innate_symbol_lookup.get_int_type()
287
fi
288
289
when 'l', 'L' then
290
is_wide = true
291
292
if is_unsigned then
293
type = _innate_symbol_lookup.get_ulong_type()
294
else
295
type = _innate_symbol_lookup.get_long_type()
296
fi
297
298
when 'w', 'W' then
299
is_wide = true
300
301
if is_unsigned then
302
type = _innate_symbol_lookup.get_uword_type()
303
conv = ILOpCode.CONV_U
304
else
305
type = _innate_symbol_lookup.get_word_type()
306
conv = ILOpCode.CONV_I
307
fi
308
309
else
310
if is_unsigned then
311
type = _innate_symbol_lookup.get_uint_type()
312
else
313
type = _innate_symbol_lookup.get_int_type()
314
fi
315
esac
316
317
working = working.substring(0, cutoff)
318
else
319
type = _innate_symbol_lookup.get_int_type()
320
fi
321
322
if !type? then
323
return null
324
fi
325
326
let (ok, parsed) = parse_magnitude(working)
327
328
if !ok \/ !fits(location, working, type) then
329
return null
330
fi
331
332
let constant =
333
if is_wide then
334
Literal.CONSTANT.I8(cast long(parsed))
335
else
336
Literal.CONSTANT.I4(cast int(cast uint(parsed)))
337
fi
338
339
return Literal.NUMBER(constant, type, conv)
340
si
341
342
// The magnitude a literal denotes, whatever radix it is written
343
// in. Both the range check and the constant the literal loads
344
// need it, so it is read once here rather than each reading the
345
// spelling for itself.
346
parse_magnitude(value_string: string) -> (ok: bool, value: ulong) =>
347
if value_string.starts_with("0x") \/ value_string.starts_with("0X") then
348
parse_hex(value_string.substring(2, value_string.length - 2))
349
else
350
(
351
let v: ulong mut = _
352
let parsed_ok = ulong.try_parse(value_string, v ref)
353
(parsed_ok, v)
354
)
355
fi
356
357
// Range-check a parsed integer literal against its target
358
// type. Comparing as a ulong using each type's unsigned max
359
// preserves the standard "literal fits in N unsigned bits,
360
// unary minus negates separately" convention. Without this,
361
// oversize literals reach IL emission, where the constant no
362
// longer fits the instruction it is loaded by (issue #580).
363
fits(location: LOCATION, value_string: string, type: Type) -> bool is
364
let (ok, parsed) = parse_magnitude(value_string)
365
366
if !ok then
367
_logger.error(location, "numeric literal {value_string} does not fit in {type}")
368
return false
369
fi
370
371
let max_value = max_unsigned_for(type)
372
373
if max_value > 0UL /\ parsed > max_value then
374
_logger.error(location, "numeric literal {value_string} does not fit in {type}")
375
return false
376
fi
377
378
return true
379
si
380
381
// Parse a hex digit string (no `0x` prefix) into a ulong.
382
// Returns (false, 0) for empty, > 16 digits, or any non-hex
383
// character.
384
parse_hex(s: string) -> (ok: bool, value: ulong) is
385
if s.length == 0 \/ s.length > 16 then
386
return (false, 0UL)
387
fi
388
389
let value: ulong mut = 0UL
390
391
for c in s do
392
let digit: int mut
393
394
if c >= '0' /\ c <= '9' then
395
digit = cast int(c) - cast int('0')
396
elif c >= 'a' /\ c <= 'f' then
397
digit = cast int(c) - cast int('a') + 10
398
elif c >= 'A' /\ c <= 'F' then
399
digit = cast int(c) - cast int('A') + 10
400
else
401
return (false, 0UL)
402
fi
403
404
value = (value << 4) + cast ulong(digit)
405
od
406
407
return (true, value)
408
si
409
410
// Largest non-negative literal value the type accepts, or 0
411
// to signal "no narrower bound than the ulong-parse range"
412
// (used for ulong / uword: ulong.try_parse already enforces
413
// the 64-bit ceiling). 0 makes `fits` skip the narrower-
414
// bound check.
415
max_unsigned_for(type: Type) -> ulong is
416
if type.matches(_innate_symbol_lookup.get_byte_type()) then
417
return 127UL
418
elif type.matches(_innate_symbol_lookup.get_ubyte_type()) then
419
return 255UL
420
elif type.matches(_innate_symbol_lookup.get_short_type()) then
421
return 32767UL
422
elif type.matches(_innate_symbol_lookup.get_ushort_type()) then
423
return 65535UL
424
elif type.matches(_innate_symbol_lookup.get_char_type()) then
425
return 65535UL
426
elif type.matches(_innate_symbol_lookup.get_int_type()) then
427
return 2147483647UL
428
elif type.matches(_innate_symbol_lookup.get_uint_type()) then
429
return 4294967295UL
430
elif type.matches(_innate_symbol_lookup.get_long_type()) then
431
return 9223372036854775807UL
432
elif type.matches(_innate_symbol_lookup.get_word_type()) then
433
return 9223372036854775807UL
434
fi
435
436
return 0UL
437
si
438
si
439
si