fix(lang): 宽度后缀支持任意位宽并修复数字分隔符/进制解析

1) _read_number 只在 '_' 后接该进制合法数字时吞下划线,'_u8' 等后缀不再被数字循环吃掉;_read_width_suffix 取消 1..64 限制,只保留 int 溢出保护。2) 负的显式宽度字面量在 parser 折叠符号,-64_i7 / -128_i8 不再先按正幅值报越界。3) atoll/atof 支持 0b/0o 前缀与 '_' 分隔符,修复 1_000 被解析成 1、0b1010 被解析成 0 的问题。
This commit is contained in:
TermiNexus committed 2026-10-28 14:42:06 +08:00
1 parent 52face702b
commit ee2a9f0832
3 files changed
+68 -38

No files matched your search

+27 -12
View File
@@ -584,6 +584,20 @@ def _read_name(lx: Lexer*):
# Number Handling
# ============================================================
# _digit_sep - 下划线分隔符:仅当后面紧跟该进制的合法数字时才算分隔符。
# 这样 `128_u8` 的 `_` 会留给 `_read_width_suffix`(宽度后缀),而 `1_000` 仍按分隔符处理。
def _digit_sep(lx: Lexer*, base: int) -> int:
"""1 when src[pos]=='_' and the next char is a digit of base (digit separator)."""
if lx.pos >= lx.len: return 0
if lx.src[lx.pos] != '_': return 0
if lx.pos + 1 >= lx.len: return 0
ch: char = lx.src[lx.pos + 1]
if base == 16: return 1 if _is_hex(ch) != 0 else 0
if base == 8: return 1 if ch >= '0' and ch <= '7' else 0
if base == 2: return 1 if ch == '0' or ch == '1' else 0
return 1 if _is_digit(ch) != 0 else 0
def _read_number(lx: Lexer*):
start: sizet = lx.pos
start_lineno: int = lx.lineno
@@ -600,7 +614,7 @@ def _read_number(lx: Lexer*):
lx.col += 2
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_hex(ch) or ch == '_':
if _is_hex(ch) != 0 or _digit_sep(lx, 16) != 0:
lx.pos += 1
lx.col += 1
else:
@@ -612,10 +626,7 @@ def _read_number(lx: Lexer*):
lx.col += 2
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if ch >= '0' and ch <= '7':
lx.pos += 1
lx.col += 1
elif ch == '_':
if (ch >= '0' and ch <= '7') or _digit_sep(lx, 8) != 0:
lx.pos += 1
lx.col += 1
else:
@@ -627,7 +638,7 @@ def _read_number(lx: Lexer*):
lx.col += 2
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if ch == '0' or ch == '1' or ch == '_':
if ch == '0' or ch == '1' or _digit_sep(lx, 2) != 0:
lx.pos += 1
lx.col += 1
else:
@@ -638,7 +649,7 @@ def _read_number(lx: Lexer*):
# Decimal integer/float
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_digit(ch) or ch == '_':
if _is_digit(ch) != 0 or _digit_sep(lx, 10) != 0:
lx.pos += 1
lx.col += 1
else:
@@ -651,7 +662,7 @@ def _read_number(lx: Lexer*):
lx.col += 1
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_digit(ch) or ch == '_':
if _is_digit(ch) != 0 or _digit_sep(lx, 10) != 0:
lx.pos += 1
lx.col += 1
else:
@@ -671,7 +682,7 @@ def _read_number(lx: Lexer*):
lx.col += 1
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_digit(ch) or ch == '_':
if _is_digit(ch) != 0 or _digit_sep(lx, 10) != 0:
lx.pos += 1
lx.col += 1
else:
@@ -696,7 +707,8 @@ def _suffix_is(lx: Lexer*, p: sizet, s: str, n: int) -> int:
# _read_width_suffix - 解析整型字面量的显式宽度后缀(_int<N>/_uint<N>/_i<N>/_u<N>)。
# 命中则推进 lx.pos/col,返回 +N(有符号)/ -N(无符号);未命中返回 0 且不动位置。
# N 取 1..64(字面量值本身是 64 位);后缀之后不得直接跟标识符字符。
# 语言层不限制 N(LLVM/MLIR 支持任意位宽);仅防止 int 溢出(超出 int32 的宽度视为无效后缀)。
# 后缀之后不得直接跟标识符字符。
def _read_width_suffix(lx: Lexer*) -> int:
"""Explicit integer width suffix: +N signed / -N unsigned / 0 none (advances on hit)."""
if lx.pos >= lx.len: return 0
@@ -720,10 +732,13 @@ def _read_width_suffix(lx: Lexer*) -> int:
while p < lx.len:
ch: char = lx.src[p]
if ch < '0' or ch > '9': break
nbits = nbits * 10 + (int(ch) - 48)
d: int = int(ch) - 48
# Overflow guard only: widths are otherwise unrestricted (LLVM/MLIR support iN for any N).
if nbits > (2147483647 - d) / 10: return 0
nbits = nbits * 10 + d
ndig += 1
p += 1
if ndig == 0 or nbits <= 0 or nbits > 64: return 0
if ndig == 0 or nbits <= 0: return 0
if p < lx.len:
ch2: char = lx.src[p]
if (ch2 >= '0' and ch2 <= '9') or (ch2 >= 'a' and ch2 <= 'z') \
+10
View File
@@ -2279,6 +2279,16 @@ def _parse_factor(ps: Parser*) -> AST*:
_record_neg_literal_overflow(_cur_str(ps), lineno, col)
ps.error_count += 1
return None
# Explicit-width negative literal: fold the sign into the constant so `-64_i7` /
# `-128_i8` are range-checked as negative values instead of a positive magnitude.
if op_token == TokOp.Minus and _cur_type(ps) == TokenType.Number \
and ps.cur.is_float == 0 and ps.cur.int_width > 0:
w_tok: Token* = ps.cur
_advance(ps)
neg_node: AST* = _new_const_int(ps.pool, -w_tok.int_val, lineno, col,
w_tok.int_width, w_tok.int_unsigned)
_set_pos(neg_node, lineno, col, 0, 0)
return neg_node
operand: AST* = _parse_factor(ps)
ast_op: int = OpKind.UAdd
if op_token == TokOp.Minus:
+31 -26
View File
@@ -298,30 +298,31 @@ def atoll(src: str) -> int64:
s += 1
elif s[0] == '+':
s += 1
# Hex prefix 0x / 0X
is_hex: int = 0
if s[0] == '0' and (s[1] == 'x' or s[1] == 'X'):
is_hex = 1
s += 2
if is_hex:
for ch in s:
d: int = 0
if ch >= '0' and ch <= '9':
d = ch - '0'
elif ch >= 'a' and ch <= 'f':
d = ch - 'a' + 10
elif ch >= 'A' and ch <= 'F':
d = ch - 'A' + 10
else:
break
digit: int64 = int64(d)
num = num * 16 + digit
else:
for ch in s:
if ch < '0' or ch > '9': break
d: int = ch - '0'
digit: int64 = int64(d)
num = num * 10 + digit
# Radix prefixes: 0x/0X hex, 0b/0B binary, 0o/0O octal (language literals).
base: int64 = 10
if s[0] == '0':
c1: char = s[1]
if c1 == 'x' or c1 == 'X':
base = 16
s += 2
elif c1 == 'b' or c1 == 'B':
base = 2
s += 2
elif c1 == 'o' or c1 == 'O':
base = 8
s += 2
for ch in s:
if ch == '_':
continue
d: int = -1
if ch >= '0' and ch <= '9':
d = ch - '0'
elif ch >= 'a' and ch <= 'f':
d = ch - 'a' + 10
elif ch >= 'A' and ch <= 'F':
d = ch - 'A' + 10
if d < 0 or int64(d) >= base: break
num = num * base + int64(d)
return num * sign
@@ -498,6 +499,9 @@ def atof(src: str) -> double:
rounded = 1
elif ch == '.':
seen_dot = 1
elif ch == '_':
# Digit separator inside a numeric literal: ignore it.
pass
else:
break
s += 1
@@ -517,8 +521,9 @@ def atof(src: str) -> double:
s += 1
elif s[0] == '+':
s += 1
while s[0] >= '0' and s[0] <= '9':
exp = exp * 10 + (s[0] - '0')
while (s[0] >= '0' and s[0] <= '9') or s[0] == '_':
if s[0] != '_':
exp = exp * 10 + (s[0] - '0')
s += 1
exp = exp * exp_sign
p: int = dexp + exp