Files
Viper_VRTE/ast/lexer.vp
T
TermiNexus e71daeea0e docs(includes): R11 注释英文化——75 个文件的中文注释改写为英文短注释
按规范「注释只讲解作用,不掺杂历史原因,尽量英文」:把 includes 下 75 个 .vp 的中文 # 注释与三引号 docstring 改写为英文,每个注释块压到 2 行以内。

保留要点:作用、约束、危险警告(不得/必须/注意)、ABI 与类型约定(如 BYTEPTR | t.CPtr = i8** 的跨模块坍缩坑)、位宽/长度约定、fail-fast 要求、内存与句柄所有权。

未动:文件头注释(规范豁免,仍是中文)、运行时中文字符串常量(printf / fatal_error / MlirFail 等用户可见消息,属代码字面量)。

验证:独立脚本比对 HEAD 与工作区的代码骨架(剥离 # 注释与三引号 docstring 后的非空行)——75 个文件逐字节一致;行尾 CRLF/LF 未被改动;全文剩余中文仅 547 行头注释、0 条行尾注释。bootstrap 三重重编 SUCCESS;全量 77/77 顺序回归通过。
2026-10-05 16:18:44 +08:00

1133 lines
39 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import t, c
from stdint import *
import memhub
import string
import stdio
import stdlib
from .tokens import (
Token, TokenType, TokOp, _init_tables, _kw_lookup, _op_head,
OpEntry, new_token, token_set_str, token_set_str_literal,
)
# ============================================================
# 整数字面量溢出(词法层 fail-fast)
#
# 整数常量在 token/AST 中是 int64(tokens.vp `int_val` / exprs.vp `int_val`)。
# 旧实现用 `string.atoll`(string.vp:292)在 int64 下 `num = num * 10 + digit`
# 逐位累加 ⇒ 值 > 2^64-1 时**静默回绕**(如 2^64 回绕成 0,2^64-1 回绕成 -1)。
# 本实现在 `_emit_number` 里对**整数**字面量先做纯文本幅值判定
# (`_lex_int_exceeds_u64`,与 AssignGenMixin._int_text_exceeds_64 同口径:按
# 进制统计有效位数 / 与 2^64-1 的十进制文本做长度+字典序比较,**不落进任何定宽
# 容器、不回绕后再比较**),超出 2^64-1 即记录 fail-fast(不再静默回绕)。
# 合法范围 0..2^64-1 仍按 64 位位模式存入 int64(既有合法路径的产物形态不变,
# 如 0xFFFFFFFFFFFFFFFF → int64 -1、0xA5A5A5A5A5A5A5A5 保持其 64 位位模式)。
# 各进制(2/8/10/16)口径与 TPC(Python int(x, 0),任意精度)一致:接受区间同为
# [-2^63, 2^64-1](字面量本身无符号,负数侧由一元减表达),超出即拒绝。
#
# 上报通道:lexer 无独立 fatal 出口,故记录到模块级三元组(`_lex_record_overflow`),
# 由 parser 的 parse_tokens 取出后转写 _g_parse_err_*(位置 + 消息),复用既有
# Traceback/SyntaxError 报告路径("文件:行:列 + 源码行"由该路径补全并终止编译)。
# 这覆盖全部源码字面量(文件级解析必经 parse_tokens);parse_expression 只用于编译器
# 合成的短类型串,不承载源码字面量,故不另设通道。
# ============================================================
_g_lex_err_line: int = 0
_g_lex_err_col: int = 0
_g_lex_err_msg: str = None
def get_lex_err_line() -> int:
"""Line number of the latest lexer fail-fast (0 = none).
Consumed by parser.parse_tokens and forwarded to the parse error channel.
"""
return _g_lex_err_line
def get_lex_err_col() -> int:
"""Column of the latest lexer fail-fast (0-based; -1 = no column info)"""
return _g_lex_err_col
def get_lex_err_msg() -> str:
"""Message of the latest lexer fail-fast (literal / required width / limit / fix hint)"""
return _g_lex_err_msg
def _lex_reset_err():
"""Clear the lexer fail-fast triple (called at each tokenize entry to avoid stale errors)"""
global _g_lex_err_line, _g_lex_err_col, _g_lex_err_msg
_g_lex_err_line = 0
_g_lex_err_col = 0
_g_lex_err_msg = None
def _lex_record_overflow(text: str, lineno: int, col: int):
"""Record an integer literal overflow (fail-fast; first record only); message carries literal
+ required width (>= 65 bits, limit 2^64-1) + fix hint (file:line:col filled later by parse_tokens).
"""
global _g_lex_err_line, _g_lex_err_col, _g_lex_err_msg
if _g_lex_err_line != 0:
return
_g_lex_err_line = lineno
_g_lex_err_col = col
msg_buf: str = stdlib.malloc(512)
if msg_buf is not None:
stdio.snprintf(msg_buf, 512,
"[TPV] 整数字面量溢出: 字面量 '%s' 超出 64 位可表示范围"
"(按位模式解释的上限为 18446744073709551615 = 2^64-1,"
"该字面量需要的位宽至少 65 位)。"
"禁止静默回绕/截断,已终止编译。"
"修法: 把该字面量改到 0..18446744073709551615 的 64 位位模式范围内;"
"本语言不存在更宽的整数类型,如需更大数值请拆分为多个 64 位字表达。", text)
_g_lex_err_msg = msg_buf
else:
_g_lex_err_msg = text
def _lex_int_exceeds_u64(text: str) -> int:
"""True if the integer literal magnitude exceeds the 64-bit limit 2^64-1 (1 = yes, 0 = no).
Pure text compare, no wrap-around: decimal length+lexicographic vs "18446744073709551615",
0x > 16, 0o > 22 (or 22 leading > '1'), 0b > 64 (literal text carries no sign).
"""
if text is None:
return 0
tl: sizet = string.strlen(text)
if tl == 0:
return 0
# Base detection (prefix letter case-insensitive)
base: int = 10
start: sizet = 0
if tl >= 2 and text[0] == '0':
pfx: char = text[1]
if pfx == 'x' or pfx == 'X':
base = 16
start = 2
elif pfx == 'o' or pfx == 'O':
base = 8
start = 2
elif pfx == 'b' or pfx == 'B':
base = 2
start = 2
# Count significant digits (skip '_' and leading zeros) and remember the first one
sig: int = 0
first: char = '0'
j: sizet = start
while j < tl:
ch: char = text[j]
j += 1
if ch == '_':
continue
ok_digit: int = 0
if base == 16:
if (ch >= '0' and ch <= '9') or (ch >= 'a' and ch <= 'f') or (ch >= 'A' and ch <= 'F'):
ok_digit = 1
elif base == 10:
if ch >= '0' and ch <= '9':
ok_digit = 1
elif base == 8:
if ch >= '0' and ch <= '7':
ok_digit = 1
else:
if ch == '0' or ch == '1':
ok_digit = 1
if ok_digit == 0:
break
if sig == 0:
if ch == '0':
continue # leading zeros not counted
first = ch
sig += 1
if sig == 0:
return 0 # value 0
if base == 16:
return 1 if sig > 16 else 0
if base == 2:
return 1 if sig > 64 else 0
if base == 8:
if sig > 22:
return 1
if sig == 22 and first > '1':
return 1
return 0
# Decimal: compare significant-digit count against the 2^64-1 text length
mlen: sizet = 20
if sig > mlen:
return 1
if sig < mlen:
return 0
# Equal length (20 digits): lexicographic compare against '18446744073709551615'
max_text: str = "18446744073709551615"
d: sizet = 0
k: sizet = start
lead: int = 1
while k < tl and d < mlen:
c: char = text[k]
k += 1
if c == '_':
continue
if c < '0' or c > '9':
return 0
if lead != 0 and c == '0':
continue
lead = 0
mc: char = max_text[d]
if c > mc:
return 1
if c < mc:
return 0
d += 1
return 0
def _lex_mag_digit_val(ch: char, base: int) -> int:
"""Digit char -> positional value (hex accepts a-f/A-F; other bases are '0'..base-1)."""
if base == 16:
return _hex_val(ch)
return int(ch) - 48
def _lex_mag_gt_text(text: str, base: int, start: sizet, sig: int, max_text: str) -> int:
"""True if the literal's significant digits exceed the same-length max text (1 = yes).
Both sides skip '_'/leading zeros, compared by positional value (hex via _hex_val); pure text.
"""
d: sizet = 0
k: sizet = start
tl: sizet = string.strlen(text)
lead: int = 1
while k < tl and d < sizet(sig):
c: char = text[k]
k += 1
if c == '_':
continue
if base == 16:
if not ((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F')):
break
elif base == 10:
if c < '0' or c > '9':
break
elif base == 8:
if c < '0' or c > '7':
break
else:
if c != '0' and c != '1':
break
if lead != 0 and c == '0':
continue
lead = 0
cv: int = _lex_mag_digit_val(c, base)
mv: int = _lex_mag_digit_val(max_text[d], base)
if cv > mv:
return 1
if cv < mv:
return 0
d += 1
return 0
def _lex_int_neg_exceeds_i64(text: str) -> int:
"""True if the magnitude is strictly greater than 2^63 (1 = yes).
For unary-minus + literal overflow: a magnitude-valid literal may become < -2^63 after
negation; exactly 2^63 is legal (INT64_MIN). Pure text compare, no wrap-around.
"""
if text is None:
return 0
tl: sizet = string.strlen(text)
if tl == 0:
return 0
base: int = 10
start: sizet = 0
if tl >= 2 and text[0] == '0':
pfx: char = text[1]
if pfx == 'x' or pfx == 'X':
base = 16
start = 2
elif pfx == 'o' or pfx == 'O':
base = 8
start = 2
elif pfx == 'b' or pfx == 'B':
base = 2
start = 2
sig: int = 0
j: sizet = start
while j < tl:
ch: char = text[j]
j += 1
if ch == '_':
continue
ok_digit: int = 0
if base == 16:
if (ch >= '0' and ch <= '9') or (ch >= 'a' and ch <= 'f') or (ch >= 'A' and ch <= 'F'):
ok_digit = 1
elif base == 10:
if ch >= '0' and ch <= '9':
ok_digit = 1
elif base == 8:
if ch >= '0' and ch <= '7':
ok_digit = 1
else:
if ch == '0' or ch == '1':
ok_digit = 1
if ok_digit == 0:
break
if sig == 0 and ch == '0':
continue # leading zeros not counted
sig += 1
if sig == 0:
return 0 # value 0
if base == 16:
if sig > 16: return 1
if sig < 16: return 0
return _lex_mag_gt_text(text, 16, start, sig, "8000000000000000")
if base == 2:
if sig > 64: return 1
if sig < 64: return 0
return _lex_mag_gt_text(text, 2, start, sig,
"1000000000000000000000000000000000000000000000000000000000000000")
if base == 8:
if sig > 22: return 1
if sig < 22: return 0
return _lex_mag_gt_text(text, 8, start, sig, "1000000000000000000000")
if sig > 19: return 1
if sig < 19: return 0
return _lex_mag_gt_text(text, 10, start, sig, "9223372036854775808")
# Maximum Indentation Stack Depth
define LEXER_MAX_INDENT = 256
# ============================================================
# Lexer State Structure
# ============================================================
class Lexer:
src: str # Source code (NUL-terminated)
pos: sizet # Current position
len: sizet # Source code length
lineno: int # Current line number (1-based)
col: int # Current column number (0-based)
pool: rawptr[memhub.MemManager] # Memory pool
indent_stack: rawptr[int] # Indent stack (int array, allocated from pool)
indent_top: int # Stack top (-1 for empty stack)
paren_depth: int # Paren depth (>0 for no TokenType.NewLine)
tokens_head: rawptr[Token] # token list head
tokens_tail: rawptr[Token] # token list tail
at_line_start: int # 1=line start (need indent)
error_count: int # Error count
def __new__(self, pool: rawptr[memhub.MemManager]):
ptr: rawptr[Lexer] = pool.alloc(Lexer.__sizeof__())
if ptr == None:
# Pool exhausted: fail-fast instead of returning NULL (downstream would treat NULL as a valid Lexer)
stdio.printf("[AST] Lexer 分配失败:内存池已耗尽(need=%zu 字节)\n",
sizet(Lexer.__sizeof__()))
stdio.fflush(None)
stdlib.exit(1)
string.memset(ptr, 0, Lexer.__sizeof__())
return ptr
def new_lexer(pool: rawptr[memhub.MemManager]) -> rawptr[Lexer]:
"""Allocate from the pool and return a Lexer (cross-module safe wrapper)"""
return Lexer(pool)
def _lexer_init(lx: rawptr[Lexer], src: str, pool: rawptr[memhub.MemManager]):
"""Initialize Lexer"""
if lx == None: return
lx.src = src
lx.pos = 0
lx.len = string.strlen(src) if src != None else 0
lx.lineno = 1
lx.col = 0
lx.pool = pool
# Allocate indentation stack
lx.indent_stack = pool.alloc(LEXER_MAX_INDENT * 8)
if lx.indent_stack == None:
stdio.printf("[AST] Lexer.indent_stack 分配失败:内存池已耗尽(need=%zu 字节)\n",
sizet(LEXER_MAX_INDENT * 8))
stdio.fflush(None)
stdlib.exit(1)
string.memset(lx.indent_stack, 0, LEXER_MAX_INDENT * 8)
lx.indent_top = -1
# Push 0 (base indent)
lx.indent_stack[0] = 0
lx.indent_top = 0
lx.paren_depth = 0
lx.tokens_head = None
lx.tokens_tail = None
lx.at_line_start = 1
lx.error_count = 0
_init_tables(pool)
# ============================================================
# Auxiliary Functions
# ============================================================
def _peek(lx: rawptr[Lexer], offset: sizet) -> char:
"""Peek character at position pos+offset, return '\0' if out of bounds"""
p: sizet = lx.pos + offset
if p >= lx.len: return '\0'
return lx.src[p]
def _peek_cur(lx: rawptr[Lexer]) -> char:
if lx.pos >= lx.len: return '\0'
return lx.src[lx.pos]
def _advance(lx: rawptr[Lexer]) -> char:
"""Advance one character, return the character"""
if lx.pos >= lx.len: return '\0'
ch: char = lx.src[lx.pos]
lx.pos += 1
if ch == '\n':
lx.lineno += 1
lx.col = 0
else:
lx.col += 1
return ch
def _is_alpha(ch: char) -> int:
if ch >= 'a' and ch <= 'z': return 1
if ch >= 'A' and ch <= 'Z': return 1
if ch == '_': return 1
return 0
def _is_digit(ch: char) -> int:
if ch >= '0' and ch <= '9': return 1
return 0
def _is_alnum(ch: char) -> int:
if _is_alpha(ch): return 1
if _is_digit(ch): return 1
return 0
def _is_hex(ch: char) -> int:
if ch >= '0' and ch <= '9': return 1
if ch >= 'a' and ch <= 'f': return 1
if ch >= 'A' and ch <= 'F': return 1
return 0
def _hex_val(ch: char) -> int:
"""Hex char -> value 0-15 (for \xNN escape parsing)"""
if ch >= '0' and ch <= '9': return int(ch) - 48
if ch >= 'a' and ch <= 'f': return int(ch) - 87
if ch >= 'A' and ch <= 'F': return int(ch) - 55
return 0
def _is_space(ch: char) -> int:
if ch == ' ': return 1
if ch == '\t': return 1
if ch == '\f': return 1
if ch == '\v': return 1
if ch == '\r': return 1
return 0
def _emit(lx: rawptr[Lexer], tok: rawptr[Token]):
"""Append token to token list"""
if tok == None: return
if lx.tokens_head == None:
lx.tokens_head = tok
lx.tokens_tail = tok
else:
lx.tokens_tail.next = tok
lx.tokens_tail = tok
def _make_op_token(lx: rawptr[Lexer], op_id: int, length: int,
lineno: int, col: int) -> rawptr[Token]:
tok: rawptr[Token] = new_token(lx.pool, TokenType.Op, lineno, col)
if tok == None: return None
tok.op_subtype = op_id
tok.end_col_offset = col + length
return tok
# ============================================================
# Indent Handling
# ============================================================
def _handle_indent(lx: rawptr[Lexer]):
"""Handle line start indent, generate TokenType.Indent/TokenType.Dedent.
Skip empty lines and comment lines."""
# Skip empty lines and comment lines
while lx.pos < lx.len:
# Save line start position for non-empty lines
line_start_pos: sizet = lx.pos
line_start_col: int = lx.col
# Skip whitespace, not across lines
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if ch == ' ' or ch == '\t' or ch == '\f' or ch == '\v':
lx.pos += 1
lx.col += 1
elif ch == '\r':
# CRLF line end \r, skip \n to trigger empty line processing
lx.pos += 1
else:
break
if lx.pos >= lx.len:
break
ch = lx.src[lx.pos]
if ch == '\n':
# Empty line, skip
lx.pos += 1
lx.lineno += 1
lx.col = 0
continue
if ch == '\r':
# CRLF line end \r, skip \n to trigger empty line processing
lx.pos += 1
if lx.pos < lx.len and lx.src[lx.pos] == '\n':
lx.pos += 1
lx.lineno += 1
lx.col = 0
continue
if ch == '#':
# Comment line, skip to end of line
while lx.pos < lx.len and lx.src[lx.pos] != '\n':
lx.pos += 1
continue
# Non-empty line: backtrack to line start to allow indent calculation
lx.pos = line_start_pos
lx.col = line_start_col
break
if lx.pos >= lx.len:
# End of file, no indent to handle
lx.at_line_start = 0
return
# Calculate current line indent
indent: int = 0
save_pos: sizet = lx.pos
save_col: int = lx.col
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if ch == ' ':
indent += 1
lx.pos += 1
lx.col += 1
elif ch == '\t':
indent += 8 - (indent % 8)
lx.pos += 1
lx.col += 1
else:
break
# If line end (empty line), skip indent calculation
if lx.pos < lx.len:
ch = lx.src[lx.pos]
if ch == '\n' or ch == '\r' or ch == '#':
# Empty line or comment line, backtrack position to allow loop to handle indent
lx.pos = save_pos
lx.col = save_col
lx.at_line_start = 0
return
# Compare indent with top of indent stack
top_indent: int = lx.indent_stack[lx.indent_top]
if indent > top_indent:
# TokenType.Indent
if lx.indent_top + 1 >= LEXER_MAX_INDENT:
lx.error_count += 1
return
lx.indent_top += 1
lx.indent_stack[lx.indent_top] = indent
tok: rawptr[Token] = new_token(lx.pool, TokenType.Indent, lx.lineno, 0)
_emit(lx, tok)
else:
while indent < lx.indent_stack[lx.indent_top] and lx.indent_top > 0:
lx.indent_top -= 1
tok: rawptr[Token] = new_token(lx.pool, TokenType.Dedent, lx.lineno, 0)
_emit(lx, tok)
lx.at_line_start = 0
# ============================================================
# Identifier/Keyword Handling
# ============================================================
def _read_name(lx: rawptr[Lexer]):
start: sizet = lx.pos
start_lineno: int = lx.lineno
start_col: int = lx.col
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_alnum(ch):
lx.pos += 1
lx.col += 1
else:
break
length: sizet = lx.pos - start
tok: rawptr[Token] = new_token(lx.pool, TokenType.Name, start_lineno, start_col)
if tok == None: return
token_set_str(lx.pool, tok, lx.src, start, length)
tok.end_lineno = lx.lineno
tok.end_col_offset = start_col + int(length)
# Look up keyword if any
kw_id: int = _kw_lookup(tok.str_val)
if kw_id: tok.kw_subtype = kw_id
_emit(lx, tok)
# ============================================================
# Number Handling
# ============================================================
def _read_number(lx: rawptr[Lexer]):
start: sizet = lx.pos
start_lineno: int = lx.lineno
start_col: int = lx.col
is_float: int = 0
is_complex: int = 0
ch0: char = lx.src[lx.pos]
# Handle 0x/0o/0b prefix
if ch0 == '0' and lx.pos + 1 < lx.len:
ch1: char = lx.src[lx.pos + 1]
if ch1 == 'x' or ch1 == 'X':
lx.pos += 2
lx.col += 2
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_hex(ch) or ch == '_':
lx.pos += 1
lx.col += 1
else:
break
_emit_number(lx, start, start_lineno, start_col, 0, 0, 0)
return
if ch1 == 'o' or ch1 == 'O':
lx.pos += 2
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 == '_':
lx.pos += 1
lx.col += 1
else:
break
_emit_number(lx, start, start_lineno, start_col, 0, 0, 0)
return
if ch1 == 'b' or ch1 == 'B':
lx.pos += 2
lx.col += 2
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if ch == '0' or ch == '1' or ch == '_':
lx.pos += 1
lx.col += 1
else:
break
_emit_number(lx, start, start_lineno, start_col, 0, 0, 0)
return
# Decimal integer/float
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_digit(ch) or ch == '_':
lx.pos += 1
lx.col += 1
else:
break
# Decimal part
if lx.pos < lx.len and lx.src[lx.pos] == '.':
is_float = 1
lx.pos += 1
lx.col += 1
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_digit(ch) or ch == '_':
lx.pos += 1
lx.col += 1
else:
break
# Exponent part
if lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if ch == 'e' or ch == 'E':
is_float = 1
lx.pos += 1
lx.col += 1
if lx.pos < lx.len:
ch = lx.src[lx.pos]
if ch == '+' or ch == '-':
lx.pos += 1
lx.col += 1
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if _is_digit(ch) or ch == '_':
lx.pos += 1
lx.col += 1
else:
break
# Complex suffix part
if lx.pos < lx.len and lx.src[lx.pos] == 'j':
is_complex = 1
is_float = 1
lx.pos += 1
lx.col += 1
_emit_number(lx, start, start_lineno, start_col, is_float, is_complex, 0)
def _emit_number(lx: rawptr[Lexer], start: sizet,
start_lineno: int, start_col: int,
is_float: int, is_complex: int, is_hex: int):
length: sizet = lx.pos - start
tok: rawptr[Token] = new_token(lx.pool, TokenType.Number, start_lineno, start_col)
if tok == None: return
token_set_str(lx.pool, tok, lx.src, start, length)
tok.is_float = is_float
tok.is_complex = is_complex
tok.end_lineno = lx.lineno
tok.end_col_offset = start_col + int(length)
# Parse number
if is_float:
tok.float_val = string.atof(tok.str_val)
else:
# Integer literal magnitude > 2^64-1: lexer fail-fast (pure text, no silent wrap).
# Range 0..2^64-1 is still stored into int64 as a 64-bit bit pattern (unchanged shape).
if _lex_int_exceeds_u64(tok.str_val) != 0:
_lex_record_overflow(tok.str_val, start_lineno, start_col)
tok.int_val = string.atoll(tok.str_val)
_emit(lx, tok)
# ============================================================
# String Handling
# ============================================================
def _read_string(lx: rawptr[Lexer]):
"""Read string literal.
Supports prefixes: r, b, u, f, rb, br, rf, fr (case insensitive)
Supports quotes: ', ", ''',
Later we need to support string prefixes like i16/i24"""
start_lineno: int = lx.lineno
start_col: int = lx.col
prefix_start: sizet = lx.pos
# Read prefix (r/b/u/f)
is_raw: int = 0
is_bytes: int = 0
is_fstring: int = 0
while lx.pos < lx.len:
ch: char = lx.src[lx.pos]
if ch == 'r' or ch == 'R':
is_raw = 1
lx.pos += 1
lx.col += 1
elif ch == 'b' or ch == 'B':
is_bytes = 1
lx.pos += 1
lx.col += 1
elif ch == 'u' or ch == 'U':
lx.pos += 1
lx.col += 1
elif ch == 'f' or ch == 'F':
is_fstring = 1
lx.pos += 1
lx.col += 1
else:
break
if lx.pos >= lx.len:
lx.error_count += 1
return
# Determine quote type
quote: char = lx.src[lx.pos]
if quote != '\'' and quote != '"':
lx.error_count += 1
return
# Check triple quote
is_triple: int = 0
if lx.pos + 2 < lx.len:
if lx.src[lx.pos] == quote and lx.src[lx.pos + 1] == quote and lx.src[lx.pos + 2] == quote:
is_triple = 1
content_start: sizet = lx.pos
if is_triple:
lx.pos += 3
lx.col += 3
else:
lx.pos += 1
lx.col += 1
# Read string content
content_buf_start: sizet = lx.pos
# f-string {} expression block nesting depth (for PEP 701 same-quote nested strings)
fstr_depth: int = 0
while lx.pos < lx.len:
ch = lx.src[lx.pos]
if is_triple:
if ch == quote:
if lx.pos + 2 < lx.len:
if lx.src[lx.pos + 1] == quote and lx.src[lx.pos + 2] == quote:
break
elif lx.pos + 2 == lx.len:
break
if ch == '\\':
lx.pos += 1
lx.col += 1
if lx.pos < lx.len:
if lx.src[lx.pos] == '\n':
lx.lineno += 1
lx.col = 0
lx.pos += 1
lx.col += 1
continue
if ch == '\n':
lx.lineno += 1
lx.col = 0
lx.pos += 1
lx.col += 1
else:
if is_fstring:
# {{ and }} are escaped literals, not counted in depth
if ch == '{' and lx.pos + 1 < lx.len and lx.src[lx.pos + 1] == '{':
lx.pos += 2
lx.col += 2
continue
if ch == '}' and lx.pos + 1 < lx.len and lx.src[lx.pos + 1] == '}':
lx.pos += 2
lx.col += 2
continue
if ch == '{':
fstr_depth += 1
lx.pos += 1
lx.col += 1
continue
if ch == '}':
if fstr_depth > 0:
fstr_depth -= 1
lx.pos += 1
lx.col += 1
continue
if fstr_depth > 0 and (ch == '\'' or ch == '"'):
# Nested string literals allowed inside {} blocks (PEP 701 same-quote nesting):
# skip to the matching quote; inner quotes do not terminate the outer f-string.
nq: char = ch
lx.pos += 1
lx.col += 1
while lx.pos < lx.len:
c2: char = lx.src[lx.pos]
if c2 == '\\':
lx.pos += 2
lx.col += 2
continue
if c2 == nq:
lx.pos += 1
lx.col += 1
break
if c2 == '\n':
# single-line string cannot span lines
lx.error_count += 1
return
lx.pos += 1
lx.col += 1
continue
if ch == quote:
break
if ch == '\n':
# Single quote string cannot span multiple lines (unless continued)
lx.error_count += 1
return
if ch == '\\' and not is_raw:
lx.pos += 1
lx.col += 1
if lx.pos < lx.len:
lx.pos += 1
lx.col += 1
continue
lx.pos += 1
lx.col += 1
content_end: sizet = lx.pos
# Skip the closing quote
if is_triple:
if lx.pos + 2 < lx.len:
lx.pos += 3
lx.col += 3
else:
lx.pos = lx.len
else:
if lx.pos < lx.len:
lx.pos += 1
lx.col += 1
# Create token
tok: rawptr[Token] = new_token(lx.pool, TokenType.String, start_lineno, start_col)
if tok == None: return
content_len: sizet = content_end - content_buf_start
# esc_len: Actual bytes after escaping (used to check single character)
esc_len: sizet = content_len
# Handle escape sequences (non-raw strings)
if is_raw == 0 and content_len > 0:
# Allocate conversion buffer (max case: each char is plain, no extension)
conv_buf: str = lx.pool.alloc(content_len + 1)
if conv_buf == None:
token_set_str(lx.pool, tok, lx.src, content_buf_start, content_len)
else:
wpos: sizet = 0
rpos: sizet = 0
while rpos < content_len:
ch: char = lx.src[content_buf_start + rpos]
if ch == '\\' and rpos + 1 < content_len:
rpos += 1
next_ch: char = lx.src[content_buf_start + rpos]
if next_ch == 'n':
conv_buf[wpos] = '\n'
elif next_ch == 't':
conv_buf[wpos] = '\t'
elif next_ch == 'r':
conv_buf[wpos] = '\r'
elif next_ch == 'f':
conv_buf[wpos] = '\f'
elif next_ch == 'v':
conv_buf[wpos] = '\v'
elif next_ch == 'a':
conv_buf[wpos] = '\a'
elif next_ch == 'b':
conv_buf[wpos] = '\b'
elif next_ch == '\\':
conv_buf[wpos] = '\\'
elif next_ch == '\'':
conv_buf[wpos] = '\''
elif next_ch == '"':
conv_buf[wpos] = '"'
elif next_ch == '0':
conv_buf[wpos] = '\0'
elif next_ch == 'x' and rpos + 2 < content_len:
# \xNN hex escape (matches Python): '\\x1b' -> single byte 0x1B.
# Must yield esc_len == 1 so '\xNN' becomes CONST_CHAR, not CONST_STR.
h1: char = lx.src[content_buf_start + rpos + 1]
h2: char = lx.src[content_buf_start + rpos + 2]
if _is_hex(h1) != 0 and _is_hex(h2) != 0:
hex_v: int = (_hex_val(h1) << 4) | _hex_val(h2)
conv_buf[wpos] = char(hex_v)
wpos += 1
rpos += 3
continue
# Invalid hex (\x followed by fewer than 2 hex digits): keep as-is
conv_buf[wpos] = '\\'
wpos += 1
conv_buf[wpos] = next_ch
else:
# Unknown escape sequence, keep original (includes \xNN)
conv_buf[wpos] = '\\'
wpos += 1
conv_buf[wpos] = next_ch
wpos += 1
rpos += 1
else:
conv_buf[wpos] = ch
wpos += 1
rpos += 1
conv_buf[wpos] = '\0'
token_set_str_literal(lx.pool, tok, conv_buf)
esc_len = wpos
else:
token_set_str(lx.pool, tok, lx.src, content_buf_start, content_len)
tok.end_lineno = lx.lineno
tok.end_col_offset = lx.col
# flags field: bit0=raw, bit1=bytes, bit2=fstring, bit3=triple, bit4=dquote, bit5=single char candidate
if is_raw: tok.kw_subtype = tok.kw_subtype | 1
if is_bytes: tok.kw_subtype = tok.kw_subtype | 2
if is_fstring: tok.kw_subtype = tok.kw_subtype | 4
if is_triple: tok.kw_subtype = tok.kw_subtype | 8
# bit4 (16) = double quote=single quote
if quote == '"': tok.kw_subtype = tok.kw_subtype | 16
# bit5 (32) = single-character candidate (not triple quotes, not f-string, exactly 1 byte after escaping)
# Used for CONST_CHAR checks to prevent '0' and other NUL bytes from being mistaken as empty strings by strlen
if is_triple == 0 and is_fstring == 0 and esc_len == 1:
tok.kw_subtype = tok.kw_subtype | 32
_emit(lx, tok)
# ============================================================
# Operator Matching
# ============================================================
def _read_operator(lx: rawptr[Lexer]):
"""Greedy match operator"""
start_lineno: int = lx.lineno
start_col: int = lx.col
remaining: sizet = lx.len - lx.pos
# Traverse operator table, find longest match
best_entry: rawptr[OpEntry] = None
best_len: int = 0
cur: rawptr[OpEntry] = _op_head
while cur != None:
if cur.length <= int(remaining):
# Compare cur.name with src[pos:pos+cur.length]
Matched: int = 1
for i in range(sizet(cur.length)):
if lx.src[lx.pos + i] != cur.name[i]:
Matched = 0
break
if Matched != 0:
if cur.length > best_len:
best_len = cur.length
best_entry = cur
cur = cur.next
if best_entry == None:
# Unknown character, report error and skip
lx.error_count += 1
lx.pos += 1
lx.col += 1
return
# Update parenthesis depth
if best_entry.op_id == TokOp.LPar or best_entry.op_id == TokOp.Lsqb or best_entry.op_id == TokOp.LBrace:
lx.paren_depth += 1
elif best_entry.op_id == TokOp.RPar or best_entry.op_id == TokOp.Rsqb or best_entry.op_id == TokOp.RBrace:
if lx.paren_depth > 0:
lx.paren_depth -= 1
tok: rawptr[Token] = _make_op_token(lx, best_entry.op_id, best_entry.length,
start_lineno, start_col)
if tok == None: return
token_set_str(lx.pool, tok, lx.src, lx.pos, sizet(best_len))
lx.pos += sizet(best_len)
lx.col += best_len
_emit(lx, tok)
# ============================================================
# Main Lexical Analysis Entry Point
# ============================================================
def tokenize(lx: rawptr[Lexer]) -> rawptr[Token]:
"""Execute tokenization, return token list head"""
if lx == None: return None
while lx.pos < lx.len:
# Handle line start indent
if lx.at_line_start and lx.paren_depth == 0:
_handle_indent(lx)
if lx.pos >= lx.len:
break
ch: char = _peek_cur(lx)
if ch == '\0':
break
# New line
if ch == '\n':
if lx.paren_depth > 0:
# # In bi brackets, generates TokenType.Nl
tok: rawptr[Token] = new_token(lx.pool, TokenType.Nl, lx.lineno, lx.col)
_emit(lx, tok)
else:
# Logical new line
tok: rawptr[Token] = new_token(lx.pool, TokenType.NewLine, lx.lineno, lx.col)
_emit(lx, tok)
lx.at_line_start = 1
lx.pos += 1
lx.lineno += 1
lx.col = 0
continue
# Backslash continuation line
# Support LF and CRLF line endings: under CRLF `\` is followed by '\r' (not '\n'),
# so checking only '\n' fails and the backslash continuation breaks.
if ch == '\\' and lx.pos + 1 < lx.len:
if lx.src[lx.pos + 1] == '\n':
lx.pos += 2
lx.lineno += 1
lx.col = 0
continue
if lx.src[lx.pos + 1] == '\r':
# \ + \r\n: skip \, \r, \n
lx.pos += 2
if lx.pos < lx.len and lx.src[lx.pos] == '\n':
lx.pos += 1
lx.lineno += 1
lx.col = 0
continue
# Whitespace
if _is_space(ch):
lx.pos += 1
lx.col += 1
continue
# Comment
if ch == '#':
while lx.pos < lx.len and lx.src[lx.pos] != '\n':
lx.pos += 1
continue
# String prefixes (like r"...") or strings
if ch == '\'' or ch == '"':
_read_string(lx)
continue
# A string prefix letter followed by a quote (must be checked before _is_alpha, otherwise f/r/b/u will be treated as TokenType.Name)
if (ch == 'r' or ch == 'R' or ch == 'b' or ch == 'B' or
ch == 'u' or ch == 'U' or ch == 'f' or ch == 'F'):
if lx.pos + 1 < lx.len:
ch1: char = lx.src[lx.pos + 1]
if ch1 == '\'' or ch1 == '"':
_read_string(lx)
continue
# Double prefixes like rb, br, rf, fr
if lx.pos + 2 < lx.len:
ch2: char = lx.src[lx.pos + 2]
if ((ch1 == 'r' or ch1 == 'R' or ch1 == 'b' or ch1 == 'B' or
ch1 == 'f' or ch1 == 'F' or ch1 == 'u' or ch1 == 'U') and
(ch2 == '\'' or ch2 == '"')):
_read_string(lx)
continue
# Identifier/keyword
if _is_alpha(ch):
_read_name(lx)
continue
# Number
if _is_digit(ch):
_read_number(lx)
continue
# Operator
_read_operator(lx)
# End of file: generate TokenType.NewLine (if there's content)
if lx.tokens_tail != None:
if lx.tokens_tail.type != TokenType.NewLine and lx.tokens_tail.type != TokenType.Nl:
tok: rawptr[Token] = new_token(lx.pool, TokenType.NewLine, lx.lineno, lx.col)
_emit(lx, tok)
# Generate remaining remaining TokenType.Dedent
while lx.indent_top > 0:
lx.indent_top -= 1
tok: rawptr[Token] = new_token(lx.pool, TokenType.Dedent, lx.lineno, 0)
_emit(lx, tok)
# TokenType.EndMarker
tok: rawptr[Token] = new_token(lx.pool, TokenType.EndMarker, lx.lineno, 0)
_emit(lx, tok)
return lx.tokens_head