from stdint import * import memhub import string import stdio import stdlib from .tokens import (Token, TokenType, Keyword, TokOp) from .base import ( AST, ASTCtx, ASTKind, OpKind, ASTFlag, _set_pos, _binop_from_op, _augop_from_op, _cmpop_from_op, _copy_str, _inherit_pos, _init_ast, CONST_INT, CONST_FLOAT, CONST_STR, CONST_BOOL, CONST_NONE, CONST_CHAR, FLAG_IS_ASYNC, FLAG_IS_EXTERN, FLAG_IS_EXPORT, FLAG_IS_DEFINE, ) from .exprs import ( BoolOp, BinOp, UnaryOp, Lambda, IfExp, Dict, Set, ListComp, SetComp, DictComp, GeneratorExp, Await, Yield, YieldFrom, FormattedValue, JoinedStr, Constant, NamedExpr, Attribute, Subscript, Starred, Name, List, Tuple, Slice, Call, Compare, OpNode, New, NEW_SRC_DEFAULT, NEW_SRC_STACK, NEW_SRC_HEAP, NEW_SRC_MEMGR, NEW_LIFE_DEFAULT, NEW_LIFE_SLIFE, NEW_LIFE_HLIFE, ) from .stmts import ( Module, Expression, Interactive, FunctionType, FunctionDef, ClassDef, Return, Delete, Assign, AugAssign, AnnAssign, For, While, If, With, Raise, Try, Assert, Global, Nonlocal, Pass, Break, Continue, FallThrough, Expr, Import, ImportFrom, Match, ) from .astaux import ( ExceptHandler, Arguments, Arg, Keyword as KwNode, Alias, WithItem, Comprehension, ) from .match import ( MatchCase, MatchValue, MatchSingleton, MatchSequence, MatchMapping, MatchClass, MatchStar, MatchAs, MatchOr, ) from .lexer import ( Lexer, _lexer_init, tokenize, new_lexer, get_lex_err_line, get_lex_err_col, get_lex_err_msg, _lex_int_neg_exceeds_i64, ) # ============================================================ # Parser State # ============================================================ # Hard cap on the token linked-list length (cycle detection): normal sources have far fewer # tokens, so reaching this count without the tail means the list is cyclic (see parse_tokens). define PARSER_MAX_TOKENS = 8000000 class ParseError: line: int col: int end_line: int end_col: int message: str # LSP severity: 0/default = error (1), 2 = warning. Appended after message. severity: int def __new__(self, pool: memhub.MemManager*): ptr: ParseError* = pool.alloc(ParseError.__sizeof__()) if ptr: string.memset(ptr, 0, ParseError.__sizeof__()) return ptr class Parser: tokens: Token* # token linked list head cur: Token* # current token prev: Token* # previous consumed token (for the `T*(x)` adjacency test) pool: memhub.MemManager* # memory pool error_count: int # error count recover: int # 1 = collect statement-level errors and return a partial tree errors: list[ParseError*]* # recover-mode error list error_limit: int # recover-mode error cap pending_decorators: list[AST*]* # pending decorators list # Pending parsed left operand for a leading `*` in argument position (see _parse_call / _parse_power). starred_seed: AST* # `static` modifier (soft keyword) pending flag: set after consuming `static` before a `Name :` # declaration; the next _parse_type_ann prepends t.StaticT and clears it (see _ParseStaticModified). pending_static: int # Whether the suite currently being parsed is directly owned by a class (1 = class body, 0 = other), # maintained by _parse_suite_into_children for the virtual/novirtual class-body check. in_class_body: int # 1 while parsing a type annotation: subscript elements then accept type syntax # (`Option[own U]`, `Option[(own U)?]`) instead of plain expressions. in_type_ann: int # Step budget (_advance count / parse_tokens budget): the statement loop fails fast once exceeded, # never looping forever (see the cycle detection in parse_tokens). step_count: sizet step_limit: sizet # Watchdog: counter and convergence flag for "query current token" (see _tick). When tick_count # exceeds step_limit, aborted=1, entries return converged values and every loop ends in bounded rounds. tick_count: sizet aborted: int def __new__(self, pool: memhub.MemManager*): ptr: Parser* = pool.alloc(Parser.__sizeof__()) if ptr: string.memset(ptr, 0, Parser.__sizeof__()) return ptr def new_parser(pool: memhub.MemManager*) -> Parser*: """Allocate from the pool and return the Parser (cross-module safe wrapper)""" return Parser(pool) def _parser_init(ps: Parser*, tokens: Token*, pool: memhub.MemManager*, recover: int = 0): if ps is None: return ps.tokens = tokens ps.cur = tokens ps.prev = None ps.pool = pool ps.error_count = 0 ps.recover = recover ps.errors = None ps.error_limit = 200 if recover != 0: ps.errors = list[ParseError*](pool, 8) ps.pending_decorators = None ps.starred_seed = None ps.pending_static = 0 ps.in_class_body = 0 ps.in_type_ann = 0 ps.step_count = 0 # Default budget is the cap (parse_tokens later tightens it to the real token count) so entries that # bypass parse_tokens (parse_expression etc.) do not misreport at 0, while a hard bound still prevents loops. ps.step_limit = PARSER_MAX_TOKENS ps.tick_count = 0 ps.aborted = 0 # ============================================================ # Parser watchdog (static convergence: no silent hang) # # Any loop that spins without consuming a token must repeatedly query the current token, so every query # entry (_cur_type / _cur_op / _cur_kw / _match_op / _accept_op / _advance) ticks the watchdog; once the # budget is exceeded it sets aborted, records a parse error and returns a converged value: # _cur_type -> EndMarker, _cur_op / _cur_kw -> 0, _match_op / _accept_op -> 1 (jump to end), # _advance -> jump to end. parse_tokens then returns None with error_count > 0 and the caller reports # file:line:col SyntaxError; there is no silent-hang path left. # ============================================================ def _tick(ps: Parser*): """Tick the watchdog; over budget sets aborted and records the first parse error (with position)""" if ps is None: return if ps.aborted != 0: return ps.tick_count += 1 if ps.tick_count > ps.step_limit: ps.aborted = 1 _record_parse_err(ps) ps.error_count += 1 # ============================================================ # Token Navigation Helpers # ============================================================ def _advance(ps: Parser*): """Advance to the next token""" _tick(ps) if ps.aborted != 0: ps.cur = None return if ps.cur is None: return if ps.cur.next is not None: ps.prev = ps.cur ps.cur = ps.cur.next ps.step_count += 1 def _cur_type(ps: Parser*) -> int: _tick(ps) if ps.aborted != 0: return TokenType.EndMarker if ps.cur is None: return TokenType.EndMarker return ps.cur.type def _cur_op(ps: Parser*) -> int: _tick(ps) if ps.aborted != 0: return 0 if ps.cur is None: return 0 return ps.cur.op_subtype def _cur_kw(ps: Parser*) -> int: _tick(ps) if ps.aborted != 0: return 0 if ps.cur is None: return 0 return ps.cur.kw_subtype def _is_funcdef_kw(ps: Parser*, kw: int) -> int: """Whether the current token is a function-definition keyword (def and its alias fn, plus extern/export). def is a hard keyword. fn/extern/export are soft: they count only when the next token is a Name followed by '(' or a generic '['; extern/export also accept ` def A(...)`. Otherwise they are ordinary identifiers. """ if kw == Keyword.Def: return 1 if kw == Keyword.Extern or kw == Keyword.Export: # extern/export def A(...) or extern/export A(...) # (after extern/export comes def or Name + '(' / '[') nxt: Token* = ps.cur.next if nxt is None: return 0 if nxt.kw_subtype == Keyword.Def: return 1 if nxt.type != TokenType.Name: return 0 nxt2: Token* = nxt.next if nxt2 is None: return 0 if nxt2.type == TokenType.Op and (nxt2.op_subtype == TokOp.LPar or nxt2.op_subtype == TokOp.Lsqb): return 1 # extern name(...):extern 允许在函数名前多一个库句柄(export 不支持) # 形态为 Name Name '(' / '[';库句柄来源由 Gen 层校验(必须绑定 ffilib.CDLL) if kw == Keyword.Extern and nxt2.type == TokenType.Name: nxt3: Token* = nxt2.next if nxt3 is not None and nxt3.type == TokenType.Op \ and (nxt3.op_subtype == TokOp.LPar or nxt3.op_subtype == TokOp.Lsqb): return 1 return 0 if kw == Keyword.Fn or kw == Keyword.Define: # Soft-keyword lookahead: next is a Name, the one after is '(' or a generic '[T...]' (PEP 695) # (the define macro function matches def, with t.DefineT appended to the return annotation) nxt: Token* = ps.cur.next if nxt is None: return 0 if nxt.type != TokenType.Name: return 0 nxt2: Token* = nxt.next if nxt2 is None: return 0 if nxt2.type == TokenType.Op \ and (nxt2.op_subtype == TokOp.LPar or nxt2.op_subtype == TokOp.Lsqb): return 1 return 0 return 0 def _is_define_const(ps: Parser*) -> int: """Soft-keyword lookahead for a define constant declaration (1 = yes, 0 = no). `define NAME[: T] = `: next is a Name, then '=' (no annotation) or ':' (annotated). `define NAME(...)` and `define` used as an identifier do not match. `define NAME: T` (missing '=') does match and then fails loudly in _parse_define_const's _expect_op(Equal). """ if _cur_kw(ps) != Keyword.Define: return 0 nxt: Token* = ps.cur.next if nxt is None or nxt.type != TokenType.Name: return 0 nxt2: Token* = nxt.next if nxt2 is None or nxt2.type != TokenType.Op: return 0 if nxt2.op_subtype == TokOp.Equal or nxt2.op_subtype == TokOp.Colon: return 1 return 0 def _is_typedef(ps: Parser*) -> int: """Soft-keyword test for a typedef alias declaration (1 = yes, 0 = no). `typedef T1 = T2` is fully equivalent to `T1: t.TypedefT = T2`. `typedef` is a statement-level reserved word (never an identifier in the repo); invalid forms fail loudly in _parse_typedef. """ if _cur_kw(ps) != Keyword.Typedef: return 0 return 1 def _is_export_extern_var(ps: Parser*) -> int: """Soft-keyword test for `export NAME :` / `extern NAME :` variable declarations (1 = yes, 0 = no). export/extern also serve function declarations (see _is_funcdef_kw); the third token distinguishes them: '(' or '[' is a function, ':' is a variable. Other forms do not match; invalid forms fail loudly. """ kw: int = _cur_kw(ps) if kw != Keyword.Export and kw != Keyword.Extern: return 0 nxt: Token* = ps.cur.next if nxt is None: return 0 if nxt.type != TokenType.Name: return 0 # The name must be a non-keyword identifier (excludes forms like `export def f()`) if nxt.kw_subtype != Keyword.NotKeyword: return 0 nxt2: Token* = nxt.next if nxt2 is None: return 0 if nxt2.type != TokenType.Op: return 0 if nxt2.op_subtype != TokOp.Colon: return 0 return 1 def _is_named_soft(ps: Parser*, name: str) -> int: """Whether the current token is the given soft-keyword identifier (Name, not a hard keyword).""" if _cur_type(ps) != TokenType.Name: return 0 if _cur_kw(ps) != Keyword.NotKeyword: return 0 s: str = _cur_str(ps) if s is None: return 0 if string.strcmp(s, name) == 0: return 1 return 0 def _is_cif_stmt(ps: Parser*) -> int: """Whether the current token starts a `cif` statement (soft keyword; 1 = yes / 0 = no). `cif EXPR:` is a compile-time conditional statement. An assignment/annotation/attribute form (`cif = ...` / `cif: T = ...` / `cif += ...` / `cif.attr`) is an ordinary identifier use and must not be treated as the keyword. """ if _is_named_soft(ps, "cif") == 0: return 0 nxt: Token* = ps.cur.next if nxt is None: return 0 if nxt.type == TokenType.Op: o: int = nxt.op_subtype if o == TokOp.Equal or o == TokOp.Colon or o == TokOp.ColonEq or o == TokOp.Dot \ or o == TokOp.PlusEq or o == TokOp.MinusEq or o == TokOp.StarEq \ or o == TokOp.SlashEq or o == TokOp.DSlashEq or o == TokOp.PercentEq \ or o == TokOp.AtEq or o == TokOp.AmpEq or o == TokOp.VBarEq \ or o == TokOp.CaretEq or o == TokOp.GtGtEq or o == TokOp.LtLtEq \ or o == TokOp.StarEqEq: return 0 return 1 # Parse error position and message (fail-fast, Python SyntaxError style, first error only) _g_parse_err_line: int = 0 _g_parse_err_col: int = 0 _g_parse_err_msg: str = None _g_parse_errors: list[ParseError*]* = None def _record_parse_err(ps: Parser*): """Record the position and message of the first parse error (later errors do not overwrite it)""" global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = _cur_lineno(ps) _g_parse_err_col = _cur_col(ps) err_s: str = _cur_str(ps) if err_s is None: err_s = "(none)" # Copy the message text via stdlib (token.str_val lives in the pool; stored independently to be safe) buf_m: str = stdlib.malloc(128) if buf_m is not None: stdio.snprintf(buf_m, 128, "unexpected token '%s'", err_s) _g_parse_err_msg = buf_m else: _g_parse_err_msg = err_s def _record_parse_err_ctx(ps: Parser*, tok_type: int, pv_str: str, pv_line: int, pv_col: int, nx_str: str, nx_line: int, nx_col: int): """Record the first parse error with token type and prev/next tokens appended to the message. Same source as _record_parse_err (first only); the message must stay a single line since logs inline it. file:line:col and the caret are filled later by the caller's dump_parse_traceback. """ global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = _cur_lineno(ps) _g_parse_err_col = _cur_col(ps) err_s: str = _cur_str(ps) if err_s is None: err_s = "(none)" buf_m: str = stdlib.malloc(512) if buf_m is None: _g_parse_err_msg = err_s return stdio.snprintf(buf_m, 512, "unexpected token '%s' (type=%d) | note: prev='%s' (line %d col %d) | next='%s' (line %d col %d)", err_s, tok_type, pv_str, pv_line, pv_col, nx_str, nx_line, nx_col); _g_parse_err_msg = buf_m def _recover_reset_err(): """Reset the single-error channel before parsing one statement in recovery mode.""" global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg _g_parse_err_line = 0 _g_parse_err_col = 0 _g_parse_err_msg = None def _recover_add_err(ps: Parser*, span_line: int = 0, span_col: int = -1): """Append the current statement's first error to the recovery list. span_line/span_col override the end position when the error did not come from the current token (e.g. a lexer fail-fast recorded before parsing started). """ global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if ps is None or ps.errors is None: return if int(len(ps.errors)) >= ps.error_limit: return line: int = _g_parse_err_line col: int = _g_parse_err_col msg: str = _g_parse_err_msg if line == 0: line = _cur_lineno(ps) col = _cur_col(ps) if msg is None: msg = "syntax error" end_line: int = line end_col: int = col + 1 if span_line > 0 and span_col >= 0: end_line = span_line end_col = span_col elif ps.cur is not None and ps.cur.lineno == line and ps.cur.end_col_offset > col: end_line = ps.cur.end_lineno end_col = ps.cur.end_col_offset e: ParseError* = ParseError(ps.pool) if e is None: return n: sizet = string.strlen(msg) buf: bytes = ps.pool.alloc(n + 1) if buf is None: return string.memcpy(buf, msg, n + 1) e.line = line e.col = col e.end_line = end_line e.end_col = end_col e.message = str(buf) ps.errors.append(e) def _recover_sync(ps: Parser*, start_col: int, top_level: int): """Skip the rest of a failed statement and stop at the next statement boundary. A physical line break is the only reliable boundary when the failed statement left a bracket unbalanced: in that state the lexer emits Nl instead of NewLine and stops generating Indent/Dedent, so logical-line nesting alone would never resynchronize. The next token must start at or left of the failed statement's column to count as its follower. top_level is set by the module loop, which consumes Dedent tokens instead of returning them to a suite owner. """ while ps.cur is not None and ps.cur.type != TokenType.EndMarker: t: int = ps.cur.type if t == TokenType.Nl or t == TokenType.NewLine: _advance(ps) # Blank lines and indentation changes do not start a statement. while ps.cur is not None: if ps.cur.type == TokenType.Nl or ps.cur.type == TokenType.Indent: _advance(ps) elif ps.cur.type == TokenType.Dedent and top_level != 0: _advance(ps) else: break if ps.cur is None or ps.cur.type == TokenType.EndMarker: return if ps.cur.type == TokenType.Dedent: return if ps.cur.col_offset <= start_col: return continue _advance(ps) def _record_neg_literal_overflow(lit_text: str, lineno: int, col: int): """Record a combined "unary minus + integer literal" overflow (fail-fast; first only). `-` is a separate unary operator, so a magnitude-valid literal (<= 2^64-1) may still be < -2^63 after negation. Message carries the literal text + allowed range [-2^63, 2^64-1] + fix hint. """ global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col buf: str = stdlib.malloc(512) if buf is not None: stdio.snprintf(buf, 512, "[VPC] negative integer literal out of range: unary minus applied to an integer literal exceeds the" " signed 64-bit representable range (allowed range -9223372036854775808..18446744073709551615" " = [-2^63, 2^64-1]; a negative result must be >= -2^63). Literal text: '-%s'." " Silent wrap-around/truncation is forbidden; compilation aborted." " Fix: change the negative number into -9223372036854775808..-1 (this language has no" " wider integer type, for larger values split into multiple 64-bit words).", lit_text) _g_parse_err_msg = buf else: _g_parse_err_msg = lit_text # ============================================================ # Type descriptors const / volatile / bigend / littlend (soft keywords, annotation position only) # ============================================================ def _IsDescriptorKeyword(ps: Parser*) -> int: """Whether the current token is a type-descriptor keyword (const / volatile / bigend / littlend), 1 = yes / 0 = no. Soft keywords consumed by _parse_type_ann only in annotation position; `t.attr.const()` as an identifier is unaffected since attribute/call resolution looks at TokenType.Name, not kw_subtype. """ if _cur_type(ps) != TokenType.Name: return 0 if _cur_kw(ps) == Keyword.Const or _cur_kw(ps) == Keyword.Volatile \ or _cur_kw(ps) == Keyword.BigEnd or _cur_kw(ps) == Keyword.LittleEnd: return 1 return 0 def _IsMagicAttributeCall(ps: Parser*) -> int: """Test for an annotation-position `__attribute__(...)` prefix, 1 = yes / 0 = no. `__attribute__` is a soft keyword (not in the keyword table): consumed here only in annotation position immediately before '('. Same family as const/volatile: order-free and mixable. """ if _cur_type(ps) != TokenType.Name: return 0 s: str = _cur_str(ps) if s is None or string.strcmp(s, "__attribute__") != 0: return 0 nxt: Token* = ps.cur.next if nxt is None or nxt.type != TokenType.Op or nxt.op_subtype != TokOp.LPar: return 0 return 1 def _MakeDescriptorMarker(ps: Parser*) -> AST*: """Build the builtin marker Name('ConstT'/'VolatileT'/'BigEndian'/'LittleEndian') from the current descriptor keyword. The marker is a bare builtin name (no module prefix); the backend recognizes these four names directly. bigend/littlend expand to the same BigEndian / LittleEndian markers. """ attr_name: str = "ConstT" if _cur_kw(ps) == Keyword.Volatile: attr_name = "VolatileT" elif _cur_kw(ps) == Keyword.BigEnd: attr_name = "BigEndian" elif _cur_kw(ps) == Keyword.LittleEnd: attr_name = "LittleEndian" marker: AST* = Name(ps.pool, attr_name, ASTCtx.Load) _set_pos(marker, _cur_lineno(ps), _cur_col(ps), 0, 0) return marker def _MakeTypeWrapper(ps: Parser*, marker: AST*, inner: AST*) -> AST*: """Wrap a type in a builtin marker subscript: marker[inner] (e.g. PtrT[T], ConstT[T]).""" node: AST* = Subscript(ps.pool, marker, inner, ASTCtx.Load) if inner is not None: _set_pos(node, inner.lineno, inner.col_offset, 0, 0) return node def _PrependDescriptorMarkers(ps: Parser*, base: AST*, mods: list[AST*]*, mod_count: int) -> AST*: """Wrap the type in the descriptor markers collected before it. `const volatile int` becomes `ConstT[VolatileT[int]]`; the marker order is irrelevant to the backend, which only asks whether a given descriptor is present. """ out: AST* = base i: int = mod_count - 1 while i >= 0: out = _MakeTypeWrapper(ps, mods.get(i), out) i -= 1 return out def _RecordTypeUnionErr(detail: str, lineno: int, col: int): """Record a retired `T1 | T2` type union (fail-fast; first only). All former union forms have dedicated syntax: T* / PtrT[T] for pointers, const/volatile/bigend/ littlend for descriptors, &T / &temp T for value references, export/extern/static for storage. """ global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col buf: str = stdlib.malloc(1024) if buf is not None: stdio.snprintf(buf, 1024, "`T1 | T2` type unions were removed (%s); only `T | None` is accepted and means `T`. " "Use the dedicated syntax instead: " "pointer -> `T*`; const/volatile/bigend/littlend -> the prefix descriptor; " "value reference -> `&T` / `&temp T`; storage -> `export` / `extern` / `static` / `def`; " "fixed array -> `[N]T` or `ArrayT[T, N]`.", detail) _g_parse_err_msg = buf else: _g_parse_err_msg = detail def _AnnotationIsNoneType(node: AST*) -> int: """1 when the node is the bare `None` literal used as a type annotation.""" if node is None or node.kind() != ASTKind.Constant: return 0 cn: Constant* = (Constant*)(node) return 1 if cn.const_kind == CONST_NONE else 0 def _ResolveTypeUnion(ps: Parser*, node: AST*) -> AST*: """Fold the one allowed union spelling `T | None` to `T`; None = rejected. Every other `|` form stays retired. The optional marker is accepted on either side and at any annotation depth (generic arguments, tuple elements); what the resolver keeps is the plain type, so `QWidget | None` and `QWidget` are the same annotation. """ if node is None: return None k: int = node.kind() if k == ASTKind.BinOp: bo: BinOp* = (BinOp*)(node) if bo.op != OpKind.BitOr: return node plain: AST* = None if _AnnotationIsNoneType(bo.right) != 0: plain = bo.left elif _AnnotationIsNoneType(bo.left) != 0: plain = bo.right # `A | B`, `A | B | None` and `None | None` keep failing: only a single # non-None type next to the literal None is an optional annotation. if plain is None or plain.kind() == ASTKind.BinOp or _AnnotationIsNoneType(plain) != 0: _RecordTypeUnionErr("`|` is not allowed in a type annotation", node.lineno, node.col_offset) ps.error_count += 1 return None return _ResolveTypeUnion(ps, plain) if k == ASTKind.Subscript: sub_u: Subscript* = (Subscript*)(node) inner: AST* = _ResolveTypeUnion(ps, sub_u.slice) if inner is None: return None sub_u.slice = inner return node if k == ASTKind.Tuple: tup_u: Tuple* = (Tuple*)(node) if tup_u.elts is not None: for ui in range(int(len(tup_u.elts))): el_u: AST* = _ResolveTypeUnion(ps, tup_u.elts.get(ui)) if el_u is None: return None tup_u.elts.set(ui, el_u) return node return node # ============================================================ # Reserved (hard) keywords: words with syntax roles that may never be used as identifiers. # `func` / `temp` / `tls` / `default` are deliberately NOT reserved: they are real identifiers in # the repo (AST field `func`, includes/viperlib.vp local `temp`, includes/vthreading.vp local `tls`, # includes/argparse.vp parameter `default`). Still NOT reserved: `fn` / `extern` / `tls` # (context keywords) and `cif`/`celif`/`celse` / `temp` / `default` / `__attribute__` (plain # words). Classification source of truth: includes/ast/tokens.vp. Keep this list in sync with # TPC: TransPyC/lib/vast/Tokenizer.py::RESERVED_KEYWORDS. def _IsReservedWord(name: str) -> int: """Whether name is a reserved (hard) keyword that may not be used as an identifier (1=yes/0=no).""" if name is None: return 0 if string.strcmp(name, "typedef") == 0: return 1 if string.strcmp(name, "define") == 0: return 1 if string.strcmp(name, "enum") == 0: return 1 if string.strcmp(name, "union") == 0: return 1 if string.strcmp(name, "renum") == 0: return 1 if string.strcmp(name, "struct") == 0: return 1 if string.strcmp(name, "switch") == 0: return 1 if string.strcmp(name, "fallthrough") == 0: return 1 if string.strcmp(name, "export") == 0: return 1 # Batch 2: annotation-position descriptors and statement-level storage modifiers. if string.strcmp(name, "const") == 0: return 1 if string.strcmp(name, "volatile") == 0: return 1 if string.strcmp(name, "bigend") == 0: return 1 if string.strcmp(name, "littlend") == 0: return 1 if string.strcmp(name, "inline") == 0: return 1 if string.strcmp(name, "static") == 0: return 1 # Batch 3: class/method vtable modifiers. if string.strcmp(name, "virtual") == 0: return 1 if string.strcmp(name, "novirtual") == 0: return 1 return 0 def _RecordReservedErr(name: str, what: str, lineno: int, col: int): """Record 'reserved keyword used as an identifier' (fail-fast; first error only).""" global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col buf: str = stdlib.malloc(384) if buf is not None: stdio.snprintf(buf, 384, "'%s' is a reserved keyword and cannot be used as %s " "(reserved: typedef / define / enum / union / renum / struct / switch / fallthrough / " "export / const / volatile / bigend / littlend / inline / static / virtual / novirtual)", name, what) _g_parse_err_msg = buf else: _g_parse_err_msg = name def _RejectReservedIdent(ps: Parser*, name: str, what: str, lineno: int, col: int) -> int: """Reject a reserved keyword used as an identifier (1=rejected / 0=not reserved).""" if _IsReservedWord(name) == 0: return 0 _RecordReservedErr(name, what, lineno, col) ps.error_count += 1 return 1 def _RejectReservedTargets(ps: Parser*, node: AST*, what: str): """Reject reserved keywords in a store target (Name / Tuple / List / Starred).""" if node is None: return k: int = node.kind() if k == ASTKind.Name: nm_r: Name* = Name*(node) _RejectReservedIdent(ps, nm_r.id, what, nm_r.lineno, nm_r.col_offset) return if k == ASTKind.Tuple or k == ASTKind.List: elts_r: list[AST*]* = None if k == ASTKind.Tuple: tp_r: Tuple* = Tuple*(node) elts_r = tp_r.elts else: ls_r: List* = List*(node) elts_r = ls_r.elts if elts_r is not None: rn: sizet = len(elts_r) for ri in range(int(rn)): _RejectReservedTargets(ps, elts_r.get(ri), what) return if k == ASTKind.Starred: st_r: Starred* = Starred*(node) _RejectReservedTargets(ps, st_r.value, what) return def _RecordDescriptorErr(detail: str, lineno: int, col: int): """Record an invalid type-descriptor form (fail-fast; first only). Message carries the expected form + a correct example; file:line:col is filled later. Descriptors are never silently ignored: missing type / non-type follower / bare use all abort compilation. """ global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col buf: str = stdlib.malloc(1024) if buf is not None: stdio.snprintf(buf, 1024, "invalid type descriptor (%s). const/volatile/bigend/littlend are soft keywords valid only in type" " position; they must be written as `name: const type` / `name: volatile type` / `name: bigend type` /" " `name: littlend type` (combinable, e.g. `name: const volatile type` / `name: const littlend type`)." " Correct examples: a: const int / a: volatile int / a: bigend int32 / a: const littlend int32 / " "a: const volatile t.CInt32T / " "def f(p: const rawptr[Foo]) -> const int." " (descriptors are never silently ignored: missing type / non-type follower / bare use all abort compilation)", detail) _g_parse_err_msg = buf else: _g_parse_err_msg = detail # ============================================================ # virtual / novirtual modifier (soft keyword; statement start before class/def only), same # representation as @t.VTableT / @t.NoVTable. Invalid forms fail loudly; `x = d.virtual` unaffected. def _IsVirtualModifier(ps: Parser*) -> int: """Whether the current token is a virtual / novirtual modifier (1 = yes / 0 = no). Called only at statement start; identifier positions never reach this test, so `x = d.virtual`, parameter names, for targets and dict keys are unaffected. """ if _cur_type(ps) != TokenType.Name: return 0 if _cur_kw(ps) == Keyword.Virtual or _cur_kw(ps) == Keyword.Novirtual: return 1 return 0 def _MakeVirtualMarker(ps: Parser*) -> AST*: """Build the builtin marker Name('VTableT'|'NoVTable') from the current virtual/novirtual token. The marker is a bare builtin name; ClassGenBase._has_decorator matches it by name. """ attr_name: str = "VTableT" if _cur_kw(ps) == Keyword.Novirtual: attr_name = "NoVTable" marker: AST* = Name(ps.pool, attr_name, ASTCtx.Load) _set_pos(marker, _cur_lineno(ps), _cur_col(ps), 0, 0) return marker def _RecordVirtualErr(detail: str, lineno: int, col: int, extra: str): """Record an invalid virtual/novirtual form (fail-fast; first only); message carries allowed positions + a correct example. Modifiers are never silently ignored. Callers build detail/extra with f-string interpolation: this language's str is i8*, so `str + str` degrades to raw pointer add and illegal IR; never concatenate strings with `+` in this file. """ global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col buf: str = stdlib.malloc(1536) if buf is not None: stdio.snprintf(buf, 1536, "[VPC] invalid virtual/novirtual modifier (%s). Valid positions: class level " "`virtual class C:` / `novirtual class C:`; method level " "`virtual def m(self): ...` / `novirtual def m(self): ...`" " (only directly inside a class body: the body of virtual class / class / novirtual class)." " Correct examples: virtual class Shape: / novirtual class Node: / " "class A: virtual def run(self) -> int: ... / " "novirtual class B: novirtual def helper(self) -> int: ....%s" " (modifiers are never silently ignored: illegal forms all abort compilation)", detail, extra) _g_parse_err_msg = buf else: _g_parse_err_msg = detail def _ParseVirtualModified(ps: Parser*, parent: AST*): """`virtual class` / `novirtual class` / `virtual def` / `novirtual def`. Synthesizes a @t.VTableT / @t.NoVTable decorator (merged with existing decorators, appended last; position-independent since _has_decorator matches by name) and hands off to _parse_classdef / _parse_funcdef. """ mod_lineno: int = _cur_lineno(ps) mod_col: int = _cur_col(ps) mod_name: str = "virtual" if _cur_kw(ps) == Keyword.Novirtual: mod_name = "novirtual" marker: AST* = _MakeVirtualMarker(ps) _advance(ps) # consume the modifier # The modifier must be followed by class / def (the def alias fn is accepted too, matching # _is_funcdef_kw; fun/func/function were removed on 2026-10-23). kw2: int = _cur_kw(ps) is_class: int = 0 is_def: int = 0 if kw2 == Keyword.Class: is_class = 1 elif kw2 == Keyword.Def or kw2 == Keyword.Fn: is_def = 1 if is_class == 0 and is_def == 0: nxt_s: str = _cur_str(ps) if nxt_s is None: nxt_s = "(none)" if kw2 == Keyword.Struct: # struct is a pure data type: no virtual / novirtual modifier allowed. A dedicated message # (not the generic "follower not class/def") points at the fix: struct already implies @t.NoVTable. _RecordStructErr(f"`{mod_name} struct` is invalid: struct forbids the virtual/novirtual modifier", mod_lineno, mod_col, "struct has no vtable by nature (a @t.NoVTable is synthesized at parse time);" " write `struct X:` directly; if vtable semantics are needed, use class instead.") else: _RecordVirtualErr(f"`{mod_name}` is followed by '{nxt_s}' (not class/def)", mod_lineno, mod_col, f"`{mod_name}` takes effect only before class/def;" f" if a plain identifier name was intended here, rename it" f" (this word is treated as a modifier at statement start).") ps.error_count += 1 # Dispatch the remaining tokens to the generic statement path (no infinite loop, no error overwrite). _parse_simple_stmt_into(ps, parent) return if is_def != 0 and ps.in_class_body == 0: _RecordVirtualErr(f"{mod_name} def appears outside a class body", mod_lineno, mod_col, f"{mod_name} def describes the 'virtual/non-virtual' of a method inside a class body;" f" at module level / function body / branch body there is no class to attach to.") ps.error_count += 1 # Merge existing decorators (@deco + virtual class/def) then hand off to the existing class/def parser merged: list[AST*]* = list[AST*](ps.pool, 8) if merged is None: return if ps.pending_decorators is not None: dn: sizet = len(ps.pending_decorators) for di in range(int(dn)): merged.append(ps.pending_decorators.get(di)) merged.append(marker) ps.pending_decorators = merged stmt: AST* = None if is_class != 0: stmt = _parse_classdef(ps) else: stmt = _parse_funcdef(ps, 0) if stmt is not None: parent.append(stmt) # ============================================================ # pure modifier (soft keyword): `pure def f(...)` / `pure class C:` # # A pure function receives and returns value-copy data. The parser makes that # explicit: every `str`/`bytes` annotation in a pure signature or body becomes # `pure_str` (the small {s: str, size: sizet} value defined in includes/_pure.vp), # and string literals become `_pure_from("...")` so concatenation goes through # the heap-allocating `pure_str.__add__` instead of raw pointer arithmetic. # # Methods of a pure class are marked pure as well; `self` is the only mutable # binding. `PureT` is a compile-time marker (no runtime decorator) handled by # the translator, vpi converter and stub merger. # ============================================================ def _PureMakeMarker(ps: Parser*, name: str, lineno: int, col: int) -> AST*: """Build a bare compile-time marker Name (PureT / NoVTable) at the given position.""" m: AST* = Name(ps.pool, name, ASTCtx.Load) _set_pos(m, lineno, col, 0, 0) return m def _PureMergeMarkers(ps: Parser*, decs: list[AST*]*, extra: AST*) -> list[AST*]*: """Return decs + extra (appended last); position-independent marker detection.""" merged: list[AST*]* = list[AST*](ps.pool, 8) if merged is None: return decs if decs is not None: dn: sizet = len(decs) for di in range(int(dn)): merged.append(decs.get(di)) if extra is not None: merged.append(extra) return merged def _PureStrAnnotation(ps: Parser*, ann: AST*) -> int: """1 when the annotation is exactly the builtin `str` or `bytes` name.""" if ann is None or ann.kind() != ASTKind.Name: return 0 an: Name* = Name*(ann) if an is None or an.id is None: return 0 if string.strcmp(an.id, "str") == 0: return 1 if string.strcmp(an.id, "bytes") == 0: return 1 return 0 def _PureRewriteAnnotation(ps: Parser*, ann: AST*) -> AST*: """Map str/bytes to pure_str inside a pure context (recurses generic args).""" if ann is None: return ann if _PureStrAnnotation(ps, ann) != 0: rep: AST* = Name(ps.pool, "pure_str", ASTCtx.Load) _set_pos(rep, ann.lineno, ann.col_offset, ann.end_lineno, ann.end_col_offset) return rep k: int = ann.kind() if k == ASTKind.Subscript: sub_a: Subscript* = Subscript*(ann) if sub_a is not None: sub_a.slice = _PureRewriteAnnotation(ps, sub_a.slice) elif k == ASTKind.Tuple: tup_a: Tuple* = Tuple*(ann) if tup_a is not None and tup_a.elts is not None: for ti in range(int(len(tup_a.elts))): tup_a.elts.set(ti, _PureRewriteAnnotation(ps, tup_a.elts.get(ti))) elif k == ASTKind.BinOp: bo_a: BinOp* = BinOp*(ann) if bo_a is not None: bo_a.left = _PureRewriteAnnotation(ps, bo_a.left) bo_a.right = _PureRewriteAnnotation(ps, bo_a.right) return ann def _PureFromCall(ps: Parser*, lit: AST*) -> AST*: """Build `_pure_from()` at the literal's position.""" fn: AST* = Name(ps.pool, "_pure_from", ASTCtx.Load) _set_pos(fn, lit.lineno, lit.col_offset, lit.end_lineno, lit.end_col_offset) args: list[AST*]* = list[AST*](ps.pool, 4) if args is None: return lit args.append(lit) kws: list[AST*]* = list[AST*](ps.pool, 1) call: AST* = Call(ps.pool, fn, args, kws) _set_pos(call, lit.lineno, lit.col_offset, lit.end_lineno, lit.end_col_offset) return call def _PureRewriteExpr(ps: Parser*, node: AST*, depth: int) -> AST*: """Rewrite string literals in a pure expression to pure_str values.""" if node is None or depth > 256: return node k: int = node.kind() if k == ASTKind.Constant: cn: Constant* = Constant*(node) if cn is not None and cn.const_kind == CONST_STR and cn.str_val is not None: return _PureFromCall(ps, node) return node if k == ASTKind.BinOp: bo: BinOp* = BinOp*(node) if bo is not None: bo.left = _PureRewriteExpr(ps, bo.left, depth + 1) bo.right = _PureRewriteExpr(ps, bo.right, depth + 1) return node if k == ASTKind.UnaryOp: uo: UnaryOp* = UnaryOp*(node) if uo is not None: uo.operand = _PureRewriteExpr(ps, uo.operand, depth + 1) return node if k == ASTKind.Compare: cp: Compare* = Compare*(node) if cp is not None: cp.left = _PureRewriteExpr(ps, cp.left, depth + 1) if cp.comparators is not None: for ci in range(int(len(cp.comparators))): cp.comparators.set(ci, _PureRewriteExpr(ps, cp.comparators.get(ci), depth + 1)) return node if k == ASTKind.BoolOp: boo: BoolOp* = BoolOp*(node) if boo is not None and boo.values is not None: for bi in range(int(len(boo.values))): boo.values.set(bi, _PureRewriteExpr(ps, boo.values.get(bi), depth + 1)) return node if k == ASTKind.IfExp: ie: IfExp* = IfExp*(node) if ie is not None: ie.test = _PureRewriteExpr(ps, ie.test, depth + 1) ie.body = _PureRewriteExpr(ps, ie.body, depth + 1) ie.orelse = _PureRewriteExpr(ps, ie.orelse, depth + 1) return node if k == ASTKind.Call: cl: Call* = Call*(node) if cl is not None: if cl.args is not None: for ai in range(int(len(cl.args))): cl.args.set(ai, _PureRewriteExpr(ps, cl.args.get(ai), depth + 1)) # Keyword values are left as-is: the AST keyword node class is registered under its # declaration name, which is shadowed by the Keyword enum in this file. Pure keyword # arguments are uncommon; positional strings are the supported form. return node if k == ASTKind.Attribute: at: Attribute* = Attribute*(node) if at is not None: at.value = _PureRewriteExpr(ps, at.value, depth + 1) return node if k == ASTKind.Subscript: sb: Subscript* = Subscript*(node) if sb is not None: sb.value = _PureRewriteExpr(ps, sb.value, depth + 1) sb.slice = _PureRewriteExpr(ps, sb.slice, depth + 1) return node if k == ASTKind.Slice: sl: Slice* = Slice*(node) if sl is not None: sl.lower = _PureRewriteExpr(ps, sl.lower, depth + 1) sl.upper = _PureRewriteExpr(ps, sl.upper, depth + 1) sl.step = _PureRewriteExpr(ps, sl.step, depth + 1) return node if k == ASTKind.Starred: st: Starred* = Starred*(node) if st is not None: st.value = _PureRewriteExpr(ps, st.value, depth + 1) return node if k == ASTKind.NamedExpr: ne: NamedExpr* = NamedExpr*(node) if ne is not None: ne.value = _PureRewriteExpr(ps, ne.value, depth + 1) return node if k == ASTKind.Tuple: tp: Tuple* = Tuple*(node) if tp is not None and tp.elts is not None: for ti in range(int(len(tp.elts))): tp.elts.set(ti, _PureRewriteExpr(ps, tp.elts.get(ti), depth + 1)) return node if k == ASTKind.List: ls: List* = List*(node) if ls is not None and ls.elts is not None: for li in range(int(len(ls.elts))): ls.elts.set(li, _PureRewriteExpr(ps, ls.elts.get(li), depth + 1)) return node if k == ASTKind.Dict: dc: Dict* = Dict*(node) if dc is not None: if dc.keys is not None: for di in range(int(len(dc.keys))): dc.keys.set(di, _PureRewriteExpr(ps, dc.keys.get(di), depth + 1)) if dc.values is not None: for dv in range(int(len(dc.values))): dc.values.set(dv, _PureRewriteExpr(ps, dc.values.get(dv), depth + 1)) return node if k == ASTKind.Set: se: Set* = Set*(node) if se is not None and se.elts is not None: for si in range(int(len(se.elts))): se.elts.set(si, _PureRewriteExpr(ps, se.elts.get(si), depth + 1)) return node if k == ASTKind.Await: aw: Await* = Await*(node) if aw is not None: aw.value = _PureRewriteExpr(ps, aw.value, depth + 1) return node if k == ASTKind.Yield: yl: Yield* = Yield*(node) if yl is not None: yl.value = _PureRewriteExpr(ps, yl.value, depth + 1) return node if k == ASTKind.YieldFrom: yf: YieldFrom* = YieldFrom*(node) if yf is not None: yf.value = _PureRewriteExpr(ps, yf.value, depth + 1) return node if k == ASTKind.Lambda: lm: Lambda* = Lambda*(node) if lm is not None: lm.body = _PureRewriteExpr(ps, lm.body, depth + 1) return node # JoinedStr / FormattedValue keep their raw form: their literal chunks are # not standalone values, and formatting a pure_str would need a separate # conversion. Pure code builds output with concatenation instead. return node def _PureRewriteBlock(ps: Parser*, stmts: list[AST*]*, depth: int): """Rewrite a statement block in pure context.""" if stmts is None or depth > 128: return for si in range(int(len(stmts))): _PureRewriteStmt(ps, stmts.get(si), depth + 1) def _PureRewriteStmt(ps: Parser*, stmt: AST*, depth: int): """Rewrite one statement in pure context (annotations + string literals).""" if stmt is None or depth > 128: return k: int = stmt.kind() if k == ASTKind.Return: r: Return* = Return*(stmt) if r is not None: r.value = _PureRewriteExpr(ps, r.value, 0) elif k == ASTKind.Expr: e: Expr* = Expr*(stmt) if e is not None: e.value = _PureRewriteExpr(ps, e.value, 0) elif k == ASTKind.Assign: a: Assign* = Assign*(stmt) if a is not None: a.value = _PureRewriteExpr(ps, a.value, 0) elif k == ASTKind.AnnAssign: aa: AnnAssign* = AnnAssign*(stmt) if aa is not None: aa.annotation = _PureRewriteAnnotation(ps, aa.annotation) aa.value = _PureRewriteExpr(ps, aa.value, 0) elif k == ASTKind.AugAssign: ag: AugAssign* = AugAssign*(stmt) if ag is not None: ag.value = _PureRewriteExpr(ps, ag.value, 0) elif k == ASTKind.If: ifn: If* = If*(stmt) if ifn is not None: ifn.test = _PureRewriteExpr(ps, ifn.test, 0) _PureRewriteBlock(ps, ifn.children, depth) _PureRewriteBlock(ps, ifn.orelse, depth) elif k == ASTKind.While: wh: While* = While*(stmt) if wh is not None: wh.test = _PureRewriteExpr(ps, wh.test, 0) _PureRewriteBlock(ps, wh.children, depth) _PureRewriteBlock(ps, wh.orelse, depth) elif k == ASTKind.For: fr: For* = For*(stmt) if fr is not None: fr.iter = _PureRewriteExpr(ps, fr.iter, 0) _PureRewriteBlock(ps, fr.children, depth) _PureRewriteBlock(ps, fr.orelse, depth) elif k == ASTKind.With: wi: With* = With*(stmt) if wi is not None and wi.items is not None: for ii in range(int(len(wi.items))): item: WithItem* = WithItem*(wi.items.get(ii)) if item is not None: item.context_expr = _PureRewriteExpr(ps, item.context_expr, 0) _PureRewriteBlock(ps, wi.children, depth) elif k == ASTKind.Raise: rs: Raise* = Raise*(stmt) if rs is not None: rs.exc = _PureRewriteExpr(ps, rs.exc, 0) rs.cause = _PureRewriteExpr(ps, rs.cause, 0) elif k == ASTKind.Assert: asr: Assert* = Assert*(stmt) if asr is not None: asr.test = _PureRewriteExpr(ps, asr.test, 0) asr.msg = _PureRewriteExpr(ps, asr.msg, 0) elif k == ASTKind.Try: tr: Try* = Try*(stmt) if tr is not None: _PureRewriteBlock(ps, tr.children, depth) if tr.handlers is not None: for hi in range(int(len(tr.handlers))): hd: ExceptHandler* = ExceptHandler*(tr.handlers.get(hi)) if hd is not None: _PureRewriteBlock(ps, hd.children, depth) _PureRewriteBlock(ps, tr.orelse, depth) _PureRewriteBlock(ps, tr.finalbody, depth) elif k == ASTKind.Match: mt: Match* = Match*(stmt) if mt is not None: mt.subject = _PureRewriteExpr(ps, mt.subject, 0) if mt.cases is not None: for mi in range(int(len(mt.cases))): mc: MatchCase* = MatchCase*(mt.cases.get(mi)) if mc is not None: mc.guard = _PureRewriteExpr(ps, mc.guard, 0) _PureRewriteBlock(ps, mc.children, depth) elif k == ASTKind.FunctionDef: # Nested definitions inside a pure body are pure as well: every callee # reachable from pure code must obey the same value-only contract. fd: FunctionDef* = FunctionDef*(stmt) if fd is not None: fd.decorator_list = _PureMergeMarkers(ps, fd.decorator_list, _PureMakeMarker(ps, "PureT", stmt.lineno, stmt.col_offset)) _PureRewriteFunction(ps, fd) def _PureRewriteFunction(ps: Parser*, fd: FunctionDef*): """Rewrite a pure function signature and body.""" if fd is None: return fd.returns = _PureRewriteAnnotation(ps, fd.returns) args_node: Arguments* = fd.args if args_node is not None: if args_node.args is not None: for ai in range(int(len(args_node.args))): ar: Arg* = Arg*(args_node.args.get(ai)) if ar is not None: ar.annotation = _PureRewriteAnnotation(ps, ar.annotation) if args_node.vararg is not None: va: Arg* = Arg*(args_node.vararg) if va is not None: va.annotation = _PureRewriteAnnotation(ps, va.annotation) if args_node.kwarg is not None: ka: Arg* = Arg*(args_node.kwarg) if ka is not None: ka.annotation = _PureRewriteAnnotation(ps, ka.annotation) _PureRewriteBlock(ps, fd.children, 0) def _PureAnnIsPointer(ps: Parser*, ann: AST*) -> int: """1 when the annotation denotes raw pointer/array storage (PtrT / rawptr / bytes / ArrayT).""" if ann is None: return 0 k: int = ann.kind() if k == ASTKind.Name: nm: Name* = Name*(ann) if nm.id is None: return 0 if string.strcmp(nm.id, "rawptr") == 0: return 1 elif k == ASTKind.Subscript: sb: Subscript* = Subscript*(ann) if sb.value is not None and sb.value.kind() == ASTKind.Name: bn: Name* = Name*(sb.value) if bn.id is not None: if string.strcmp(bn.id, "PtrT") == 0: return 1 if string.strcmp(bn.id, "rawptr") == 0: return 1 if string.strcmp(bn.id, "ArrayT") == 0: return 1 return 0 def _PureRewriteClass(ps: Parser*, cd: ClassDef*): """Rewrite a pure class: value fields, pure methods.""" global _g_parse_err_msg, _g_parse_err_line, _g_parse_err_col if cd is None: return if cd.children is not None: for ci in range(int(len(cd.children))): ch: AST* = cd.children.get(ci) if ch is None: continue ck: int = ch.kind() if ck == ASTKind.AnnAssign: aa: AnnAssign* = AnnAssign*(ch) if aa is not None: if _PureAnnIsPointer(ps, aa.annotation) != 0: if _g_parse_err_line == 0: _g_parse_err_line = ch.lineno _g_parse_err_col = ch.col_offset _g_parse_err_msg = "pure class fields must be value types: a pointer/array field would alias external storage and could outlive it. Use str/bytes for a pure_str field, or a pure class value" ps.error_count += 1 aa.annotation = _PureRewriteAnnotation(ps, aa.annotation) aa.value = _PureRewriteExpr(ps, aa.value, 0) elif ck == ASTKind.FunctionDef: fd: FunctionDef* = FunctionDef*(ch) if fd is not None: fd.decorator_list = _PureMergeMarkers(ps, fd.decorator_list, _PureMakeMarker(ps, "PureT", ch.lineno, ch.col_offset)) _PureRewriteFunction(ps, fd) def _IsPureModifier(ps: Parser*) -> int: """Whether the current token is `pure` before class/def (1 = yes / 0 = no).""" if _cur_type(ps) != TokenType.Name: return 0 s: str = _cur_str(ps) if s is None or string.strcmp(s, "pure") != 0: return 0 nxt: Token* = ps.cur.next if nxt is None: return 0 if nxt.kw_subtype == Keyword.Class: return 1 if nxt.kw_subtype == Keyword.Def: return 1 if nxt.kw_subtype == Keyword.Fn: nxt2: Token* = nxt.next if nxt2 is not None and nxt2.type == TokenType.Name: return 1 return 0 def _ParsePureModified(ps: Parser*, parent: AST*): """`pure class C:` / `pure def f(...)`: mark and rewrite the declaration.""" global _g_parse_err_msg, _g_parse_err_line, _g_parse_err_col _advance(ps) # consume `pure` kw2: int = _cur_kw(ps) is_class: int = 0 if kw2 == Keyword.Class: is_class = 1 elif kw2 == Keyword.Def or kw2 == Keyword.Fn: is_class = 0 else: nxt_s: str = _cur_str(ps) if nxt_s is None: nxt_s = "(none)" _g_parse_err_msg = "`pure` must be followed by class or def" _g_parse_err_line = _cur_lineno(ps) _g_parse_err_col = _cur_col(ps) ps.error_count += 1 return marker: AST* = _PureMakeMarker(ps, "PureT", _cur_lineno(ps), _cur_col(ps)) merged: list[AST*]* = _PureMergeMarkers(ps, ps.pending_decorators, marker) if is_class != 0: # A pure class is a value type: no vtable. nv: AST* = _PureMakeMarker(ps, "NoVTable", _cur_lineno(ps), _cur_col(ps)) merged = _PureMergeMarkers(ps, merged, nv) ps.pending_decorators = merged stmt: AST* = None if is_class != 0: stmt = _parse_classdef(ps) if stmt is not None: _PureRewriteClass(ps, ClassDef*(stmt)) else: stmt = _parse_funcdef(ps, 0) if stmt is not None: _PureRewriteFunction(ps, FunctionDef*(stmt)) if stmt is not None: parent.append(stmt) # ============================================================ # inline modifier (soft keyword): `inline def f(...)` -> synthesizes @t.attr.always_inline, same shape as # virtual/novirtual (statement start before a function definition only); kept out of the keyword table. # ============================================================ def _IsInlineModifier(ps: Parser*) -> int: """Whether the current token is `inline` (soft keyword followed by a function definition), 1 = yes / 0 = no""" if _cur_type(ps) != TokenType.Name: return 0 s: str = _cur_str(ps) if s is None or string.strcmp(s, "inline") != 0: return 0 nxt: Token* = ps.cur.next if nxt is None or nxt.type != TokenType.Name: return 0 nxt2: Token* = nxt.next if nxt2 is None: return 0 # Followed by def (hard keyword): `inline def f(...)` if nxt.kw_subtype == Keyword.Def: return 1 # Followed by the function-definition soft-keyword alias (fn): same rule as _is_funcdef_kw -- # must be Name + '(' or '[T]', so `inline fn` is not misread. if nxt.kw_subtype == Keyword.Fn: if nxt2.type == TokenType.Op \ and (nxt2.op_subtype == TokOp.LPar or nxt2.op_subtype == TokOp.Lsqb): return 1 return 0 def _MakeInlineMarker(ps: Parser*) -> AST*: """Build the builtin marker Attribute(Name('attr'), 'always_inline') from the current inline token. `attr` is a builtin namespace recognized by the backend; the marker needs no module prefix. """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) attr_root: AST* = Name(ps.pool, "attr", ASTCtx.Load) marker: AST* = Attribute(ps.pool, attr_root, "always_inline", ASTCtx.Load) _set_pos(marker, lineno, col, 0, 0) return marker def _ParseInlineModified(ps: Parser*, parent: AST*): """`inline def f(...)`: consume inline, synthesize @t.attr.always_inline and hand off to _parse_funcdef""" marker: AST* = _MakeInlineMarker(ps) if marker is None: return _advance(ps) # consume the inline modifier # Merge existing decorators (@deco + inline def), appending the marker last; position-independent # (matched by name downstream). Same pending_decorators channel as _ParseVirtualModified. merged: list[AST*]* = list[AST*](ps.pool, 8) if merged is None: return if ps.pending_decorators is not None: dn: sizet = len(ps.pending_decorators) for di in range(int(dn)): merged.append(ps.pending_decorators.get(di)) merged.append(marker) ps.pending_decorators = merged stmt: AST* = _parse_funcdef(ps, 0) if stmt is not None: parent.append(stmt) def _IsTlsModifier(ps: Parser*) -> int: """Whether the current token is a `tls` modifier before a function definition, 1 = yes / 0 = no. Soft keyword: matches only at statement start with a following def (or the fn alias + '(' / '['), so `tls: local = local()` / `tls.set(...)` / `tls.get()` stay plain identifiers. """ if _cur_type(ps) != TokenType.Name: return 0 if _cur_kw(ps) != Keyword.Tls: return 0 nxt: Token* = ps.cur.next if nxt is None or nxt.type != TokenType.Name: return 0 if nxt.kw_subtype == Keyword.Def: return 1 nxt2: Token* = nxt.next if nxt.kw_subtype == Keyword.Fn: if nxt2 is not None and nxt2.type == TokenType.Op \ and (nxt2.op_subtype == TokOp.LPar or nxt2.op_subtype == TokOp.Lsqb): return 1 return 0 def _MakeTlsMarker(ps: Parser*) -> AST*: """Build the builtin marker Name('TLS') from the current tls token. The marker is a bare builtin name; FuncGenDef / ClassGenBase's _has_decorator("TLS") matches it. """ marker: AST* = Name(ps.pool, "TLS", ASTCtx.Load) _set_pos(marker, _cur_lineno(ps), _cur_col(ps), 0, 0) return marker def _ParseTlsModified(ps: Parser*, parent: AST*): """`tls def f(...)`: consume tls, synthesize @t.TLS and hand off to _parse_funcdef. Same pending_decorators channel as _ParseVirtualModified / _ParseInlineModified, so `@deco` + `tls def` merge and position does not matter. """ marker: AST* = _MakeTlsMarker(ps) if marker is None: return _advance(ps) # consume the tls modifier merged: list[AST*]* = list[AST*](ps.pool, 8) if merged is None: return if ps.pending_decorators is not None: dn: sizet = len(ps.pending_decorators) for di in range(int(dn)): merged.append(ps.pending_decorators.get(di)) merged.append(marker) ps.pending_decorators = merged stmt: AST* = _parse_funcdef(ps, 0) if stmt is not None: parent.append(stmt) def _IsStaticModifier(ps: Parser*) -> int: """Whether the current token is a `static` modifier (soft keyword), 1 = yes / 0 = no. Called only at statement start; matches when followed by (1) a function definition (`def` or the fn alias + '(' / '[') or (2) a variable declaration (`Name :`, i.e. AnnAssign). Identifier positions never reach this test, so `x = d.static` / `f(static=1)` are unaffected. """ if _cur_type(ps) != TokenType.Name: return 0 s: str = _cur_str(ps) if s is None or string.strcmp(s, "static") != 0: return 0 nxt: Token* = ps.cur.next if nxt is None or nxt.type != TokenType.Name: return 0 # (1) followed by def (hard keyword): `static def f(...)` if nxt.kw_subtype == Keyword.Def: return 1 nxt2: Token* = nxt.next # (1') followed by a function-definition alias: must be Name + '(' or '[T]', same rule as # _IsInlineModifier / _is_funcdef_kw, so `static func` is not misread. if nxt.kw_subtype == Keyword.Fn: if nxt2 is not None and nxt2.type == TokenType.Op \ and (nxt2.op_subtype == TokOp.LPar or nxt2.op_subtype == TokOp.Lsqb): return 1 # (2) followed by a variable declaration `Name :` (AnnAssign) if nxt2 is not None and nxt2.type == TokenType.Op and nxt2.op_subtype == TokOp.Colon: return 1 return 0 def _IsStaticDescriptorKeyword(ps: Parser*) -> int: """Whether the current token is an annotation-position `static` (soft keyword), 1 = yes / 0 = no. This position (`name: static T`) is no longer supported: _parse_type_ann only detects it as misuse and fails loudly, pointing at the prefix form `static name: T`; it is no longer desugared to t.StaticT. Valid forms are statement-start `static name: T` and the `@t.StaticT` decorator. Reporting (not ignoring) is mandatory: unconsumed, `static` degrades into an opaque "class" named static and errors far from the cause. """ if _cur_type(ps) != TokenType.Name: return 0 s: str = _cur_str(ps) if s is None or string.strcmp(s, "static") != 0: return 0 return 1 def _MakeStaticMarker(ps: Parser*) -> AST*: """Build the builtin marker Name('StaticT'). The marker is a bare builtin name, so downstream (FuncGenDef storage-class detection, TypeGenResolve qualifier resolution) matches it by name. """ marker: AST* = Name(ps.pool, "StaticT", ASTCtx.Load) _set_pos(marker, _cur_lineno(ps), _cur_col(ps), 0, 0) return marker def _ParseStaticModified(ps: Parser*, parent: AST*) -> int: """Dispatch the `static` modifier: 1 = fully handled (function definition), 0 = only flagged (variable declaration, caller continues with the generic statement dispatch). Function: consume static and synthesize an @t.StaticT decorator, handed to _parse_funcdef. Variable: set pending_static and consume static; the next _parse_type_ann prepends t.StaticT. """ nxt: Token* = ps.cur.next is_func: int = 0 if nxt is not None and nxt.type == TokenType.Name: if nxt.kw_subtype == Keyword.Def: is_func = 1 elif nxt.kw_subtype == Keyword.Fn: nxt2: Token* = nxt.next if nxt2 is not None and nxt2.type == TokenType.Op \ and (nxt2.op_subtype == TokOp.LPar or nxt2.op_subtype == TokOp.Lsqb): is_func = 1 if is_func == 0: # Variable branch: only flag + consume static, then let the caller continue generic dispatch # (AnnAssign parsing reads pending_static in _parse_type_ann). ps.pending_static = 1 _advance(ps) return 0 marker: AST* = _MakeStaticMarker(ps) if marker is None: return 1 _advance(ps) # consume the static modifier # Merge existing decorators (@deco + static def), appending the marker last; position-independent # (matched by name downstream). Same pending_decorators channel as _ParseInlineModified. merged: list[AST*]* = list[AST*](ps.pool, 8) if merged is None: return 1 if ps.pending_decorators is not None: dn: sizet = len(ps.pending_decorators) for di in range(int(dn)): merged.append(ps.pending_decorators.get(di)) merged.append(marker) ps.pending_decorators = merged stmt: AST* = _parse_funcdef(ps, 0) if stmt is not None: parent.append(stmt) return 1 def _IsBareDescriptor(ps: Parser*) -> int: """Bare-descriptor test: the whole statement is a single descriptor run (const[: volatile][: bigend][: littlend]), 1 = yes / 0 = no. Matches only when the descriptor run is followed directly by newline / EOF / semicolon; identifiers (`const = 5` / `const(x)` / `t.attr.const()`) do not match (soft-keyword boundary). """ if _IsDescriptorKeyword(ps) == 0: return 0 nxt: Token* = ps.cur.next while nxt is not None and nxt.type == TokenType.Name \ and (nxt.kw_subtype == Keyword.Const or nxt.kw_subtype == Keyword.Volatile \ or nxt.kw_subtype == Keyword.BigEnd or nxt.kw_subtype == Keyword.LittleEnd): nxt = nxt.next if nxt is None: return 1 if nxt.type == TokenType.NewLine or nxt.type == TokenType.EndMarker: return 1 if nxt.type == TokenType.Op and nxt.op_subtype == TokOp.Semi: return 1 return 0 def get_parse_err_line() -> int: """Line number of the latest parse error (0 = none)""" return _g_parse_err_line def get_parse_err_col() -> int: """Column of the latest parse error (0 = none)""" return _g_parse_err_col def get_parse_err_msg() -> str: """Message of the latest parse error (Python SyntaxError style)""" return _g_parse_err_msg def get_parse_errors() -> list[ParseError*]*: """Recovery-mode error list (None outside recovery mode).""" return _g_parse_errors def _cur_str(ps: Parser*) -> str: if ps.cur is None: return None return ps.cur.str_val def _cur_lineno(ps: Parser*) -> int: if ps.cur is None: return 0 return ps.cur.lineno def _cur_col(ps: Parser*) -> int: if ps.cur is None: return 0 return ps.cur.col_offset def _skip_nl(ps: Parser*): """Skip TokenType.Nl token""" while ps.cur is not None and ps.cur.type == TokenType.Nl: _advance(ps) def _skip_nls_and_newlines(ps: Parser*): """Skip TokenType.Nl and TokenType.NewLine (used for line continuation)""" while ps.cur is not None: if ps.cur.type == TokenType.Nl or ps.cur.type == TokenType.NewLine: _advance(ps) else: break def _expect_type(ps: Parser*, ttype: int) -> int: """Expect the current token to be of the specified type, advance and return 1, otherwise return 0""" if _cur_type(ps) == ttype: _advance(ps) return 1 _record_parse_err(ps) ps.error_count += 1 return 0 def _expect_op(ps: Parser*, op_id: int) -> int: """Expecting the current one to be some operator""" if _cur_type(ps) == TokenType.Op and _cur_op(ps) == op_id: _advance(ps) return 1 _record_parse_err(ps) ps.error_count += 1 return 0 def _match_op(ps: Parser*, op_id: int) -> int: """Check but not consume the current token""" _tick(ps) if ps.aborted != 0: # Convergence: if tokens remain, "match" and jump to the end so both # `while not _match_op(...)` and `while _match_op(...)` end within <= 2 rounds if ps.cur is not None: ps.cur = None return 1 return 0 if _cur_type(ps) == TokenType.Op and _cur_op(ps) == op_id: return 1 return 0 def _accept_op(ps: Parser*, op_id: int) -> int: """If match, consume and return 1""" if _match_op(ps, op_id): _advance(ps) return 1 return 0 # ============================================================ # Constant Construction Helper (New API: Constant directly accepts lineno/col) # ============================================================ def _new_const_int(pool: memhub.MemManager*, val: int64, lineno: int, col: int, int_width: int = 0, int_unsigned: int = 0) -> AST*: return Constant(pool, CONST_INT, val, 0.0, None, lineno, col, int_width, int_unsigned) def _new_const_float(pool: memhub.MemManager*, val: double, lineno: int, col: int) -> AST*: global _g_dbg_pts, _g_dbg_vals, _g_dbg_cnt new_c: Constant* = Constant(pool, CONST_FLOAT, 0, val, None, lineno, col) # Store the numeric text in str_val (stdlib allocation, bypassing the pool overwrite of float_val); # REG-SRC recovers the original value with string.atof(str_val). lit_txt: str = stdlib.malloc(64) if lit_txt is not None: stdio.snprintf(lit_txt, 64, "%.17g", val) new_c.str_val = lit_txt # Record float node ptr->val into a stdlib table (value correct at construction; REG-SRC uses the # table to bypass the pool overwrite). Reset when full: old entries belong to finished files. if _g_dbg_pts is None: _g_dbg_pts = stdlib.malloc(4096 * 8) _g_dbg_vals = stdlib.malloc(4096 * 8) _g_dbg_cnt = 0 elif _g_dbg_cnt >= 4096: _g_dbg_cnt = 0 if _g_dbg_pts is not None and _g_dbg_vals is not None: _g_dbg_pts[_g_dbg_cnt] = uint64(new_c) _g_dbg_vals[_g_dbg_cnt] = val _g_dbg_cnt += 1 return new_c # Float-node construction-value table (stdlib allocated to avoid polluting the pool) _g_dbg_pts: uint64* = None _g_dbg_vals: double* = None _g_dbg_cnt: int = 0 # Whether the latest _dbg_lookup_lit_val hit _g_dbg_lit_found: int = 0 def _dbg_lookup_lit_val(ptr: uint64) -> double: """Look up the float-node construction-value table: return the original value (miss returns 0, found=0). Used to bypass the pool allocation overwriting the AST float_val field (correct at construction). """ global _g_dbg_lit_found _g_dbg_lit_found = 0 if _g_dbg_pts is None or _g_dbg_vals is None: return 0.0 for i in range(int(_g_dbg_cnt)): if _g_dbg_pts[i] == ptr: _g_dbg_lit_found = 1 return _g_dbg_vals[i] return 0.0 def _dbg_lit_found() -> int: """Check whether the latest _dbg_lookup_lit_val hit""" return _g_dbg_lit_found def _new_const_str(pool: memhub.MemManager*, val: str, lineno: int, col: int, is_dquote: int = 1, is_triple: int = 0, is_char: int = 0) -> AST*: """Create a string/character constant node is_dquote: 0 = single quote, non-0 = double quote (default double quote) is_triple: non-0 = triple-quoted string is_char: non-0 = single-character candidate (lexer has confirmed that after escaping it's exactly 1 byte, including '0') Semantic distinctions: - 'x' (single quote, single character, not triple-quoted) → CONST_CHAR (i8 value) - "x" (double quote, includes 0 terminator) → CONST_STR (i8* pointer, i8 x 2) - 'ab' (single quotes, multiple characters) → CONST_STR (i8* pointer) - '''x''' / \"\"\"x\"\"\" (triple quotes) → CONST_STR (i8* pointer) - '' / "" (empty string) → CONST_STR (i8* pointer) """ # Single quotes, non-triple quotes, single-character candidates marked by lexer → CONST_CHAR # Mark with is_char instead of strlen(val), because '0' becomes a NUL byte after escaping, strlen would return 0 if is_dquote == 0 and is_triple == 0 and is_char != 0: # Single quote # Single character → CONST_CHAR, int_val stores the character's ASCII value # val[0] is a legal value of 0 even if it's NUL ch_val: int64 = val[0] return Constant(pool, CONST_CHAR, ch_val, 0.0, val, lineno, col) # Other situations → CONST_STR return Constant(pool, CONST_STR, 0, 0.0, val, lineno, col) def _new_const_bool(pool: memhub.MemManager*, val: int, lineno: int, col: int) -> AST*: return Constant(pool, CONST_BOOL, val, 0.0, None, lineno, col) def _new_const_none(pool: memhub.MemManager*, lineno: int, col: int) -> AST*: return Constant(pool, CONST_NONE, 0, 0.0, None, lineno, col) # ============================================================ # ctx Helper (Recursive Fixing Store Context) # ============================================================ def _set_ctx(node: AST*, ctx: int): """Set ctx field of node (based on kind conversion)""" if node is None: return k: int = node.kind() if k == ASTKind.Name: nm: Name* = (Name*)(node) nm.ctx = ctx elif k == ASTKind.Attribute: at: Attribute* = (Attribute*)(node) at.ctx = ctx elif k == ASTKind.Subscript: sb: Subscript* = (Subscript*)(node) sb.ctx = ctx elif k == ASTKind.Tuple: tp: Tuple* = (Tuple*)(node) tp.ctx = ctx elif k == ASTKind.List: ls: List* = (List*)(node) ls.ctx = ctx elif k == ASTKind.Starred: st: Starred* = (Starred*)(node) st.ctx = ctx def _fix_store_ctx(node: AST*): """Fix ctx of store targets to be Store""" if node is None: return _set_ctx(node, ASTCtx.Store) k: int = node.kind() if k == ASTKind.Tuple or k == ASTKind.List: tp_elts: list[AST*]* = None if k == ASTKind.Tuple: tp: Tuple* = (Tuple*)(node) tp_elts = tp.elts else: ls: List* = (List*)(node) tp_elts = ls.elts if tp_elts is not None: n: sizet = tp_elts.__len__() for i in range(int(n)): cur: AST* = tp_elts.get(i) _fix_store_ctx(cur) elif k == ASTKind.Starred: st2: Starred* = (Starred*)(node) _fix_store_ctx(st2.value) # ============================================================ # Expression Parser # ============================================================ def _parse_expr(ps: Parser*) -> AST*: """expr: yield_expr | namedexpr_test""" if _cur_kw(ps) == Keyword.Yield: return _parse_yield(ps) return _parse_namedexpr(ps) def _parse_namedexpr(ps: Parser*) -> AST*: """namedexpr_test: test [':=' test]""" left: AST* = _parse_test(ps) if left is None: return None if _match_op(ps, TokOp.ColonEq): _advance(ps) right: AST* = _parse_test(ps) node: AST* = NamedExpr(ps.pool, left, right) _set_pos(node, left.lineno, left.col_offset, 0, 0) return node return left def _IsVolatileCallPrefix(ps: Parser*) -> int: """Test for a value-position `volatile f()` / `volatile S()` prefix, 1 = yes / 0 = no. `volatile` is a soft keyword consumed only in value position before a call (Name followed by '('); its use as an annotation descriptor (`a: volatile int`) or an identifier is unaffected. """ if _cur_kw(ps) != Keyword.Volatile: return 0 nxt: Token* = ps.cur.next if nxt is None or nxt.type != TokenType.Name: return 0 nxt2: Token* = nxt.next if nxt2 is None or nxt2.type != TokenType.Op or nxt2.op_subtype != TokOp.LPar: return 0 return 1 def _parse_test(ps: Parser*) -> AST*: """test: or_test ['if' or_test 'else' test] | lambdef""" if _cur_kw(ps) == Keyword.Lambda: return _parse_lambda(ps) # Value-position volatile call prefix: `a: int = volatile f()` / `volatile S()` -> consume volatile and # mark the call node (result slot accessed volatile, struct writes too; LLVM call has no volatile qualifier). if _IsVolatileCallPrefix(ps) != 0: _advance(ps) # consume volatile vinner: AST* = _parse_test(ps) if vinner is not None and vinner.kind() == ASTKind.Call: (Call*(vinner)).IsVolatile = 1 return vinner left: AST* = _parse_or_test(ps) if left is None: return None if _cur_kw(ps) == Keyword.If: _advance(ps) test: AST* = _parse_or_test(ps) if _cur_kw(ps) == Keyword.Else: _advance(ps) orelse: AST* = _parse_test(ps) node: AST* = IfExp(ps.pool, test, left, orelse) _set_pos(node, left.lineno, left.col_offset, 0, 0) return node ps.error_count += 1 return left return left def _parse_lambda(ps: Parser*) -> AST*: """lambdef: 'lambda' [varargslist] ':' test""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # lambda args: AST* = None if not _match_op(ps, TokOp.Colon): args = _parse_varargslist(ps, 1) if not _expect_op(ps, TokOp.Colon): return None body: AST* = _parse_test(ps) node: AST* = Lambda(ps.pool, args, body) _set_pos(node, lineno, col, 0, 0) return node def _parse_or_test(ps: Parser*) -> AST*: """or_test: and_test ('or' and_test)*""" left: AST* = _parse_and_test(ps) if left is None: return None values: list[AST*]* = None while True: # Supports both leading `or` ((A\n or B)) and trailing `or` (A or\n B) inside parens: # each round skips Nl before checking, otherwise A or B\n or C stops on Nl after B. _skip_nl(ps) if _cur_kw(ps) != Keyword.Or: break if values is None: values = list[AST*](ps.pool, 8) values.append(left) _advance(ps) _skip_nl(ps) # cross-line inside parens: a or\n b right: AST* = _parse_and_test(ps) if right is None: break values.append(right) if values is not None: node: AST* = BoolOp(ps.pool, OpKind.Or, values) _set_pos(node, left.lineno, left.col_offset, 0, 0) return node return left def _parse_and_test(ps: Parser*) -> AST*: """and_test: not_test ('and' not_test)*""" left: AST* = _parse_not_test(ps) if left is None: return None values: list[AST*]* = None while True: # Supports both leading `and` ((A\n and B)) and trailing `and` (A and\n B) inside parens: # each round skips Nl before checking, otherwise A and B\n and C stops on Nl after B. _skip_nl(ps) if _cur_kw(ps) != Keyword.And: break if values is None: values = list[AST*](ps.pool, 8) values.append(left) _advance(ps) _skip_nl(ps) # cross-line inside parens: a and\n b right: AST* = _parse_not_test(ps) if right is None: break values.append(right) if values is not None: node: AST* = BoolOp(ps.pool, OpKind.And, values) _set_pos(node, left.lineno, left.col_offset, 0, 0) return node return left def _parse_not_test(ps: Parser*) -> AST*: """not_test: 'not' not_test | comparison""" if _cur_kw(ps) == Keyword.Not: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) operand: AST* = _parse_not_test(ps) node: AST* = UnaryOp(ps.pool, OpKind.Not, operand) _set_pos(node, lineno, col, 0, 0) return node return _parse_comparison(ps) def _parse_comparison(ps: Parser*) -> AST*: """comparison: bitor_expr (comp_op bitor_expr)* Single comparison design: a < b → Compare(left=a, op=Lt, right=b) Chained comparison a < b < c → BoolOp(And, [Compare(a,<,b), Compare(b,<,c)]) """ left: AST* = _parse_bitor(ps) if left is None: return None _skip_nl(ps) # leading comparison operator inside parens: (a\n < b) op_id: int = _match_comp_op(ps) if op_id == OpKind.NoneOp: return left _advance_comp_op(ps) _skip_nl(ps) # cross-line inside parens: a ==\n b right: AST* = _parse_bitor(ps) # Single comparison: construct ops and comparators list ops: list[int]* = list[int](ps.pool, 8) ops.append(op_id) comparators: list[AST*]* = list[AST*](ps.pool, 8) comparators.append(right) first_cmp: AST* = Compare(ps.pool, left, ops, comparators) _set_pos(first_cmp, left.lineno, left.col_offset, 0, 0) # Check chained comparison _skip_nl(ps) # leading chained comparison inside parens: (a < b\n < c) next_op: int = _match_comp_op(ps) if next_op == OpKind.NoneOp: return first_cmp # Single comparison with chained conditions: a < b < c → BoolOp(And, [Compare(a,<,b), Compare(b,<,c), ...]) values: list[AST*]* = list[AST*](ps.pool, 8) values.append(first_cmp) cur_left: AST* = right while next_op != OpKind.NoneOp: _advance_comp_op(ps) _skip_nl(ps) # cross-line chained comparison inside parens cur_right: AST* = _parse_bitor(ps) ops2: list[int]* = list[int](ps.pool, 8) ops2.append(next_op) comparators2: list[AST*]* = list[AST*](ps.pool, 8) comparators2.append(cur_right) cmp: AST* = Compare(ps.pool, cur_left, ops2, comparators2) _set_pos(cmp, cur_left.lineno, cur_left.col_offset, 0, 0) values.append(cmp) cur_left = cur_right _skip_nl(ps) # leading chained comparison inside parens: (a < b\n < c) next_op = _match_comp_op(ps) node: AST* = BoolOp(ps.pool, OpKind.And, values) _set_pos(node, left.lineno, left.col_offset, 0, 0) return node def _match_comp_op(ps: Parser*) -> int: """Check if the current one is a comparison operator, return OP_* or OpKind.NoneOp""" if _cur_type(ps) == TokenType.Op: op_id: int = _cur_op(ps) if op_id == TokOp.EqEq: return OpKind.Eq if op_id == TokOp.ExclaimEq: return OpKind.Ne if op_id == TokOp.Less: return OpKind.Lt if op_id == TokOp.LessEq: return OpKind.Le if op_id == TokOp.Greater: return OpKind.Gt if op_id == TokOp.GreaterEq: return OpKind.Ge if _cur_kw(ps) == Keyword.Is: if ps.cur.next is not None and ps.cur.next.type == TokenType.Name and ps.cur.next.kw_subtype == Keyword.Not: return OpKind.IsNot return OpKind.Is if _cur_kw(ps) == Keyword.Not: if ps.cur.next is not None and ps.cur.next.kw_subtype == Keyword.In: return OpKind.NotIn if _cur_kw(ps) == Keyword.In: return OpKind.In return OpKind.NoneOp def _advance_comp_op(ps: Parser*): """Consume comparison operator token""" if _cur_type(ps) == TokenType.Op: _advance(ps) return if _cur_kw(ps) == Keyword.Is: _advance(ps) if _cur_kw(ps) == Keyword.Not: _advance(ps) return if _cur_kw(ps) == Keyword.Not: _advance(ps) if _cur_kw(ps) == Keyword.In: _advance(ps) return if _cur_kw(ps) == Keyword.In: _advance(ps) def _parse_bitor(ps: Parser*) -> AST*: left: AST* = _parse_bitxor(ps) if left is None: return None while True: _skip_nl(ps) # leading '|' inside parens: (a\n | b) if _match_op(ps, TokOp.VBar) == 0: break _advance(ps) _skip_nl(ps) # cross-line inside parens: a |\n b right: AST* = _parse_bitxor(ps) node: AST* = BinOp(ps.pool, left, OpKind.BitOr, right) _set_pos(node, left.lineno, left.col_offset, 0, 0) left = node return left def _parse_bitxor(ps: Parser*) -> AST*: left: AST* = _parse_bitand(ps) if left is None: return None while True: _skip_nl(ps) # leading '^' inside parens: (a\n ^ b) if _match_op(ps, TokOp.Caret) == 0: break _advance(ps) _skip_nl(ps) # cross-line inside parens: a ^\n b right: AST* = _parse_bitand(ps) node: AST* = BinOp(ps.pool, left, OpKind.BitXor, right) _set_pos(node, left.lineno, left.col_offset, 0, 0) left = node return left def _parse_bitand(ps: Parser*) -> AST*: left: AST* = _parse_shift(ps) if left is None: return None while True: _skip_nl(ps) # leading '&' inside parens: (a\n & b) if _match_op(ps, TokOp.Amp) == 0: break _advance(ps) _skip_nl(ps) # cross-line inside parens: a &\n b right: AST* = _parse_shift(ps) node: AST* = BinOp(ps.pool, left, OpKind.BitAnd, right) _set_pos(node, left.lineno, left.col_offset, 0, 0) left = node return left def _parse_shift(ps: Parser*) -> AST*: left: AST* = _parse_arith(ps) if left is None: return None while True: _skip_nl(ps) # leading '<<'/'>>' inside parens: (a\n << b) if _match_op(ps, TokOp.LtLt) == 0 and _match_op(ps, TokOp.GtGt) == 0: break op_token: int = _cur_op(ps) _advance(ps) _skip_nl(ps) # cross-line inside parens: a <<\n b right: AST* = _parse_arith(ps) ast_op: int = OpKind.LShift if op_token == TokOp.GtGt: ast_op = OpKind.RShift node: AST* = BinOp(ps.pool, left, ast_op, right) _set_pos(node, left.lineno, left.col_offset, 0, 0) left = node return left def _parse_arith(ps: Parser*) -> AST*: left: AST* = _parse_term(ps) if left is None: return None while True: _skip_nl(ps) # leading '+'/'-' inside parens: (a\n + b) if _match_op(ps, TokOp.Plus) == 0 and _match_op(ps, TokOp.Minus) == 0: break op_token: int = _cur_op(ps) _advance(ps) _skip_nl(ps) # cross-line inside parens: a +\n b right: AST* = _parse_term(ps) ast_op: int = OpKind.Add if op_token == TokOp.Minus: ast_op = OpKind.Sub node: AST* = BinOp(ps.pool, left, ast_op, right) _set_pos(node, left.lineno, left.col_offset, 0, 0) left = node return left # ============================================================ # C-style pointer suffix ('*' / '**') -- same source as TransPyC lib/vast _AtPtrSuffix # ============================================================ # Start position (line/col) of the last consumed C-style pointer suffix ('*' / '**'). # _parse_paren matches it against ps.prev to tell the paren-less cast `(T*)x` from a # plain group like `(a | b)`. Recorded as ints, not as a token pointer: comparing two # token pointers whose declared pointer levels differ is rejected by the compare check. _g_ptr_suffix_line: int = 0 _g_ptr_suffix_col: int = 0 def _ptr_suffix_end_op(op: int) -> int: """Whether the suffix-end token is a "no right operand" position (']' ',' ')' ':' '='). Multiplication/power would lack a right operand there anyway (a pre-existing syntax error), so treating '*'/'**' as a pointer suffix changes no valid expression (x[a * b] / a ** b unaffected). """ if op == TokOp.Rsqb or op == TokOp.Comma or op == TokOp.RPar \ or op == TokOp.Colon or op == TokOp.Equal: return 1 return 0 def _at_ptr_suffix(ps: Parser*, base: AST*) -> int: """Whether the current '*'/'**' run is a C-style pointer suffix applied to base. Rule (same as vast): base is a type atom (Name/Attribute/Subscript) and, skipping consecutive '*'/'**' and Nl, the next token is ']' / ',' / ')' / ':' / '='. Covers annotation and expression contexts. """ if base is None: return 0 k: int = base.kind() if k != ASTKind.Name and k != ASTKind.Attribute and k != ASTKind.Subscript: return 0 nxt: Token* = ps.cur stars: int = 0 while nxt is not None: if nxt.type == TokenType.Op \ and (nxt.op_subtype == TokOp.Star or nxt.op_subtype == TokOp.StarStar): stars += 1 nxt = nxt.next elif nxt.type == TokenType.Nl: nxt = nxt.next else: break if stars == 0 or nxt is None or nxt.type != TokenType.Op: return 0 return _ptr_suffix_end_op(nxt.op_subtype) def _make_ptr_type(ps: Parser*, base: AST*) -> AST*: """Build one pointer wrapper layer: PtrT[base] (builtin pointer marker).""" cptr: AST* = Name(ps.pool, "PtrT", ASTCtx.Load) node: AST* = _MakeTypeWrapper(ps, cptr, base) _set_pos(node, base.lineno, base.col_offset, 0, 0) return node def _consume_ptr_suffix(ps: Parser*, base: AST*) -> AST*: """Consume consecutive C-style pointer suffixes ('*' = 1 layer, '**' = 2), expanding into a base | rawptr chain.""" global _g_ptr_suffix_line, _g_ptr_suffix_col while True: _skip_nl(ps) if _match_op(ps, TokOp.StarStar): _advance(ps) _g_ptr_suffix_line = ps.prev.lineno _g_ptr_suffix_col = ps.prev.col_offset base = _make_ptr_type(ps, base) base = _make_ptr_type(ps, base) continue if _match_op(ps, TokOp.Star): _advance(ps) _g_ptr_suffix_line = ps.prev.lineno _g_ptr_suffix_col = ps.prev.col_offset base = _make_ptr_type(ps, base) continue break return base def _at_cast_operand(ps: Parser*) -> int: """Whether the current token may start the operand of a paren-less cast `(T*)x`. '(' is deliberately absent: `(T*)(x)` is already handled by the trailer loop as a cast call, and keeping it there leaves that path's AST untouched. Nl is not skipped either: a line break after ')' starts a new statement, not an operand. """ if _cur_type(ps) == TokenType.Name: return 1 if _cur_type(ps) == TokenType.Number: return 1 if _cur_type(ps) == TokenType.String: return 1 op: int = _cur_op(ps) if op == TokOp.Lsqb or op == TokOp.LBrace or op == TokOp.Star or op == TokOp.Amp or op == TokOp.Minus or op == TokOp.Plus or op == TokOp.Tilde: return 1 return 0 def _at_ptr_suffix_call(ps: Parser*, base: AST*) -> int: """Whether the current '*'/'**' run is a `T*(x)` pointer-suffix cast call. Rule (same as TPC lib/vast _AtPtrSuffixCall): (1) base is a type atom; (2) base and the star run are adjacent (same line, no whitespace/newline); (3) the run, internally adjacent, is immediately followed by '('. Required because `a * (b)` is valid multiplication, which a bare star rule would misread. """ if base is None: return 0 k: int = base.kind() if k != ASTKind.Name and k != ASTKind.Attribute and k != ASTKind.Subscript: return 0 nxt: Token* = ps.cur pv: Token* = ps.prev if pv is None or nxt is None: return 0 if pv.type == TokenType.Nl or pv.type == TokenType.NewLine: return 0 if pv.end_lineno != nxt.lineno or pv.end_col_offset != nxt.col_offset: return 0 # whitespace/newline between base and '*': not a C-style cast stars: int = 0 prev: Token* = None last: Token* = None while nxt is not None: if nxt.type == TokenType.Op \ and (nxt.op_subtype == TokOp.Star or nxt.op_subtype == TokOp.StarStar): if prev is not None \ and (nxt.lineno != prev.end_lineno or nxt.col_offset != prev.end_col_offset): return 0 # whitespace inside the star run: not adjacent, not a cast stars += 1 prev = nxt last = nxt nxt = nxt.next else: break if stars == 0 or last is None or nxt is None: return 0 if nxt.type != TokenType.Op or nxt.op_subtype != TokOp.LPar: return 0 if nxt.lineno != last.end_lineno or nxt.col_offset != last.end_col_offset: return 0 return 1 def _consume_ptr_suffix_call(ps: Parser*, base: AST*) -> AST*: """Parse a recognized `T*(...)` pointer-suffix cast as a call. Identical to the existing cast form `(T | rawptr)(...)`: a Call (func = the expanded base | rawptr chain, args/keywords = the parenthesized arguments); no second cast representation is introduced. """ func: AST* = _consume_ptr_suffix(ps, base) return _parse_call(ps, func) def _parse_term(ps: Parser*) -> AST*: left: AST* = _parse_factor(ps) if left is None: return None while True: _skip_nl(ps) # leading '*'/'/'/'//'/'%'/'@' inside parens: (a\n * b) if _match_op(ps, TokOp.Star) and _at_ptr_suffix(ps, left) != 0: left = _consume_ptr_suffix(ps, left) # C-style pointer suffix, not multiplication continue op_token: int = 0 if _match_op(ps, TokOp.Star): op_token = TokOp.Star elif _match_op(ps, TokOp.Slash): op_token = TokOp.Slash elif _match_op(ps, TokOp.DSlash): op_token = TokOp.DSlash elif _match_op(ps, TokOp.Percent): op_token = TokOp.Percent elif _match_op(ps, TokOp.At): op_token = TokOp.At if op_token == 0: break _advance(ps) _skip_nl(ps) # cross-line inside parens: a *\n b right: AST* = _parse_factor(ps) ast_op: int = _binop_from_op(op_token) node: AST* = BinOp(ps.pool, left, ast_op, right) _set_pos(node, left.lineno, left.col_offset, 0, 0) left = node return left def _parse_factor(ps: Parser*) -> AST*: """factor: '+' factor | '-' factor | '~' factor | '*' factor | '&' factor | power""" # A leading `*` in argument position was already parsed by _parse_call as a tightly bound Starred # operand; return it as this level's factor, before the unary test (so `*p + n` => `(*p) + n`). if ps.starred_seed is not None: seed: AST* = ps.starred_seed ps.starred_seed = None return seed if (_match_op(ps, TokOp.Plus) or _match_op(ps, TokOp.Minus) or _match_op(ps, TokOp.Tilde) or _match_op(ps, TokOp.Star) or _match_op(ps, TokOp.Amp)): op_token: int = _cur_op(ps) lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # Combined unary-minus + integer-literal overflow check: `-` is unary, so a magnitude-valid literal # may become < -2^63 after negation. Only a unary minus directly on an integer constant is checked here. if op_token == TokOp.Minus and _cur_type(ps) == TokenType.Number \ and ps.cur.is_float == 0 \ and _lex_int_neg_exceeds_i64(_cur_str(ps)) != 0: _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: ast_op = OpKind.USub elif op_token == TokOp.Tilde: ast_op = OpKind.Invert elif op_token == TokOp.Star: ast_op = OpKind.Deref elif op_token == TokOp.Amp: ast_op = OpKind.AddrOf node: AST* = UnaryOp(ps.pool, ast_op, operand) _set_pos(node, lineno, col, 0, 0) return node return _parse_power(ps) def _parse_power(ps: Parser*) -> AST*: """power: await_expr ['**' factor] | atom_expr ['**' factor]""" base: AST* = _parse_atom_expr(ps) if base is None: return None if _match_op(ps, TokOp.StarStar) and _at_ptr_suffix(ps, base) != 0: return _consume_ptr_suffix(ps, base) # C-style pointer suffix ('**' counts as 2 layers) if _match_op(ps, TokOp.StarStar): _advance(ps) exp: AST* = _parse_factor(ps) node: AST* = BinOp(ps.pool, base, OpKind.Pow, exp) _set_pos(node, base.lineno, base.col_offset, 0, 0) return node return base def _parse_atom_expr(ps: Parser*) -> AST*: """atom_expr: ['await'] atom trailer*""" if _cur_kw(ps) == Keyword.Await: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) inner: AST* = _parse_atom_expr(ps) node: AST* = Await(ps.pool, inner) _set_pos(node, lineno, col, 0, 0) return node base: AST* = _parse_atom(ps) if base is None: return None # trailer loop while True: if (_match_op(ps, TokOp.Star) or _match_op(ps, TokOp.StarStar)) \ and _at_ptr_suffix_call(ps, base) != 0: # `T*(x)` / `T**(x)` pointer-suffix cast: parsed as a cast call (same shape as # `(T | rawptr)(x)`). Consumed in the trailer loop, so subsequent `.attr` / `[i]` / `(…)` # are still attached by this same loop. base = _consume_ptr_suffix_call(ps, base) elif _match_op(ps, TokOp.Dot) or _match_op(ps, TokOp.QuestionDot): opt_dot: int = 1 if _cur_op(ps) == TokOp.QuestionDot else 0 _advance(ps) if _cur_type(ps) != TokenType.Name: ps.error_count += 1 break attr_name: str = _cur_str(ps) attr_lineno: int = _cur_lineno(ps) attr_col: int = _cur_col(ps) _advance(ps) ctx: int = ASTCtx.Load if _match_op(ps, TokOp.Equal): ctx = ASTCtx.Store elif _match_op(ps, TokOp.PlusEq) or _match_op(ps, TokOp.MinusEq): ctx = ASTCtx.Store base = Attribute(ps.pool, base, attr_name, ctx) if opt_dot != 0: (Attribute*(base)).optional = 1 _set_pos(base, attr_lineno, attr_col, 0, 0) elif _match_op(ps, TokOp.LPar): base = _parse_call(ps, base) elif _match_op(ps, TokOp.Lsqb): base = _parse_subscript(ps, base) else: break return base def _parse_call(ps: Parser*, func: AST*) -> AST*: """Parse function call trailer. Both args and keywords are lists.""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # ( _skip_nls_and_newlines(ps) # Skip the line break after '(' args: list[AST*]* = list[AST*](ps.pool, 8) keywords: list[AST*]* = list[AST*](ps.pool, 8) if not _match_op(ps, TokOp.RPar): while True: # **expr if _match_op(ps, TokOp.StarStar): _advance(ps) _skip_nls_and_newlines(ps) val: AST* = _parse_expr(ps) kw: AST* = KwNode(ps.pool, None, val) _set_pos(kw, _cur_lineno(ps), _cur_col(ps), 0, 0) keywords.append(kw) # *expr elif _match_op(ps, TokOp.Star): _advance(ps) _skip_nls_and_newlines(ps) # Bind only a unary factor (preserving `*p[0]` = `*(p[0])`), put it on starred_seed and let # _parse_expr continue the binary ops: `*p + n` => `(*p) + n`, not `*(p + n)`. seed: AST* = _parse_factor(ps) if seed is None: ps.error_count += 1 break star: AST* = Starred(ps.pool, seed, ASTCtx.Load) _set_pos(star, seed.lineno, seed.col_offset, 0, 0) ps.starred_seed = star val = _parse_expr(ps) args.append(val) # keyword = expr elif _cur_type(ps) == TokenType.Name: if ps.cur.next is not None and ps.cur.next.type == TokenType.Op and ps.cur.next.op_subtype == TokOp.Equal: kw_name: str = _cur_str(ps) kw_lineno: int = _cur_lineno(ps) kw_col: int = _cur_col(ps) _advance(ps) # name _advance(ps) # = _skip_nls_and_newlines(ps) val = _parse_expr(ps) kw = KwNode(ps.pool, kw_name, val) _set_pos(kw, kw_lineno, kw_col, 0, 0) keywords.append(kw) else: val = _parse_expr(ps) args.append(val) else: val = _parse_expr(ps) args.append(val) _skip_nls_and_newlines(ps) # Skip the line break after the parameter if not _accept_op(ps, TokOp.Comma): break _skip_nls_and_newlines(ps) # Skip the line break after the comma _skip_nls_and_newlines(ps) # Skip the line break before ')' _expect_op(ps, TokOp.RPar) node: AST* = Call(ps.pool, func, args, keywords) _set_pos(node, lineno, col, 0, 0) return node def _parse_subscript(ps: Parser*, value: AST*) -> AST*: """Parse subscript trailer.""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # [ _skip_nl(ps) slice_node: AST* = _parse_slice(ps) _expect_op(ps, TokOp.Rsqb) ctx: int = ASTCtx.Load if _match_op(ps, TokOp.Equal): ctx = ASTCtx.Store node: AST* = Subscript(ps.pool, value, slice_node, ctx) _set_pos(node, lineno, col, 0, 0) return node def _parse_slice(ps: Parser*) -> AST*: """Parse slice: [expr] ':' [expr] [':' [expr]] | expr | expr, expr, ...""" lower: AST* = None # Empty subscript (`T[]`): `[` directly followed by `]` => slice is None (the type side treats it as # an empty param list); calling _parse_expr here would report an unexpected token ']'. if not _match_op(ps, TokOp.Colon) and not _match_op(ps, TokOp.Rsqb): lower = _parse_slice_elem(ps) # Check if there’s a comma (tuple syntax: a[x, y, ...]) if _match_op(ps, TokOp.Comma): elts: list[AST*]* = list[AST*](ps.pool, 8) if lower is not None: elts.append(lower) while _accept_op(ps, TokOp.Comma): if _match_op(ps, TokOp.Rsqb): break # Trailing comma elem: AST* = _parse_slice_elem(ps) if elem is not None: elts.append(elem) lineno: int = 0 if lower is not None: lineno = lower.lineno node: AST* = Tuple(ps.pool, elts, ASTCtx.Load) _set_pos(node, lineno, 0, 0, 0) return node if _match_op(ps, TokOp.Colon): _advance(ps) upper: AST* = None if not _match_op(ps, TokOp.Colon) and not _match_op(ps, TokOp.Rsqb): upper = _parse_expr(ps) step: AST* = None if _accept_op(ps, TokOp.Colon): if not _match_op(ps, TokOp.Rsqb): step = _parse_expr(ps) lineno: int = 0 if lower is not None: lineno = lower.lineno elif upper is not None: lineno = upper.lineno else: lineno = _cur_lineno(ps) node: AST* = Slice(ps.pool, lower, upper, step) _set_pos(node, lineno, 0, 0, 0) return node return lower def _is_new_expr(ps: Parser*) -> int: """是否 `new` 描述符表达式(软关键字判定)。 `new` 是软关键字:只有紧跟一个**普通名**(描述符 hlife/slife/stack/heap/memgr 或类型名 `T(args)`)时才当关键字。`new = 1`、`new.x`、`new(1)`、`for new in ...` 都仍是标识符。 """ if _is_named_soft(ps, "new") == 0: return 0 nxt: Token* = ps.cur.next if nxt is None: return 0 if nxt.type != TokenType.Name: return 0 if nxt.kw_subtype != Keyword.NotKeyword: return 0 return 1 def _parse_new_expr(ps: Parser*) -> AST*: """`new [Descriptor...] T(args)`:描述符顺序无关、各轴最多一次(冲突即报错)。 结果恒为 `T*`;来源缺省 = heap(D3),生命期缺省由来源决定(语义侧落地)。 """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # 吃掉 `new` src: int = NEW_SRC_DEFAULT life: int = NEW_LIFE_DEFAULT memgr_arg: AST* = None while True: if _is_named_soft(ps, "hlife") != 0: if life != NEW_LIFE_DEFAULT: _record_parse_err(ps) ps.error_count += 1 return None life = NEW_LIFE_HLIFE _advance(ps) continue if _is_named_soft(ps, "slife") != 0: if life != NEW_LIFE_DEFAULT: _record_parse_err(ps) ps.error_count += 1 return None life = NEW_LIFE_SLIFE _advance(ps) continue if _is_named_soft(ps, "stack") != 0: if src != NEW_SRC_DEFAULT: _record_parse_err(ps) ps.error_count += 1 return None src = NEW_SRC_STACK _advance(ps) continue if _is_named_soft(ps, "heap") != 0: if src != NEW_SRC_DEFAULT: _record_parse_err(ps) ps.error_count += 1 return None src = NEW_SRC_HEAP _advance(ps) continue if _is_named_soft(ps, "memgr") != 0: if src != NEW_SRC_DEFAULT: _record_parse_err(ps) ps.error_count += 1 return None _advance(ps) # memgr if _expect_op(ps, TokOp.LPar) == 0: return None _skip_nl(ps) memgr_arg = _parse_test(ps) if memgr_arg is None: return None if _expect_op(ps, TokOp.RPar) == 0: return None src = NEW_SRC_MEMGR continue break # 类型 + 可选实参:类型用**注解规则**解析 —— 这样前导数组声明符 `[N]T`、指针后缀 `T*`、 # 泛型下标 `list[int]`、描述符前缀都与注解位置同口径;`T(args)` 的实参括号会被 `_parse_test` # 自身吃成 Call,故这里按"是不是 Call"分流,行为与旧版一致。 callee: AST* = _parse_type_ann(ps) if callee is None: _record_parse_err(ps) ps.error_count += 1 return None callee_kind: int = callee.kind() func_node: AST* = callee args_node: list[AST*]* = None if callee_kind == ASTKind.Call: cl: Call* = Call*(callee) func_node = cl.func args_node = cl.args elif callee_kind != ASTKind.Name and callee_kind != ASTKind.Attribute and callee_kind != ASTKind.Subscript: _record_parse_err(ps) ps.error_count += 1 return None node: New* = New(ps.pool, src, life, memgr_arg, func_node, args_node) _set_pos(node, lineno, col, 0, 0) return node def _parse_atom(ps: Parser*) -> AST*: """atom: the smallest unit of expression""" ttype: int = _cur_type(ps) lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) # `..` is reserved for the inline for-iterator range; reject it everywhere else. if _match_op(ps, TokOp.DotDot) != 0: _RecordRangeLitErr("inline range (`a..b`) is only allowed as a for-loop iterator", lineno, col) ps.error_count += 1 _advance(ps) return None if ttype == TokenType.Op and (_cur_op(ps) == TokOp.Lsqb or _cur_op(ps) == TokOp.LPar): if _BracketContainsDotDot(ps) != 0: _RecordRangeLitErr("inline range (`a..b`) is only allowed as a for-loop iterator", lineno, col) ps.error_count += 1 return None if ttype == TokenType.Name: # M2.4 `new` 描述符分配(软关键字):仅当后面确实跟着描述符/类型调用时才当关键字 if _is_new_expr(ps) != 0: return _parse_new_expr(ps) kw: int = _cur_kw(ps) if kw == Keyword.True_: _advance(ps) return _new_const_bool(ps.pool, 1, lineno, col) if kw == Keyword.False_: _advance(ps) return _new_const_bool(ps.pool, 0, lineno, col) if kw == Keyword.None_: _advance(ps) return _new_const_none(ps.pool, lineno, col) name_str: str = _cur_str(ps) _advance(ps) node: AST* = Name(ps.pool, name_str, ASTCtx.Load) _set_pos(node, lineno, col, 0, 0) return node if ttype == TokenType.Number: tok: Token* = ps.cur _advance(ps) if tok.is_float: return _new_const_float(ps.pool, tok.float_val, lineno, col) return _new_const_int(ps.pool, tok.int_val, lineno, col, tok.int_width, tok.int_unsigned) if ttype == TokenType.String: sval: str = _cur_str(ps) is_fstr: int = ps.cur.kw_subtype & 4 is_triple: int = ps.cur.kw_subtype & 8 is_dquote: int = ps.cur.kw_subtype & 16 is_char: int = ps.cur.kw_subtype & 32 _advance(ps) node: AST* if is_fstr: node = _parse_fstring(ps, sval, lineno, col) else: node = _new_const_str(ps.pool, sval, lineno, col, is_dquote, is_triple, is_char) # Python implicit adjacent string literal concatenation: "a" "b" -> "ab". # Nl is the in-paren newline token (allowed across lines); statement NewLine is not skipped. while True: _skip_nl(ps) if _cur_type(ps) != TokenType.String: break n_sval: str = _cur_str(ps) n_fstr: int = ps.cur.kw_subtype & 4 n_triple: int = ps.cur.kw_subtype & 8 n_dquote: int = ps.cur.kw_subtype & 16 n_char: int = ps.cur.kw_subtype & 32 _advance(ps) n_node: AST* if n_fstr: n_node = _parse_fstring(ps, n_sval, lineno, col) else: n_node = _new_const_str(ps.pool, n_sval, lineno, col, n_dquote, n_triple, n_char) node = _concat_str_nodes(ps.pool, node, n_node) return node if _match_op(ps, TokOp.Ellipsis): _advance(ps) return _new_const_none(ps.pool, lineno, col) if _match_op(ps, TokOp.LPar): return _parse_paren(ps) if _match_op(ps, TokOp.Lsqb): return _parse_list(ps) if _match_op(ps, TokOp.LBrace): return _parse_brace(ps) _record_parse_err(ps) ps.error_count += 1 return None def _parse_paren(ps: Parser*) -> AST*: """Parenthesis expressions: () | (expr) | (expr,) | (expr, ...) | (yield)""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # ( _skip_nl(ps) # Skip the line break after '(' (multi-line tuple) if _match_op(ps, TokOp.RPar): _advance(ps) # Empty tuple empty: list[AST*]* = list[AST*](ps.pool, 8) node: AST* = Tuple(ps.pool, empty, ASTCtx.Load) _set_pos(node, lineno, col, 0, 0) return node # yield if _cur_kw(ps) == Keyword.Yield: y: AST* = _parse_yield(ps) _expect_op(ps, TokOp.RPar) return y first: AST* = _parse_expr(ps) if _match_op(ps, TokOp.Comma): # Tuple elts: list[AST*]* = list[AST*](ps.pool, 8) elts.append(first) while _accept_op(ps, TokOp.Comma): _skip_nl(ps) if _match_op(ps, TokOp.RPar): break e: AST* = _parse_expr(ps) elts.append(e) _skip_nl(ps) # finish when the last element has no trailing comma (multi-line tuple) _expect_op(ps, TokOp.RPar) node = Tuple(ps.pool, elts, ASTCtx.Load) _set_pos(node, lineno, col, 0, 0) return node # Paren-less C cast `(T*)x`: the inner expression ends with a pointer suffix # (ps.prev is that suffix's '*'/'**' token) and the operand follows ')' directly. # Same product as `(T*)(x)` / `T*(x)`: a Call whose func is `T | rawptr`. is_ptr_cast: int = 0 if _g_ptr_suffix_line > 0 and _match_op(ps, TokOp.RPar) and ps.prev.lineno == _g_ptr_suffix_line and ps.prev.col_offset == _g_ptr_suffix_col: is_ptr_cast = 1 _skip_nl(ps) # (expr\n) close a single-expression paren spanning lines _expect_op(ps, TokOp.RPar) # Check for generator expression if _cur_kw(ps) == Keyword.For: return _parse_comprehension(ps, first, ASTKind.GeneratorExp, lineno, col) if is_ptr_cast != 0 and _at_cast_operand(ps) != 0: operand: AST* = _parse_factor(ps) cast_args: list[AST*]* = list[AST*](ps.pool, 8) cast_args.append(operand) cast_kws: list[AST*]* = list[AST*](ps.pool, 8) cast_node: AST* = Call(ps.pool, first, cast_args, cast_kws) _set_pos(cast_node, first.lineno, first.col_offset, 0, 0) return cast_node return first def _parse_list(ps: Parser*) -> AST*: """List expressions: [] | [expr_list] | [expr_list comp_for]""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # [ _skip_nl(ps) # Skip the line break after '[' (multi-line list) if _match_op(ps, TokOp.Rsqb): _advance(ps) empty: list[AST*]* = list[AST*](ps.pool, 8) node: AST* = List(ps.pool, empty, ASTCtx.Load) _set_pos(node, lineno, col, 0, 0) return node first: AST* = _parse_expr(ps) if _cur_kw(ps) == Keyword.For: node = _parse_comprehension(ps, first, ASTKind.ListComp, lineno, col) _expect_op(ps, TokOp.Rsqb) return node elts: list[AST*]* = list[AST*](ps.pool, 8) elts.append(first) while _accept_op(ps, TokOp.Comma): _skip_nl(ps) if _match_op(ps, TokOp.Rsqb): break e: AST* = _parse_expr(ps) elts.append(e) _skip_nl(ps) # finish when the last element has no trailing comma (multi-line list) _expect_op(ps, TokOp.Rsqb) node = List(ps.pool, elts, ASTCtx.Load) _set_pos(node, lineno, col, 0, 0) return node def _parse_brace(ps: Parser*) -> AST*: """Brace expressions: {} | {set_items} | {dict_items} | {comp_for}""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # { _skip_nl(ps) # Skip the line break after '{' (multi-line dict) if _match_op(ps, TokOp.RBrace): _advance(ps) empty_k: list[AST*]* = list[AST*](ps.pool, 8) empty_v: list[AST*]* = list[AST*](ps.pool, 8) node: AST* = Dict(ps.pool, empty_k, empty_v) _set_pos(node, lineno, col, 0, 0) return node first: AST* = _parse_expr(ps) # dict: key: value if _match_op(ps, TokOp.Colon): _advance(ps) val: AST* = _parse_expr(ps) if _cur_kw(ps) == Keyword.For: # dict comprehension gens: list[AST*]* = _parse_comp_for(ps) node = DictComp(ps.pool, first, val, gens) _set_pos(node, lineno, col, 0, 0) _expect_op(ps, TokOp.RBrace) return node keys: list[AST*]* = list[AST*](ps.pool, 8) values: list[AST*]* = list[AST*](ps.pool, 8) keys.append(first) values.append(val) while _accept_op(ps, TokOp.Comma): _skip_nl(ps) if _match_op(ps, TokOp.RBrace): break k: AST* = _parse_expr(ps) _expect_op(ps, TokOp.Colon) v: AST* = _parse_expr(ps) keys.append(k) values.append(v) _skip_nl(ps) # finish when the last element has no trailing comma _expect_op(ps, TokOp.RBrace) node = Dict(ps.pool, keys, values) _set_pos(node, lineno, col, 0, 0) return node # set if _cur_kw(ps) == Keyword.For: node = _parse_comprehension(ps, first, ASTKind.SetComp, lineno, col) _expect_op(ps, TokOp.RBrace) return node elts: list[AST*]* = list[AST*](ps.pool, 8) elts.append(first) while _accept_op(ps, TokOp.Comma): _skip_nl(ps) # cross-line set literal inside braces if _match_op(ps, TokOp.RBrace): break e: AST* = _parse_expr(ps) elts.append(e) _skip_nl(ps) # finish when the last element has no trailing comma _expect_op(ps, TokOp.RBrace) node = Set(ps.pool, elts) _set_pos(node, lineno, col, 0, 0) return node # ============================================================ # f-string parsing # ============================================================ def _parse_sub_expr(ps: Parser*, src: str) -> AST*: """Create a sub-lexer and parser to parse expressions (used for {expr} inside f-strings)""" pool: memhub.MemManager* = ps.pool lx: Lexer* = new_lexer(pool) if lx is None: return None _lexer_init(lx, src, pool) tokens: Token* = tokenize(lx) if tokens is None: return None sub_ps: Parser* = new_parser(pool) if sub_ps is None: return None _parser_init(sub_ps, tokens, pool) return _parse_test(sub_ps) def _append_str_value(pool: memhub.MemManager*, values: list[AST*]*, node: AST*): """Flatten a string node into values (JoinedStr expands its values, everything else is appended)""" if node is None: return if node.kind() == ASTKind.JoinedStr: js_val: JoinedStr* = (JoinedStr*)(node) if js_val.values is not None: n_v: sizet = js_val.values.__len__() i_v: sizet for i_v in range(int(n_v)): values.append(js_val.values.get(i_v)) else: values.append(node) def _concat_str_nodes(pool: memhub.MemManager*, a: AST*, b: AST*) -> AST*: """Merge adjacent string literal nodes (Python implicit concatenation "a" "b" -> "ab"). Pure constant strings are concatenated as text; f-strings (JoinedStr/FormattedValue) are merged into a JoinedStr values list. """ if a is None: return b if b is None: return a # Both are constant strings -> concatenate the text directly if a.kind() == ASTKind.Constant and b.kind() == ASTKind.Constant: ca_c: Constant* = (Constant*)(a) cb_c: Constant* = (Constant*)(b) if ca_c.const_kind == CONST_STR and cb_c.const_kind == CONST_STR \ and ca_c.str_val is not None and cb_c.str_val is not None: la_c: sizet = string.strlen(ca_c.str_val) lb_c: sizet = string.strlen(cb_c.str_val) buf_c: str = pool.alloc(la_c + lb_c + 1) if buf_c is not None: string.memcpy(buf_c, ca_c.str_val, la_c) string.memcpy(buf_c + la_c, cb_c.str_val, lb_c + 1) return _new_const_str(pool, buf_c, a.lineno, a.col_offset, 0, 0, 0) # Involves an f-string -> merge into a JoinedStr values_c: list[AST*]* = list[AST*](pool, 8) _append_str_value(pool, values_c, a) _append_str_value(pool, values_c, b) if values_c.__len__() == 1: return values_c.get(0) node_c: AST* = JoinedStr(pool, values_c) _set_pos(node_c, a.lineno, a.col_offset, 0, 0) return node_c def _parse_fstring(ps: Parser*, content: str, lineno: int, col: int) -> AST*: """Parse f-string content, building JoinedStr or FormattedValue nodes""" pool: memhub.MemManager* = ps.pool values: list[AST*]* = list[AST*](pool, 8) i: sizet = 0 content_len: sizet = string.strlen(content) # Literal text buffer for f-string content lit_buf: str = pool.alloc(content_len + 1) if lit_buf is None: return None lit_pos: sizet = 0 while i < content_len: ch: char = content[i] if ch == '{': # {{ → Literal { if i + 1 < content_len and content[i + 1] == '{': lit_buf[lit_pos] = '{' lit_pos += 1 i += 2 continue # Refresh literal buffer if lit_pos > 0: lit_buf[lit_pos] = '\0' lit_node: AST* = _new_const_str(pool, lit_buf, lineno, col) values.append(lit_node) lit_buf = pool.alloc(content_len + 1) if lit_buf is None: break lit_pos = 0 # Find matching closing '}' while tracking nested '{}' and string literals inside the f-string j: sizet = i + 1 depth: int = 1 conv_pos: sizet = 0 fmt_pos: sizet = 0 while j < content_len and depth > 0: c: char = content[j] if c == '\'' or c == '"': # Skip nested string literals inside an expression block (PEP 701 same-quote nesting) # so their characters do not confuse the {} depth / conversion / format delimiter scan nq: char = c j += 1 while j < content_len: c2: char = content[j] if c2 == '\\': j += 2 continue if c2 == nq: j += 1 break j += 1 continue if c == '{': depth += 1 elif c == '}': depth -= 1 if depth == 0: break elif depth == 1: if c == '!' and conv_pos == 0 and fmt_pos == 0: if j + 1 < content_len: nc: char = content[j + 1] if nc == 's' or nc == 'r' or nc == 'a': if j + 2 < content_len: after: char = content[j + 2] if after == ':' or after == '}': conv_pos = j elif j + 2 == content_len: conv_pos = j elif c == ':' and fmt_pos == 0: fmt_pos = j j += 1 if depth != 0: _record_parse_err(ps) ps.error_count += 1 break # Extract expression text expr_end: sizet = j if conv_pos > 0: expr_end = conv_pos elif fmt_pos > 0: expr_end = fmt_pos expr_len: sizet = expr_end - i - 1 if expr_len == 0: _record_parse_err(ps) ps.error_count += 1 i = j + 1 continue expr_buf: str = pool.alloc(expr_len + 1) if expr_buf is None: break string.memcpy(expr_buf, content + i + 1, expr_len) expr_buf[expr_len] = '\0' # Parse expression text expr_node: AST* = _parse_sub_expr(ps, expr_buf) # Conversion specifier conversion: int = -1 if conv_pos > 0: conv_char: char = content[conv_pos + 1] if conv_char == 's': conversion = 115 elif conv_char == 'r': conversion = 114 elif conv_char == 'a': conversion = 97 # Format specifier format_spec: AST* = None if fmt_pos > 0: spec_len: sizet = j - fmt_pos - 1 if spec_len > 0: spec_buf: str = pool.alloc(spec_len + 1) if spec_buf is not None: string.memcpy(spec_buf, content + fmt_pos + 1, spec_len) spec_buf[spec_len] = '\0' format_spec = _new_const_str(pool, spec_buf, lineno, col) fv: AST* = FormattedValue(pool, expr_node, conversion, format_spec) _set_pos(fv, lineno, col, 0, 0) values.append(fv) i = j + 1 elif ch == '}': if i + 1 < content_len and content[i + 1] == '}': lit_buf[lit_pos] = '}' lit_pos += 1 i += 2 else: _record_parse_err(ps) ps.error_count += 1 i += 1 else: lit_buf[lit_pos] = ch lit_pos += 1 i += 1 # Refresh remaining literal buffer if lit_pos > 0: lit_buf[lit_pos] = '\0' lit_node2: AST* = _new_const_str(pool, lit_buf, lineno, col) values.append(lit_node2) # If no content, return empty string if values.__len__() == 0: return _new_const_str(pool, "", lineno, col) # If only one FormattedValue and no literal, return FormattedValue if values.__len__() == 1: first: AST* = values.get(0) if first.kind() == ASTKind.FormattedValue: return first node: AST* = JoinedStr(pool, values) _set_pos(node, lineno, col, 0, 0) return node def _parse_comprehension(ps: Parser*, elt: AST*, vtype: int, lineno: int, col: int) -> AST*: """General comprehension: elt comp_for""" gens: list[AST*]* = _parse_comp_for(ps) node: AST* = None if vtype == ASTKind.ListComp: node = ListComp(ps.pool, elt, gens) elif vtype == ASTKind.SetComp: node = SetComp(ps.pool, elt, gens) else: node = GeneratorExp(ps.pool, elt, gens) _set_pos(node, lineno, col, 0, 0) return node def _parse_comp_for(ps: Parser*) -> list[AST*]*: """comp_for: ['async'] 'for' exprlist 'in' or_test [comp_if] (comp_for)* Returns generators list.""" gens: list[AST*]* = list[AST*](ps.pool, 8) while True: is_async: int = 0 if _cur_kw(ps) == Keyword.Async: is_async = 1 _advance(ps) if _cur_kw(ps) != Keyword.For: break lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) target: AST* = _parse_target_list(ps) if _cur_kw(ps) != Keyword.In: ps.error_count += 1 break _advance(ps) iter_: AST* = _parse_or_test(ps) # Collect ifs to list ifs: list[AST*]* = list[AST*](ps.pool, 8) while _cur_kw(ps) == Keyword.If: _advance(ps) cond: AST* = _parse_or_test(ps) ifs.append(cond) comp: AST* = Comprehension(ps.pool, target, iter_, ifs, is_async) _set_pos(comp, lineno, col, 0, 0) gens.append(comp) return gens def _parse_target_list(ps: Parser*) -> AST*: """Parse assignment target list (comma-separated)""" first: AST* = _parse_target(ps) if _match_op(ps, TokOp.Comma): elts: list[AST*]* = list[AST*](ps.pool, 8) elts.append(first) while _accept_op(ps, TokOp.Comma): if _cur_kw(ps) == Keyword.In: break e: AST* = _parse_target(ps) elts.append(e) node: AST* = Tuple(ps.pool, elts, ASTCtx.Store) _set_pos(node, first.lineno, first.col_offset, 0, 0) return node return first def _parse_target(ps: Parser*) -> AST*: """Parse assignment target""" if _match_op(ps, TokOp.Star): _advance(ps) inner: AST* = _parse_atom_expr(ps) node: AST* = Starred(ps.pool, inner, ASTCtx.Store) _set_pos(node, inner.lineno, inner.col_offset, 0, 0) return node if _match_op(ps, TokOp.LPar): _advance(ps) _skip_nl(ps) t_: AST* = _parse_target_list(ps) _skip_nl(ps) _expect_op(ps, TokOp.RPar) return t_ if _match_op(ps, TokOp.Lsqb): _advance(ps) _skip_nl(ps) first: AST* = _parse_target(ps) elts: list[AST*]* = list[AST*](ps.pool, 8) elts.append(first) while _accept_op(ps, TokOp.Comma): _skip_nl(ps) if _match_op(ps, TokOp.Rsqb): break e: AST* = _parse_target(ps) elts.append(e) _expect_op(ps, TokOp.Rsqb) node = List(ps.pool, elts, ASTCtx.Store) _set_pos(node, first.lineno, first.col_offset, 0, 0) return node # Normal atom_expr, ctx=Store node = _parse_atom_expr(ps) if node is not None: _set_ctx(node, ASTCtx.Store) return node def _parse_yield(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # yield if _cur_kw(ps) == Keyword.From: _advance(ps) val: AST* = _parse_expr(ps) node: AST* = YieldFrom(ps.pool, val) _set_pos(node, lineno, col, 0, 0) return node val2: AST* = None if not _match_op(ps, TokOp.RPar) and _cur_type(ps) != TokenType.NewLine and _cur_type(ps) != TokenType.EndMarker: if _cur_type(ps) == TokenType.Op and (_cur_op(ps) == TokOp.Comma or _cur_op(ps) == TokOp.Rsqb): pass else: val2 = _parse_test(ps) node = Yield(ps.pool, val2) _set_pos(node, lineno, col, 0, 0) return node def _parse_varargslist(ps: Parser*, is_lambda: int = 0) -> AST*: """Parse function parameter list New API: args/defaults/kw_defaults are lists, vararg/kwarg are fields.""" args: list[AST*]* = list[AST*](ps.pool, 8) vararg: AST* = None kwarg: AST* = None defaults: list[AST*]* = list[AST*](ps.pool, 8) kw_defaults: list[AST*]* = list[AST*](ps.pool, 8) while True: _skip_nl(ps) if _match_op(ps, TokOp.RPar): break # *args or * (kwonly separated) if _match_op(ps, TokOp.Star): _advance(ps) _skip_nl(ps) if _cur_type(ps) == TokenType.Name: vararg = _parse_arg_def(ps, is_lambda) if not _accept_op(ps, TokOp.Comma): break _skip_nl(ps) continue # **kwargs if _match_op(ps, TokOp.StarStar): _advance(ps) _skip_nl(ps) kwarg = _parse_arg_def(ps, is_lambda) if not _accept_op(ps, TokOp.Comma): break _skip_nl(ps) continue # / Delimiter (posonly end)— New API: posonly/regular are not distinguished if _match_op(ps, TokOp.Slash): _advance(ps) if not _accept_op(ps, TokOp.Comma): break _skip_nl(ps) continue # Normal parameter a: AST* = _parse_arg_def(ps, is_lambda) if a is None: break args.append(a) if _match_op(ps, TokOp.Equal): _advance(ps) d: AST* = _parse_test(ps) defaults.append(d) if not _accept_op(ps, TokOp.Comma): break _skip_nl(ps) return Arguments(ps.pool, args, vararg, kwarg, defaults, kw_defaults) def _parse_array_prefix_declarator(ps: Parser*) -> AST*: """C-style leading array declarator `[N]T` / `[]T` -> `t.ArrayT[T, N]` / `t.ArrayT[T, None]`. Consumed only in annotation position (_parse_type_ann); returns None if the current token is not '['. Desugars to the canonical array form shared with TPC / StubGen / DeclarationGenerator. Must be called before _parse_test, otherwise `[N]` is parsed as a list literal and the element type then errors. """ if _match_op(ps, TokOp.Lsqb) == 0: return None lb_lineno: int = _cur_lineno(ps) lb_col: int = _cur_col(ps) _advance(ps) # consume '[' count_node: AST* = None if _match_op(ps, TokOp.Rsqb) == 0: # Length expression (constant/constant expression; invalid lengths are rejected downstream) count_node = _parse_test(ps) if count_node is None: _record_parse_err(ps) return None if _match_op(ps, TokOp.Rsqb) == 0: _record_parse_err(ps) # missing ']' ps.error_count += 1 return None _advance(ps) # consume ']' # Element type: recurse into _parse_type_ann, so `[N]const int` / `[N]int*` share one rule source elem: AST* = _parse_type_ann(ps) if elem is None: return None if count_node is None: count_node = _new_const_none(ps.pool, lb_lineno, lb_col) # Element size must be explicitly 8 (list[T](pool, elem_size) addresses by byte stride; passing 2 # would write 8-byte pointers with a 2-byte stride -> heap overflow). Same arg as other list sites. elts: list[AST*]* = list[AST*](ps.pool, 8) elts.append(elem) elts.append(count_node) carray: AST* = Subscript(ps.pool, Name(ps.pool, "ArrayT", ASTCtx.Load), Tuple(ps.pool, elts, ASTCtx.Load), ASTCtx.Load) _set_pos(carray, lb_lineno, lb_col, 0, 0) return carray def _IsCTypedefAnnotation(ann: AST*) -> int: """1 if the annotation is the bare TypedefT marker. Used to parse a typedef's right-hand side as a **type**: `typedef X = const uint16*` synthesizes descriptors and the `*` suffix through _parse_type_ann (the annotation rule), so both sides agree. TPC's vast Parser._IsCTypedefAnnotation uses the same name-based test. """ if ann is None: return 0 if ann.kind() != ASTKind.Name: return 0 nm: Name* = Name*(ann) if nm.id is not None and string.strcmp(nm.id, "TypedefT") == 0: return 1 return 0 def _paren_is_type_group(ps: Parser*) -> int: """1 when the `(` at the cursor belongs to `( T )` rather than a tuple expression.""" t: Token* = ps.cur depth: int = 0 while t is not None: if t.type == TokenType.Op: if t.op_subtype == TokOp.LPar: depth += 1 elif t.op_subtype == TokOp.RPar: depth -= 1 if depth == 0: return 1 elif t.op_subtype == TokOp.Comma and depth == 1: return 0 t = t.next return 0 def _parse_paren_type(ps: Parser*) -> AST*: """Parse `( type )` inside an annotation; the caller keeps applying outer suffixes.""" _advance(ps) # ( _skip_nl(ps) inner: AST* = _parse_type_ann(ps) if inner is None: _record_parse_err(ps) return None _skip_nl(ps) _expect_op(ps, TokOp.RPar) return inner def _paren_has_type_syntax(ps: Parser*) -> int: """1 when the `(...)` group at the cursor contains `own` or `?` (type-only tokens).""" t: Token* = ps.cur depth: int = 0 while t is not None: if t.type == TokenType.Op: if t.op_subtype == TokOp.LPar: depth += 1 elif t.op_subtype == TokOp.RPar: depth -= 1 if depth == 0: return 0 elif t.op_subtype == TokOp.Question: return 1 elif t.type == TokenType.Name and t.str_val is not None \ and string.strcmp(t.str_val, "own") == 0: return 1 t = t.next return 0 def _parse_slice_elem(ps: Parser*) -> AST*: """One subscript element: `own T` is a type in any subscript, other type syntax only in annotations.""" if _cur_type(ps) == TokenType.Name: nm_se: str = _cur_str(ps) if nm_se is not None and string.strcmp(nm_se, "own") == 0: nxt_se: Token* = ps.cur.next if nxt_se is not None and (nxt_se.type == TokenType.Name or (nxt_se.type == TokenType.Op and (nxt_se.op_subtype == TokOp.Lsqb or nxt_se.op_subtype == TokOp.LPar))): return _parse_type_ann(ps) elif _match_op(ps, TokOp.LPar) != 0 and _paren_is_type_group(ps) != 0 \ and (ps.in_type_ann != 0 or _paren_has_type_syntax(ps) != 0): return _parse_type_ann(ps) return _parse_expr(ps) def _parse_type_ann(ps: Parser*) -> AST*: """Parse a type annotation: descriptor prefixes (const / volatile / __attribute__) and C-style pointer suffixes (* / **), field-identical to TPC's expansion. `static` is invalid here; missing type / non-type follower / bare use fail loudly. Reclaims the Mult/Pow junk _parse_test leaves and rebuilds rawptr chains. """ # Value-reference parameter prefix: `&T` (ref mode) / `&temp T` (temp mode). Recorded as a t.RefT / # t.TempRefT union member so the Gen layer can register call-site auto-address. This is the annotation # position only; the expression-position `&x` (AddrOf) stays in _parse_factor and is left untouched. # 回收伪错(见下 ptr_level 分支)时要连**位置/消息**一起回退,故这里读写解析错误通道。 global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg ref_marker: str = None if _accept_op(ps, TokOp.Amp) != 0: ref_marker = "RefT" if _cur_type(ps) == TokenType.Name: nm_ref: str = _cur_str(ps) if nm_ref is not None and string.strcmp(nm_ref, "temp") == 0: # `temp` is a soft keyword only when a type starts right after it: a type name # (`&temp int`, `&temp const int`) or a C-style array-prefix declarator # (`&temp [4]int`, aligned with TPC). Anything else keeps `temp` as the type name, # so a plain identifier named temp is unaffected. nxt_ref: Token* = ps.cur.next if nxt_ref is not None and (nxt_ref.type == TokenType.Name or (nxt_ref.type == TokenType.Op and nxt_ref.op_subtype == TokOp.Lsqb)): _advance(ps) ref_marker = "TempRefT" # Ownership prefix `own T`: soft keyword only when a type starts right after it, so a # plain identifier named `own` still works as a type name. own_marker: int = 0 if _cur_type(ps) == TokenType.Name: nm_own: str = _cur_str(ps) if nm_own is not None and string.strcmp(nm_own, "own") == 0: nxt_own: Token* = ps.cur.next if nxt_own is not None and (nxt_own.type == TokenType.Name or (nxt_own.type == TokenType.Op and (nxt_own.op_subtype == TokOp.Lsqb or nxt_own.op_subtype == TokOp.Amp or nxt_own.op_subtype == TokOp.LPar))): _advance(ps) own_marker = 1 # Descriptor prefix: collect const / volatile markers (soft keywords, consumed only here) mods: list[AST*]* = None mod_count: int = 0 mod_lineno: int = _cur_lineno(ps) mod_col: int = _cur_col(ps) # `static` modifier (consumed at statement start): append t.StaticT as the first descriptor marker # (pasted on the union's left later), then clear it so it applies only to this annotation. if ps.pending_static != 0: ps.pending_static = 0 if mods is None: mods = list[AST*](ps.pool, 8) st_mk: AST* = _MakeStaticMarker(ps) if st_mk is not None: mods.append(st_mk) mod_count += 1 while _IsDescriptorKeyword(ps) != 0 or _IsMagicAttributeCall(ps) != 0 \ or _IsStaticDescriptorKeyword(ps) != 0: if _IsStaticDescriptorKeyword(ps) != 0: # Annotation-position `static` (`name: static T`) is no longer supported: storage-class markers # are always prefixed. Fail loudly here (otherwise `static` becomes an opaque "static" class). _RecordDescriptorErr("static in annotation position is no longer supported: use the prefix form" " `static name: type = value` (equivalent to `name: t.StaticT | type`)", _cur_lineno(ps), _cur_col(ps)) _advance(ps) continue if mods is None: # Element size is explicitly 8: list[T](pool, elem_size) addresses by byte stride, so passing 4 # would make the 2nd 8-byte pointer (e.g. for `const volatile`) write out of bounds. mods = list[AST*](ps.pool, 8) if _IsMagicAttributeCall(ps) != 0: # C-style `a: __attribute__(section(".text.wo"), aligned(16)) const int = ...`: same position # and order-free mixing as const / volatile; each argument desugars to `t.attr.` and joins # the same markers list. # Use only _parse_call for this call itself, never _parse_test: the latter is a general expression # parser and would swallow the following `[]T` as a legal empty subscript, silently degrading the # annotation to a bare element type with zero errors reported. mka_lineno: int = _cur_lineno(ps) mka_col: int = _cur_col(ps) mka_fn: AST* = Name(ps.pool, "__attribute__", ASTCtx.Load) _set_pos(mka_fn, mka_lineno, mka_col, 0, 0) _advance(ps) # consume `__attribute__` ac_node: AST* = _parse_call(ps, mka_fn) # parse only `( args )` if ac_node is None: break if ac_node.kind() != ASTKind.Call: break ac: Call* = Call*(ac_node) if ac.args is not None: acn: sizet = len(ac.args) for aci in range(int(acn)): mk: AST* = _MakeTAttrMarker(ps, ac.args.get(aci)) if mk is not None: mods.append(mk) mod_count += 1 continue mods.append(_MakeDescriptorMarker(ps)) mod_count += 1 _advance(ps) # C-style leading array declarator `[N]T` / `[]T` (incl. `const [N]T`): desugar in place to # `t.ArrayT[T, N]` / `t.ArrayT[T, None]` and paste the already-collected descriptors onto it. if _match_op(ps, TokOp.Lsqb) != 0: arr: AST* = _parse_array_prefix_declarator(ps) if arr is None: return None if mod_count > 0: arr = _PrependDescriptorMarkers(ps, arr, mods, mod_count) return arr if mod_count > 0 and _cur_type(ps) != TokenType.Name \ and (_match_op(ps, TokOp.LPar) == 0 or _paren_is_type_group(ps) == 0): # Missing type after a descriptor (`a: const` / `a: const = 1` / `def f(a: const)`): # fail loudly -- never silently drop the descriptor. _RecordDescriptorErr("missing type after descriptor", mod_lineno, mod_col) ps.error_count += 1 return None before_ec: int = ps.error_count # 错误记录快照:_parse_test 会把"末尾 '*' 无右操作数"记成一处语法错误(见下方 ptr_level 回收), # 回收时连位置/消息一起回退,否则后面真正的语法错误会报出这个**过期**位置。 err_line_before: int = _g_parse_err_line err_col_before: int = _g_parse_err_col err_msg_before: str = _g_parse_err_msg base: AST* = None ps.in_type_ann += 1 if _match_op(ps, TokOp.LPar) != 0 and _paren_is_type_group(ps) != 0: # `(T)`: parentheses group a type; suffixes (`?`, `*`) then apply to the group. base = _parse_paren_type(ps) else: base = _parse_test(ps) ps.in_type_ann -= 1 if base is None: if mod_count > 0: # Descriptor consumed but type parsing failed (e.g. `a: const = 1`): fail loudly keeping the descriptor error. _RecordDescriptorErr("missing type after descriptor (type parse failed)", mod_lineno, mod_col) _record_parse_err(ps) # annotation parse failed: record position (error_count already bumped in _parse_test) return None # `T1 | T2` unions were retired: every former union form has dedicated syntax. # `T | None` is the exception and folds to `T` (an annotation is storage, not a value). resolved_union: AST* = _ResolveTypeUnion(ps, base) if resolved_union is None: return None base = resolved_union if mod_count > 0 and base.kind() == ASTKind.Constant: # Non-type follower (`a: const 1` / `a: const "x"` / `a: const True`): fail loudly, since # descriptors modify types only and constants are not types -- never silently degrade or drop. _RecordDescriptorErr("descriptor followed by a non-type (descriptors modify types only)", mod_lineno, mod_col) ps.error_count += 1 return None ptr_level: int = 0 # Reclaim the junk nodes left by _parse_test consuming the * / ** suffixes if base.kind() == ASTKind.BinOp: gb: BinOp* = (BinOp*)(base) if gb.op == OpKind.Mult and gb.right is None: ptr_level += 1 base = gb.left elif gb.op == OpKind.Pow: if gb.right is None: ptr_level += 2 base = gb.left elif gb.right.kind() == ASTKind.UnaryOp: gu: UnaryOp* = (UnaryOp*)(gb.right) if gu.op == OpKind.Deref and gu.operand is None: ptr_level += 3 base = gb.left if ptr_level > 0: ps.error_count = before_ec # reclaim succeeded: restore error count to avoid whole-file parse failure # 位置/消息同样要回退:这次回收解决的只有刚记下的那一处伪错(末尾 '*' 被当成乘法、右操作数缺失)。 # 只回退计数不回退位置,过期位置就会顶掉后面真正的语法错误(现场:EscapeTest 第 13 行 # `new stack char` 被报成第 6 行 `a: char*` 的 'unexpected token (none)',排查方向被完全带偏)。 # 判据用"记录是否在本次解析中改变":若快照本来就非 0(更早的真错误),当前值未变,回退是空操作。 if _g_parse_err_line != err_line_before: _g_parse_err_line = err_line_before _g_parse_err_col = err_col_before _g_parse_err_msg = err_msg_before # Consume remaining suffixes ('**' is one token counting as 2 layers, '*' as 1) while _match_op(ps, TokOp.StarStar): _advance(ps) ptr_level += 2 while _match_op(ps, TokOp.Star): _advance(ps) ptr_level += 1 # Build n nested PtrT[base] layers for li_an in range(int(ptr_level)): cptr_an: AST* = Name(ps.pool, "PtrT", ASTCtx.Load) base = _MakeTypeWrapper(ps, cptr_an, base) # Paste descriptors last (outermost wrappers) if mod_count > 0: base = _PrependDescriptorMarkers(ps, base, mods, mod_count) # Append the value-reference marker as the outermost wrapper: RefT[base] / TempRefT[base]. if ref_marker is not None: ref_marker_node: AST* = Name(ps.pool, ref_marker, ASTCtx.Load) base = _MakeTypeWrapper(ps, ref_marker_node, base) # Ownership prefix `own T`: OwnT[T] (one owned pointer layer). if own_marker != 0: own_node: AST* = Name(ps.pool, "OwnT", ASTCtx.Load) base = _MakeTypeWrapper(ps, own_node, base) # Nullable suffix `T?`: OptionT[T] (resolves to Ptr(T) when T is not a pointer already). # `Option[T]` is the explicit spelling of the same type. if _match_op(ps, TokOp.Question) != 0: _advance(ps) nul_marker_node: AST* = Name(ps.pool, "OptionT", ASTCtx.Load) base = _MakeTypeWrapper(ps, nul_marker_node, base) return base def _parse_arg_def(ps: Parser*, is_lambda: int = 0) -> AST*: """arg: TokenType.Name [':' annotation]""" if _cur_type(ps) != TokenType.Name: ps.error_count += 1 return None name_str: str = _cur_str(ps) lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _RejectReservedIdent(ps, name_str, "a parameter name", lineno, col) _advance(ps) annotation: AST* = None if is_lambda == 0: if _accept_op(ps, TokOp.Colon): annotation = _parse_type_ann(ps) result: AST* = Arg(ps.pool, name_str, annotation) _set_pos(result, lineno, col, 0, 0) return result # ============================================================ # Statement parsing (Design: directly append to parent, do not return a list) # ============================================================ def _parse_suite_into_children(ps: Parser*, parent: AST*, owner_is_class: int = 0): """Parse statement suite, directly append to parent's children. suite: TokenType.NewLine TokenType.Indent stmt+ TokenType.Dedent | simple_stmt owner_is_class: whether this suite's direct owner is a ClassDef. During suite parsing ps.in_class_body is set so _ParseVirtualModified can tell if `virtual def`/`novirtual def` is directly in a class body (function/branch/loop bodies do not count). Default 0 keeps all non-class owners correct. """ prev_in_class: int = ps.in_class_body ps.in_class_body = owner_is_class _skip_nl(ps) if _cur_type(ps) == TokenType.NewLine: _advance(ps) _skip_nl(ps) if _cur_type(ps) != TokenType.Indent: _record_parse_err(ps) ps.error_count += 1 if ps.recover != 0: _recover_add_err(ps) ps.in_class_body = prev_in_class return _advance(ps) sidx: int = 0 while _cur_type(ps) != TokenType.Dedent and _cur_type(ps) != TokenType.EndMarker: sidx += 1 # fail-fast anti-hang: over-budget steps (cyclic token list / cursor spinning) report and return if ps.step_count > ps.step_limit: _record_parse_err(ps) if ps.recover != 0: _recover_add_err(ps) return if ps.recover != 0: _recover_reset_err() before_cur: Token* = ps.cur before_err: int = ps.error_count before_errors: int = 0 if ps.recover != 0: before_errors = int(len(ps.errors)) # Decorator (class body methods can have decorators) if _match_op(ps, TokOp.At): ps.pending_decorators = _parse_decorators(ps) _parse_statement_into(ps, parent) if ps.recover != 0: if int(len(ps.errors)) > before_errors: pass elif ps.error_count > before_err: _recover_add_err(ps) ps.cur = before_cur _recover_sync(ps, before_cur.col_offset, 0) elif ps.cur is before_cur: _record_parse_err(ps) _recover_add_err(ps) _advance(ps) else: # Defensive: if parse did not advance token, force advance to avoid infinite loop if ps.cur is before_cur: ps.error_count += 1 _advance(ps) _skip_nl(ps) _expect_type(ps, TokenType.Dedent) ps.in_class_body = prev_in_class return # Single-line suite _parse_simple_stmt_into(ps, parent) ps.in_class_body = prev_in_class def _parse_statement_into(ps: Parser*, parent: AST*): """Parse statement (compound or simple), append to the parent.""" ttype: int = _cur_type(ps) kw: int = _cur_kw(ps) if ttype == TokenType.Name: # cif: compile-time conditional statement (soft keyword). Desugars to If(test=c.@CIf(EXPR)). if _is_cif_stmt(ps) != 0: cif_stmt: AST* = _parse_cif(ps) if cif_stmt is not None: parent.append(cif_stmt) return # celif/celse are valid only inside a cif chain; a bare occurrence is fail-fast. Skip the line. if _is_named_soft(ps, "celif") != 0 or _is_named_soft(ps, "celse") != 0: _RecordCifChainErr("celif/celse must be part of a single cif chain (they cannot stand alone, nor appear after the chain's celse)", _cur_lineno(ps), _cur_col(ps)) ps.error_count += 1 while _cur_type(ps) != TokenType.NewLine and _cur_type(ps) != TokenType.EndMarker: _advance(ps) return # define constant declaration: `define NAME[: T] = ` (no parentheses). Must run before # _parse_simple_stmt_into, otherwise `define` is parsed as an identifier expression and NAME is left # as a stray token. `define NAME(...)` (macro function) is excluded by _is_define_const's lookahead. if kw == Keyword.Define and _is_define_const(ps) != 0: dc_stmt: AST* = _parse_define_const(ps) if dc_stmt is not None: parent.append(dc_stmt) return # typedef alias declaration: `typedef T1 = T2` (statement-level reserved word, no lookahead since # typedef is never an identifier in existing sources). Invalid forms fail loudly in _parse_typedef. if _is_typedef(ps) != 0: td_stmt: AST* = _parse_typedef(ps) if td_stmt is not None: parent.append(td_stmt) return # export / extern variable declaration (soft keyword): `export NAME: T [= v]` / `extern NAME: T` # -> a module-level AnnAssign with storage-class markers; function forms are excluded by lookahead. if _is_export_extern_var(ps) != 0: ee_stmt: AST* = _parse_export_extern_var(ps) if ee_stmt is not None: parent.append(ee_stmt) return # pure modifier (soft keyword): statement start + pure + (class | def). Rewrites str/bytes # annotations to pure_str and string literals to _pure_from(...); synthesizes @PureT. if _IsPureModifier(ps) != 0: _ParsePureModified(ps, parent) return # virtual / novirtual modifier (soft keyword): only at statement start before class/def; synthesizes # @t.VTableT / @t.NoVTable then proceeds with class/def parsing. Placed before the class/def dispatch. if _IsVirtualModifier(ps) != 0: _ParseVirtualModified(ps, parent) return # inline modifier (soft keyword): only at statement start before a function definition; synthesizes # @t.always_inline then proceeds with def parsing (same pending_decorators channel as virtual). if _IsInlineModifier(ps) != 0: _ParseInlineModified(ps, parent) return # tls modifier (soft keyword): statement start + tls + def; synthesizes @t.TLS and reuses the # existing one-shot-init codegen path (FuncGenDef / ClassGenBase _has_decorator("TLS")). if _IsTlsModifier(ps) != 0: _ParseTlsModified(ps, parent) return # static modifier (soft keyword): statement start + static + (def | `Name :`). The function branch # synthesizes @t.StaticT (returns); the variable branch flags pending_static for _parse_type_ann. if _IsStaticModifier(ps) != 0: if _ParseStaticModified(ps, parent) != 0: return # Bare descriptor use: the whole statement is only `const` / `volatile` (missing type) -> fail loudly. # Matches only when followed by newline/EOF/semicolon (soft-keyword boundary), so `const = 5` / # `t.attr.const()` do not match. After recording, continue the generic simple_stmt dispatch. if _IsBareDescriptor(ps) != 0: _RecordDescriptorErr("bare descriptor use (the whole statement is only a descriptor, missing type)", _cur_lineno(ps), _cur_col(ps)) ps.error_count += 1 if kw == Keyword.If or kw == Keyword.While or kw == Keyword.For or kw == Keyword.Try: stmt: AST* = _parse_compound(ps) if stmt is not None: parent.append(stmt) return if kw == Keyword.With or _is_funcdef_kw(ps, kw) != 0 or kw == Keyword.Class \ or kw == Keyword.Enum or kw == Keyword.Union or kw == Keyword.Renum \ or kw == Keyword.Struct: stmt = _parse_compound(ps) if stmt is not None: parent.append(stmt) return if kw == Keyword.Match or kw == Keyword.Switch or kw == Keyword.Async: stmt = _parse_compound(ps) if stmt is not None: parent.append(stmt) return _parse_simple_stmt_into(ps, parent) def _parse_simple_stmt_into(ps: Parser*, parent: AST*): """simple_stmt: small_stmt (';' small_stmt)* [';'] TokenType.NewLine""" first: AST* = _parse_small_stmt(ps) if first is not None: parent.append(first) while _accept_op(ps, TokOp.Semi): if _cur_type(ps) == TokenType.NewLine or _cur_type(ps) == TokenType.EndMarker: break s: AST* = _parse_small_stmt(ps) if s is None: break parent.append(s) # First skip Nl (in-paren newline, e.g. between multi-line call args; a legal token) _skip_nl(ps) # Statement-end check: simple_stmt must end with a newline/EOF. Stray tokens (e.g. adjacent # expressions split into multiple statements, or a def missing its keyword) are a fail-fast syntax # error, reported Python-style (SyntaxError at line/col). if _cur_type(ps) != TokenType.NewLine and _cur_type(ps) != TokenType.EndMarker: # Stray tokens (e.g. adjacent expressions split into statements, or a def missing its keyword) are a # fail-fast syntax error. The message is no longer self-assembled: position/message go through the # parse error channel with prev/next tokens appended, output Python-style by the caller's # dump_parse_traceback (Traceback + File/line + source line + caret + SyntaxError). # Locate: walk the list from the head to find cur, recording prev and next token context. prev_tok: Token* = None walk_tok: Token* = ps.tokens while walk_tok is not None and walk_tok is not ps.cur: prev_tok = walk_tok walk_tok = walk_tok.next pv_str: str = "(head)" pv_line: int = 0 pv_col: int = 0 if prev_tok is not None: pv_str = prev_tok.str_val pv_line = prev_tok.lineno pv_col = prev_tok.col_offset nxt_tok: Token* = ps.cur.next nxt_str: str = "(none)" nxt_line: int = 0 nxt_col: int = 0 if nxt_tok is not None: nxt_str = nxt_tok.str_val nxt_line = nxt_tok.lineno nxt_col = nxt_tok.col_offset _record_parse_err_ctx(ps, _cur_type(ps), pv_str, pv_line, pv_col, nxt_str, nxt_line, nxt_col) ps.error_count += 1 return if _cur_type(ps) == TokenType.NewLine: _advance(ps) def _parse_small_stmt(ps: Parser*) -> AST*: """small_stmt: various simple statements""" kw: int = _cur_kw(ps) # fallthrough keyword (hard): legal only inside a switch case body ("fall to the next case"). # Legality is checked by the generator; writing it in a match or outside a switch is fail-fast. if kw == Keyword.FallThrough: ft_lineno: int = _cur_lineno(ps) ft_col: int = _cur_col(ps) _advance(ps) ft_node: AST* = FallThrough(ps.pool) _set_pos(ft_node, ft_lineno, ft_col, 0, 0) return ft_node if kw == Keyword.Pass: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) node: AST* = Pass(ps.pool) _set_pos(node, lineno, col, 0, 0) return node if kw == Keyword.Break: lineno = _cur_lineno(ps) col = _cur_col(ps) _advance(ps) node = Break(ps.pool) _set_pos(node, lineno, col, 0, 0) return node if kw == Keyword.Continue: lineno = _cur_lineno(ps) col = _cur_col(ps) _advance(ps) node = Continue(ps.pool) _set_pos(node, lineno, col, 0, 0) return node if kw == Keyword.Return: return _parse_return(ps) if kw == Keyword.Raise: return _parse_raise(ps) if kw == Keyword.Global: return _parse_global(ps) if kw == Keyword.Nonlocal: return _parse_nonlocal(ps) if kw == Keyword.Import: return _parse_import(ps) if kw == Keyword.From: return _parse_from_import(ps) if kw == Keyword.Del: return _parse_del(ps) if kw == Keyword.Assert: return _parse_assert(ps) if kw == Keyword.Yield: return _parse_expr_stmt(ps, _parse_yield(ps)) # Expression statement / assignment / augassign / annassign return _parse_expr_stmt(ps, None) def _maybe_tuple(ps: Parser*, first: AST*) -> AST*: """If there is a comma after first, collect the comma-separated expressions into a Tuple.""" if first is None: return None if not _match_op(ps, TokOp.Comma): return first lineno: int = first.lineno col: int = first.col_offset elts: list[AST*]* = list[AST*](ps.pool, 8) elts.append(first) _accept_op(ps, TokOp.Comma) while True: tt: int = _cur_type(ps) if tt == TokenType.NewLine or tt == TokenType.EndMarker: break if tt == TokenType.Op: op: int = _cur_op(ps) if op == TokOp.Equal or op == TokOp.Semi: break e: AST* = _parse_expr(ps) if e is None: break elts.append(e) if not _accept_op(ps, TokOp.Comma): break node: AST* = Tuple(ps.pool, elts, ASTCtx.Load) _set_pos(node, lineno, col, 0, 0) return node def _parse_testlist(ps: Parser*) -> AST*: """Parse a comma-separated list of expressions, if there’s more than one, form a tuple.""" first: AST* = _parse_expr(ps) return _maybe_tuple(ps, first) def _parse_expr_stmt(ps: Parser*, first: AST*) -> AST*: """Expression statement, handle assignment/augassign/annassign""" if first is None: first = _parse_testlist(ps) else: first = _maybe_tuple(ps, first) if first is None: return None lineno: int = first.lineno col: int = first.col_offset # Reserved keywords are statement syntax, never identifiers: reject them in a store target # (Name / unpacking tuple/list), regardless of which assignment form follows. _RejectReservedTargets(ps, first, "a variable name") # Assignment if _match_op(ps, TokOp.Equal): targets: list[AST*]* = list[AST*](ps.pool, 8) targets.append(first) _fix_store_ctx(first) value: AST* = None accept_result: int = _accept_op(ps, TokOp.Equal) while accept_result: v: AST* = _parse_testlist(ps) if v is None: break if value is not None: targets.append(value) _fix_store_ctx(value) value = v accept_result = _accept_op(ps, TokOp.Equal) node: AST* = Assign(ps.pool, targets, value) _set_pos(node, lineno, col, 0, 0) return node # augassign if _cur_type(ps) == TokenType.Op: op_id: int = _cur_op(ps) aug_op: int = _augop_from_op(op_id) if aug_op != OpKind.NoneOp: _advance(ps) value2: AST* = _parse_testlist(ps) _fix_store_ctx(first) node2: AST* = AugAssign(ps.pool, first, aug_op, value2) _set_pos(node2, lineno, col, 0, 0) return node2 # annassign: target ':' annotation [= value] if _match_op(ps, TokOp.Colon): _advance(ps) annotation: AST* = _parse_type_ann(ps) value3: AST* = None if _accept_op(ps, TokOp.Equal): # A typedef's right-hand side is a **type** (`const str` / `uint16*`), not a general # expression: parse it with the annotation rule so descriptors and C-style pointer # suffixes behave exactly as in annotation position (same as TPC's vast). if _IsCTypedefAnnotation(annotation) != 0: value3 = _parse_type_ann(ps) else: value3 = _parse_testlist(ps) if ps.pool is None: return None _fix_store_ctx(first) if ps.pool is None: return None node3: AST* = AnnAssign(ps.pool, first, annotation, value3, 1) _set_pos(node3, lineno, col, 0, 0) return node3 # Expression statement node4: AST* = Expr(ps.pool, first) _set_pos(node4, lineno, col, 0, 0) return node4 def _parse_return(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) val: AST* = None if _cur_type(ps) != TokenType.NewLine and _cur_type(ps) != TokenType.EndMarker and _cur_type(ps) != TokenType.Op: val = _parse_testlist(ps) elif _cur_type(ps) == TokenType.Op and _cur_op(ps) != TokOp.Semi: val = _parse_testlist(ps) node: AST* = Return(ps.pool, val) _set_pos(node, lineno, col, 0, 0) return node def _parse_raise(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) exc: AST* = None cause: AST* = None if _cur_type(ps) != TokenType.NewLine and _cur_type(ps) != TokenType.EndMarker: exc = _parse_test(ps) if _cur_kw(ps) == Keyword.From: _advance(ps) cause = _parse_test(ps) node: AST* = Raise(ps.pool, exc, cause) _set_pos(node, lineno, col, 0, 0) return node def _parse_global(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) names: list[AST*]* = list[AST*](ps.pool, 8) if _cur_type(ps) == TokenType.Name: n: AST* = Name(ps.pool, _cur_str(ps), ASTCtx.Store) _set_pos(n, _cur_lineno(ps), _cur_col(ps), 0, 0) names.append(n) _advance(ps) while _accept_op(ps, TokOp.Comma): if _cur_type(ps) != TokenType.Name: break n = Name(ps.pool, _cur_str(ps), ASTCtx.Store) _set_pos(n, _cur_lineno(ps), _cur_col(ps), 0, 0) names.append(n) _advance(ps) node: AST* = Global(ps.pool, names) _set_pos(node, lineno, col, 0, 0) return node def _parse_nonlocal(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) names: list[AST*]* = list[AST*](ps.pool, 8) if _cur_type(ps) == TokenType.Name: n: AST* = Name(ps.pool, _cur_str(ps), ASTCtx.Store) _set_pos(n, _cur_lineno(ps), _cur_col(ps), 0, 0) names.append(n) _advance(ps) while _accept_op(ps, TokOp.Comma): if _cur_type(ps) != TokenType.Name: break n = Name(ps.pool, _cur_str(ps), ASTCtx.Store) _set_pos(n, _cur_lineno(ps), _cur_col(ps), 0, 0) names.append(n) _advance(ps) node: AST* = Nonlocal(ps.pool, names) _set_pos(node, lineno, col, 0, 0) return node def _parse_import(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) names: list[AST*]* = _parse_alias_list(ps) node: AST* = Import(ps.pool, names) _set_pos(node, lineno, col, 0, 0) return node def _parse_from_import(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # Parse module (possibly with dots) level: int = 0 while _match_op(ps, TokOp.Dot): level += 1 _advance(ps) while _match_op(ps, TokOp.Ellipsis): level += 3 _advance(ps) module_name: str = None # The token type for keywords (like import) is also Name, so they must be excluded using kw_subtype, # otherwise the `import` in `from . import` would be mistakenly treated as a module_name. # Note: only `import` itself should be excluded. Module names can be keywords (like in `from .match import ...`, # where match is a Viper match statement keyword), requiring NotKeyword would cause # `from .match import` to fail parsing and the dependency scan would miss that module. if _cur_type(ps) == TokenType.Name and _cur_kw(ps) != Keyword.Import: module_name = _cur_str(ps) _advance(ps) while _match_op(ps, TokOp.Dot): _advance(ps) if _cur_type(ps) == TokenType.Name and _cur_kw(ps) == Keyword.NotKeyword: next_name: str = _cur_str(ps) mlen: int = int(string.strlen(module_name)) # 显式 cast:避免 64→32 隐式窄化 nlen: int = int(string.strlen(next_name)) # 显式 cast:避免 64→32 隐式窄化 new_mod: str = ps.pool.alloc(mlen + 1 + nlen + 1) if new_mod is not None: string.strcpy(new_mod, module_name) new_mod[mlen] = '.' string.strcpy(new_mod + mlen + 1, next_name) module_name = new_mod _advance(ps) if _cur_kw(ps) != Keyword.Import: ps.error_count += 1 return None _advance(ps) _skip_nl(ps) # import * if _match_op(ps, TokOp.Star): _advance(ps) star_alias: AST* = Alias(ps.pool, "*", None) _set_pos(star_alias, lineno, col, 0, 0) star_names: list[AST*]* = list[AST*](ps.pool, 8) star_names.append(star_alias) node: AST* = ImportFrom(ps.pool, module_name, star_names, level) _set_pos(node, lineno, col, 0, 0) return node # ( names ) or names if _match_op(ps, TokOp.LPar): _advance(ps) _skip_nl(ps) names: list[AST*]* = _parse_alias_list(ps) _skip_nl(ps) _expect_op(ps, TokOp.RPar) else: names = _parse_alias_list(ps) node: AST* = ImportFrom(ps.pool, module_name, names, level) _set_pos(node, lineno, col, 0, 0) return node def _parse_alias_list(ps: Parser*) -> list[AST*]*: """Parse alias list.""" names: list[AST*]* = list[AST*](ps.pool, 8) while True: if _cur_type(ps) != TokenType.Name: break name_str: str = _cur_str(ps) lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # dotted name while _match_op(ps, TokOp.Dot): _advance(ps) if _cur_type(ps) == TokenType.Name: next_n: str = _cur_str(ps) nmlen: int = int(string.strlen(name_str)) # 显式 cast:避免 64→32 隐式窄化 nxtlen: int = int(string.strlen(next_n)) # 显式 cast:避免 64→32 隐式窄化 new_name: str = ps.pool.alloc(nmlen + 1 + nxtlen + 1) if new_name is not None: string.strcpy(new_name, name_str) new_name[nmlen] = '.' string.strcpy(new_name + nmlen + 1, next_n) name_str = new_name _advance(ps) asname: str = None if _cur_kw(ps) == Keyword.As: _advance(ps) if _cur_type(ps) == TokenType.Name: asname = _cur_str(ps) as_lineno: int = _cur_lineno(ps) as_col: int = _cur_col(ps) # Module names may be keywords on purpose (`from .match import ...`), but the alias # names a new local identifier and must not be a reserved word. _RejectReservedIdent(ps, asname, "a module alias", as_lineno, as_col) _advance(ps) a: AST* = Alias(ps.pool, name_str, asname) _set_pos(a, lineno, col, 0, 0) names.append(a) if not _accept_op(ps, TokOp.Comma): break _skip_nl(ps) return names def _parse_del(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) targets: list[AST*]* = list[AST*](ps.pool, 8) while True: t_: AST* = _parse_atom_expr(ps) if t_ is None: break _set_ctx(t_, ASTCtx.Del) targets.append(t_) if not _accept_op(ps, TokOp.Comma): break node: AST* = Delete(ps.pool, targets) _set_pos(node, lineno, col, 0, 0) return node def _parse_assert(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) test: AST* = _parse_test(ps) msg: AST* = None if _accept_op(ps, TokOp.Comma): msg = _parse_test(ps) node: AST* = Assert(ps.pool, test, msg) _set_pos(node, lineno, col, 0, 0) return node # ============================================================ # Compound statements # ============================================================ def _parse_compound(ps: Parser*) -> AST*: kw: int = _cur_kw(ps) if kw == Keyword.If: return _parse_if(ps) if kw == Keyword.While: return _parse_while(ps) if kw == Keyword.For: return _parse_for(ps, 0) if kw == Keyword.Try: return _parse_try(ps) if kw == Keyword.With: return _parse_with(ps, 0) if _is_funcdef_kw(ps, kw) != 0: return _parse_funcdef(ps, 0) if kw == Keyword.Class: return _parse_classdef(ps) if kw == Keyword.Enum: return _parse_enumdef(ps) if kw == Keyword.Union: return _parse_uniondef(ps) if kw == Keyword.Renum: return _parse_renumdef(ps) if kw == Keyword.Struct: return _parse_structdef(ps) if kw == Keyword.Async: _advance(ps) if _is_funcdef_kw(ps, _cur_kw(ps)) != 0: return _parse_funcdef(ps, 1) if _cur_kw(ps) == Keyword.For: return _parse_for(ps, 1) if _cur_kw(ps) == Keyword.With: return _parse_with(ps, 1) ps.error_count += 1 return None if kw == Keyword.Match: return _parse_match(ps) if kw == Keyword.Switch: return _parse_switch(ps) ps.error_count += 1 return None # ============================================================ # cif / celif / celse compile-time conditional statements # ============================================================ # `cif EXPR:` is a soft-keyword statement desugared at parse time into # If(test=c.@CIf(EXPR), body, orelse). `celif`/`celse` extend the same chain and are recognized only # inside _parse_cif_tail. The marker attribute name '@CIf' is internal-only spelling (a leading '@' # cannot be produced by the lexer), so a user-written c.CIf(...) never matches it and is rejected. def _MakeCifMarker(ps: Parser*, test: AST*, lineno: int, col: int) -> AST*: """Build the internal marker call @CIf(test) that wraps a cif/celif condition.""" attr: AST* = Name(ps.pool, "@CIf", ASTCtx.Load) args: list[AST*]* = list[AST*](ps.pool, 4) if test is not None: args.append(test) kw: list[AST*]* = list[AST*](ps.pool, 1) marker: AST* = Call(ps.pool, attr, args, kw) _set_pos(marker, lineno, col, 0, 0) return marker def _RecordCifChainErr(detail: str, lineno: int, col: int): """Record an invalid cif/celif/celse chain (fail-fast; first only). Message states the fix. A single cif chain is cif ... celif ... celse: only `cif` opens a chain, and the chain must be terminated by `celse`; a plain else/elif cannot close it and celif/celse cannot stand alone. """ global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col buf: str = stdlib.malloc(1024) if buf is not None: stdio.snprintf(buf, 1024, "invalid cif chain (%s). cif/celif/celse form a single compile-time conditional chain:" " only `cif` opens a chain, `celif` extends the current cif chain, and the chain must be" " terminated by `celse` (a plain else/elif cannot close a cif chain, and celif/celse cannot" " stand alone). Correct form: cif : ... celif : ... celse: ...", detail) _g_parse_err_msg = buf else: _g_parse_err_msg = detail def _parse_cif(ps: Parser*) -> AST*: """Parse a `cif EXPR: ...` statement and its celif/celse chain (desugars to nested If nodes).""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # cif test: AST* = _parse_test(ps) _expect_op(ps, TokOp.Colon) empty: list[AST*]* = list[AST*](ps.pool, 8) node: If* = If(ps.pool, _MakeCifMarker(ps, test, lineno, col), empty) _parse_suite_into_children(ps, node) node.orelse = _parse_cif_tail(ps) _set_pos(node, lineno, col, 0, 0) return node def _parse_cif_tail(ps: Parser*) -> list[AST*]*: """Parse the celif/celse continuation of a cif chain and return the orelse list. celif: orelse = [nested If(test=c.@CIf(EXPR), ...)] (same chain, recurses). celse: orelse = statement list (a plain else; no marker). A plain elif/else after a cif chain is fail-fast (a cif chain must end with celse). """ if _is_named_soft(ps, "celif") != 0: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # celif test: AST* = _parse_test(ps) _expect_op(ps, TokOp.Colon) empty: list[AST*]* = list[AST*](ps.pool, 8) elif_node: If* = If(ps.pool, _MakeCifMarker(ps, test, lineno, col), empty) _parse_suite_into_children(ps, elif_node) elif_node.orelse = _parse_cif_tail(ps) _set_pos(elif_node, lineno, col, 0, 0) orelse: list[AST*]* = list[AST*](ps.pool, 8) orelse.append(elif_node) return orelse if _is_named_soft(ps, "celse") != 0: _advance(ps) # celse _expect_op(ps, TokOp.Colon) scratch: Module* = Module(ps.pool) _parse_suite_into_children(ps, scratch) return scratch.children # A plain elif/else cannot close a cif chain; consume and discard its clause to keep parsing. if _cur_kw(ps) == Keyword.Elif or _cur_kw(ps) == Keyword.Else: _RecordCifChainErr("a plain elif/else cannot follow a cif/celif branch; the cif chain must be terminated by celse", _cur_lineno(ps), _cur_col(ps)) ps.error_count += 1 is_elif: int = 0 if _cur_kw(ps) == Keyword.Elif: is_elif = 1 _advance(ps) # elif / else if is_elif != 0: _parse_test(ps) _expect_op(ps, TokOp.Colon) scratch2: Module* = Module(ps.pool) _parse_suite_into_children(ps, scratch2) return list[AST*](ps.pool, 8) return list[AST*](ps.pool, 8) def _parse_if(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # if test: AST* = _parse_test(ps) _expect_op(ps, TokOp.Colon) # First construct with empty orelse, then fill it in later empty: list[AST*]* = list[AST*](ps.pool, 8) node: If* = If(ps.pool, test, empty) _parse_suite_into_children(ps, node) orelse: list[AST*]* = _parse_if_tail(ps) node.orelse = orelse _set_pos(node, lineno, col, 0, 0) return node def _parse_if_tail(ps: Parser*) -> list[AST*]*: """Parse elif/else chain and return orelse list. elif: orelse = list containing single If node (nested) else: orelse = statement list """ # celif/celse belong to a cif chain only; after a plain if they are fail-fast (and discarded). if _is_named_soft(ps, "celif") != 0 or _is_named_soft(ps, "celse") != 0: _RecordCifChainErr("celif/celse follow a plain if statement (not a cif chain); a compile-time chain must start with cif", _cur_lineno(ps), _cur_col(ps)) ps.error_count += 1 is_celif: int = _is_named_soft(ps, "celif") _advance(ps) # celif / celse if is_celif != 0: _parse_test(ps) _expect_op(ps, TokOp.Colon) scratch_ce: Module* = Module(ps.pool) _parse_suite_into_children(ps, scratch_ce) return list[AST*](ps.pool, 8) if _cur_kw(ps) == Keyword.Elif: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) elif_test: AST* = _parse_test(ps) _expect_op(ps, TokOp.Colon) empty: list[AST*]* = list[AST*](ps.pool, 8) elif_node: If* = If(ps.pool, elif_test, empty) _parse_suite_into_children(ps, elif_node) elif_orelse: list[AST*]* = _parse_if_tail(ps) elif_node.orelse = elif_orelse _set_pos(elif_node, lineno, col, 0, 0) orelse: list[AST*]* = list[AST*](ps.pool, 8) orelse.append(elif_node) return orelse if _cur_kw(ps) == Keyword.Else: _advance(ps) _expect_op(ps, TokOp.Colon) scratch: Module* = Module(ps.pool) _parse_suite_into_children(ps, scratch) return scratch.children return list[AST*](ps.pool, 8) # empty list # ============================================================ # Parsing Compound Statements (Continued) # ============================================================ def _parse_while(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # while test: AST* = _parse_test(ps) _expect_op(ps, TokOp.Colon) empty: list[AST*]* = list[AST*](ps.pool, 8) node: While* = While(ps.pool, test, empty) _parse_suite_into_children(ps, node) if _cur_kw(ps) == Keyword.Else: _advance(ps) _expect_op(ps, TokOp.Colon) scratch: Module* = Module(ps.pool) _parse_suite_into_children(ps, scratch) node.orelse = scratch.children _set_pos(node, lineno, col, 0, 0) return node # _RecordRangeLitErr - record the first inline-range diagnostic. def _RecordRangeLitErr(msg: str, lineno: int, col: int): global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col # f-string messages point at the caller's stack buffer; copy before returning. n: sizet = string.strlen(msg) buf: str = stdlib.malloc(n + 1) if buf is not None: string.strcpy(buf, msg) _g_parse_err_msg = buf else: _g_parse_err_msg = msg # _LooksLikeRangeLit - non-destructive lookahead for `[a..b]` / `(a..b]` / ...,[step] # in a for-iterator position. Spaces are already dropped by the lexer. def _LooksLikeRangeLit(ps: Parser*) -> int: t: Token* = ps.cur if t is None or t.type != TokenType.Op: return 0 if t.op_subtype != TokOp.Lsqb and t.op_subtype != TokOp.LPar: return 0 s: Token* = t.next if s is not None and s.type == TokenType.Op and s.op_subtype == TokOp.Minus: s = s.next if s is None or s.type == TokenType.Op: return 0 s = s.next if s is None or s.type != TokenType.Op or s.op_subtype != TokOp.DotDot: return 0 s = s.next if s is not None and s.type == TokenType.Op and s.op_subtype == TokOp.Minus: s = s.next if s is None or s.type == TokenType.Op: return 0 s = s.next if s is not None and s.type == TokenType.Op and s.op_subtype == TokOp.Comma: s = s.next if s is not None and s.type == TokenType.Op and s.op_subtype == TokOp.Minus: s = s.next if s is None or s.type == TokenType.Op: return 0 s = s.next return 1 # _BracketContainsDotDot - 1 when the bracket group starting at ps.cur contains `..` # at its top level (used to reject inline ranges outside a for-iterator). def _BracketContainsDotDot(ps: Parser*) -> int: t: Token* = ps.cur if t is None or t.type != TokenType.Op: return 0 open_op: int = t.op_subtype if open_op != TokOp.Lsqb and open_op != TokOp.LPar: return 0 close_op: int = TokOp.Rsqb if open_op == TokOp.LPar: close_op = TokOp.RPar depth: int = 0 steps: int = 0 s: Token* = t while s != None and steps < 4096: if s.type == TokenType.Op: o: int = s.op_subtype if o == open_op: depth += 1 elif o == close_op: depth -= 1 if depth <= 0: return 0 elif o == TokOp.DotDot and depth == 1: return 1 s = s.next steps += 1 return 0 # _ParseRangeInt - one signed integer literal bound/step. def _ParseRangeInt(ps: Parser*, what: str) -> int64: neg: int = 0 if _match_op(ps, TokOp.Minus) != 0: neg = 1 _advance(ps) t: Token* = ps.cur if t is None or t.type != TokenType.Number or t.is_float != 0: _RecordRangeLitErr(f"inline range: {what} must be an integer literal", _cur_lineno(ps), _cur_col(ps)) ps.error_count += 1 return 0 v: int64 = t.int_val _advance(ps) if neg != 0: return -v return v # _ParseRangeLit - parse `[a..b]`, `(a..b]`, `[a..b)`, `(a..b, step]` and lower it to # range(start, stop, step), whose stop is always exclusive. def _ParseRangeLit(ps: Parser*) -> AST*: open_tok: Token* = ps.cur lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) lo_incl: int = 1 if open_tok.op_subtype == TokOp.Lsqb else 0 hi_incl: int = 1 _advance(ps) lo: int64 = _ParseRangeInt(ps, "lower bound") if _match_op(ps, TokOp.DotDot) == 0: _RecordRangeLitErr("inline range needs `..` between the two bounds", lineno, col) ps.error_count += 1 return None _advance(ps) hi: int64 = _ParseRangeInt(ps, "upper bound") step: int64 = 1 if _match_op(ps, TokOp.Comma) != 0: _advance(ps) step = _ParseRangeInt(ps, "step") cur_close: int = _cur_op(ps) if cur_close != TokOp.Rsqb and cur_close != TokOp.RPar: _RecordRangeLitErr("inline range: the closing bracket must be `]` or `)`", lineno, col) ps.error_count += 1 return None hi_incl = 1 if cur_close == TokOp.Rsqb else 0 _advance(ps) if step == 0: _RecordRangeLitErr("inline range: step cannot be 0", lineno, col) ps.error_count += 1 return None # normalize: exclusive start skips one step; inclusive end becomes range's exclusive stop start: int64 = lo if lo_incl == 0: start = lo + step stop: int64 = hi if hi_incl != 0: if step > 0: stop = hi + 1 else: stop = hi - 1 args: list[AST*]* = list[AST*](ps.pool, 8) if args is None: return None a0: AST* = Constant(ps.pool, CONST_INT, start, 0.0, None, lineno, col) a1: AST* = Constant(ps.pool, CONST_INT, stop, 0.0, None, lineno, col) a2: AST* = Constant(ps.pool, CONST_INT, step, 0.0, None, lineno, col) if a0 is None or a1 is None or a2 is None: return None args.append(a0) args.append(a1) args.append(a2) fn: AST* = Name(ps.pool, "range", ASTCtx.Load) if fn is None: return None empty_kw: list[AST*]* = list[AST*](ps.pool, 8) if empty_kw is None: return None node: Call* = Call(ps.pool, fn, args, empty_kw) if node is None: return None _set_pos(node, lineno, col, 0, 0) return node def _parse_for(ps: Parser*, is_async: int) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # for target: AST* = _parse_target_list(ps) _RejectReservedTargets(ps, target, "a loop variable name") if _cur_kw(ps) != Keyword.In: ps.error_count += 1 return None _advance(ps) iter_: AST* = None if _LooksLikeRangeLit(ps) != 0: iter_ = _ParseRangeLit(ps) if iter_ is None: # parse error already recorded; keep a placeholder so the AST is well-formed iter_ = Constant(ps.pool, CONST_NONE, 0, 0.0, None, lineno, col) else: iter_ = _parse_or_test(ps) _expect_op(ps, TokOp.Colon) empty: list[AST*]* = list[AST*](ps.pool, 8) node: For* = For(ps.pool, target, iter_, empty, is_async) _fix_store_ctx(target) _parse_suite_into_children(ps, node) if _cur_kw(ps) == Keyword.Else: _advance(ps) _expect_op(ps, TokOp.Colon) scratch: Module* = Module(ps.pool) _parse_suite_into_children(ps, scratch) node.orelse = scratch.children _set_pos(node, lineno, col, 0, 0) return node def _parse_try(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # try _expect_op(ps, TokOp.Colon) empty: list[AST*]* = list[AST*](ps.pool, 8) node: Try* = Try(ps.pool, empty, empty, empty) _parse_suite_into_children(ps, node) # handlers (except) handlers: list[AST*]* = list[AST*](ps.pool, 8) while _cur_kw(ps) == Keyword.Except: h: AST* = _parse_except_handler(ps) if h is not None: handlers.append(h) node.handlers = handlers # else if _cur_kw(ps) == Keyword.Else: _advance(ps) _expect_op(ps, TokOp.Colon) scratch: Module* = Module(ps.pool) _parse_suite_into_children(ps, scratch) node.orelse = scratch.children # finally if _cur_kw(ps) == Keyword.Finally: _advance(ps) _expect_op(ps, TokOp.Colon) scratch2: Module* = Module(ps.pool) _parse_suite_into_children(ps, scratch2) node.finalbody = scratch2.children _set_pos(node, lineno, col, 0, 0) return node def _parse_except_handler(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # except type_: AST* = None name: str = None if _cur_op(ps) != TokOp.Colon: type_ = _parse_test(ps) if _cur_kw(ps) == Keyword.As: _advance(ps) if _cur_type(ps) == TokenType.Name: name = _cur_str(ps) _advance(ps) _expect_op(ps, TokOp.Colon) node: AST* = ExceptHandler(ps.pool, type_, name) _parse_suite_into_children(ps, node) _set_pos(node, lineno, col, 0, 0) return node def _parse_with(ps: Parser*, is_async: int) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # with items: list[AST*]* = list[AST*](ps.pool, 8) # items while True: item: AST* = _parse_with_item(ps) if item is None: break items.append(item) if not _accept_op(ps, TokOp.Comma): break # Check if we can continue with a newline (with (...): form) if _cur_type(ps) == TokenType.NewLine or _cur_type(ps) == TokenType.Nl: break _expect_op(ps, TokOp.Colon) node: With* = With(ps.pool, items, is_async) _parse_suite_into_children(ps, node) _set_pos(node, lineno, col, 0, 0) return node def _parse_with_item(ps: Parser*) -> AST*: """with_item: test ['as' expr]""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) context_expr: AST* = _parse_or_test(ps) if context_expr is None: return None optional_vars: AST* = None if _cur_kw(ps) == Keyword.As: _advance(ps) optional_vars = _parse_target(ps) _fix_store_ctx(optional_vars) node: AST* = WithItem(ps.pool, context_expr, optional_vars) _set_pos(node, lineno, col, 0, 0) return node def _parse_funcdef(ps: Parser*, is_async: int) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) is_extern_fd: int = 0 is_export_fd: int = 0 is_define_fd: int = 0 extern_lib: str = None pre_name: str = None pre_name_lineno: int = 0 pre_name_col: int = 0 if _cur_kw(ps) == Keyword.Extern: # extern declaration: extern [def] A(...) [-> T] [optional ": pass"] is_extern_fd = 1 _advance(ps) # extern if _cur_kw(ps) == Keyword.Def: _advance(ps) # extern name(...): 函数名前面允许再写一个"库句柄"位置(必须是绑定到 ffilib.CDLL 的变量)。 # 不需要前瞻:先吞一个 Name,若其后仍是 Name 则前一个是库句柄,否则它就是函数名。 if _cur_type(ps) == TokenType.Name: cand_nm: str = _cur_str(ps) cand_ln: int = _cur_lineno(ps) cand_col: int = _cur_col(ps) _advance(ps) if _cur_type(ps) == TokenType.Name: extern_lib = cand_nm else: pre_name = cand_nm pre_name_lineno = cand_ln pre_name_col = cand_col elif _cur_kw(ps) == Keyword.Export: # export declaration: export [def] A(...) [-> T] [optional ": pass"] is_export_fd = 1 _advance(ps) # export if _cur_kw(ps) == Keyword.Def: _advance(ps) elif _cur_kw(ps) == Keyword.Define: # define macro function: syntax identical to def; t.DefineT is appended to the return annotation # (StubGen keeps it as-is -> C #define / alwaysinline) is_define_fd = 1 _advance(ps) # define else: _advance(ps) # def / fn / ... name: str = None fd_name_lineno: int = 0 fd_name_col: int = 0 if pre_name is not None: # extern :第一个 Name 是库句柄,已吞掉,当前 token 就是函数名 name = pre_name fd_name_lineno = pre_name_lineno fd_name_col = pre_name_col else: if _cur_type(ps) != TokenType.Name: ps.error_count += 1 return None name = _cur_str(ps) fd_name_lineno = _cur_lineno(ps) fd_name_col = _cur_col(ps) _advance(ps) _RejectReservedIdent(ps, name, "a function name", fd_name_lineno, fd_name_col) # PEP 695 generic function type parameters: def add[T](a: T, b: T) -- parsed and stored into # type_params, consistent with the class parser. type_params maps to int as a fallback during translation. type_params_fd: list[str]* = None if _accept_op(ps, TokOp.Lsqb): type_params_fd = list[str](ps.pool, 8) while not _match_op(ps, TokOp.Rsqb): if _cur_type(ps) == TokenType.EndMarker: break if _cur_type(ps) == TokenType.Name: tp_name_fd: str = _cur_str(ps) _advance(ps) # Skip the optional bound/constraint: [T: bound] or [T: (A, B)] if _match_op(ps, TokOp.Colon): _advance(ps) _parse_test(ps) type_params_fd.append(tp_name_fd) if not _accept_op(ps, TokOp.Comma): break _expect_op(ps, TokOp.Rsqb) args: AST* = None if _accept_op(ps, TokOp.LPar): args = _parse_varargslist(ps, 0) _expect_op(ps, TokOp.RPar) returns: AST* = None if _accept_op(ps, TokOp.RArrow): returns = _parse_type_ann(ps) if is_extern_fd: # extern declaration: wrap the return annotation as State[T], reusing the existing # State/external-C-function path (a lone State marker -> Void, no function body) state_attr_fd: AST* = Name(ps.pool, "State", ASTCtx.Load) if returns != None: returns = _MakeTypeWrapper(ps, state_attr_fd, returns) else: returns = state_attr_fd elif is_export_fd: # export declaration: wrap the return annotation as ExportT[T], reusing the existing # ExportT path (bare-name symbol, no module SHA1 header) export_attr_fd: AST* = Name(ps.pool, "ExportT", ASTCtx.Load) if returns != None: returns = _MakeTypeWrapper(ps, export_attr_fd, returns) else: returns = export_attr_fd elif is_define_fd: # define macro function: wrap the return annotation as DefineT[T], reusing the existing # DefineT path (StubGen keeps it as-is -> C #define / alwaysinline) define_attr_fd: AST* = Name(ps.pool, "DefineT", ASTCtx.Load) if returns != None: returns = _MakeTypeWrapper(ps, define_attr_fd, returns) else: returns = define_attr_fd dec_list: list[AST*]* = ps.pending_decorators ps.pending_decorators = None node: FunctionDef* = FunctionDef(ps.pool, name, args, dec_list, returns, is_async, type_params_fd) if is_extern_fd: node.flags = node.flags | FLAG_IS_EXTERN if is_export_fd: node.flags = node.flags | FLAG_IS_EXPORT if is_define_fd: node.flags = node.flags | FLAG_IS_DEFINE # extern name(...):记录库句柄,交给 Gen 层校验(必须绑定到 ffilib.CDLL) if extern_lib is not None: node.ffi_lib = extern_lib if (is_extern_fd == 0 and is_export_fd == 0 and is_define_fd == 0) or _match_op(ps, TokOp.Colon): _expect_op(ps, TokOp.Colon) _parse_suite_into_children(ps, node) else: # extern/export without ': pass' (extern A(...) -> int): empty body, declaration only. The NewLine # must be consumed here: the module/class statement loop only skips Nl, so a leftover NewLine would be # parsed as an expression next round and fail parse_tokens. if _cur_type(ps) == TokenType.NewLine: _advance(ps) _set_pos(node, lineno, col, 0, 0) return node def _parse_classdef(ps: Parser*) -> AST*: lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # class if _cur_type(ps) != TokenType.Name: ps.error_count += 1 return None name: str = _cur_str(ps) cd_name_lineno: int = _cur_lineno(ps) cd_name_col: int = _cur_col(ps) _RejectReservedIdent(ps, name, "a class name", cd_name_lineno, cd_name_col) _advance(ps) # Parse type parameters [T] or [T, U] (PEP 695 generic class syntax) type_params: list[str]* = None if _accept_op(ps, TokOp.Lsqb): type_params = list[str](ps.pool, 8) while not _match_op(ps, TokOp.Rsqb): if _cur_type(ps) == TokenType.EndMarker: break if _cur_type(ps) == TokenType.Name: tp_name: str = _cur_str(ps) _advance(ps) # Skip optional bound/constraint: [T: bound] or [T: (A, B)] if _match_op(ps, TokOp.Colon): _advance(ps) _parse_test(ps) type_params.append(tp_name) if not _accept_op(ps, TokOp.Comma): break _expect_op(ps, TokOp.Rsqb) bases: list[AST*]* = list[AST*](ps.pool, 8) keywords: list[AST*]* = list[AST*](ps.pool, 8) # bases / keywords if _accept_op(ps, TokOp.LPar): while not _match_op(ps, TokOp.RPar): # Cross-line base list inside parens (class X(A,\n B):): skip the newline after a comma, # otherwise cur=NL is not Name/Comma -> break -> _expect_op(RPar) reports an unexpected token. _skip_nl(ps) if _cur_type(ps) == TokenType.EndMarker: break # base or keyword=value if _cur_type(ps) == TokenType.Name: # Parse as expression, if followed by '=', then as keyword=value e: AST* = _parse_or_test(ps) if _match_op(ps, TokOp.Equal): _advance(ps) v: AST* = _parse_test(ps) kw_node: AST* = KwNode(ps.pool, e, v) keywords.append(kw_node) else: bases.append(e) if not _accept_op(ps, TokOp.Comma): break _skip_nl(ps) # finish the newline after the last base (class X(A,\n B):) _expect_op(ps, TokOp.RPar) _expect_op(ps, TokOp.Colon) dec_list: list[AST*]* = ps.pending_decorators ps.pending_decorators = None node: ClassDef* = ClassDef(ps.pool, name, bases, keywords, dec_list, type_params) # owner_is_class=1: ps.in_class_body=1 during the class body, so virtual/novirtual modifiers can tell # whether a method-level modifier is directly in the class body (see _ParseVirtualModified). _parse_suite_into_children(ps, node, 1) _set_pos(node, lineno, col, 0, 0) return node # ============================================================ # enum sugar (fully equivalent to `class X(t.EnumBase):`): `enum Name:` header + member lines # (`A` / `A: T` / `A = 0`). The synthesized ClassDef is field-identical to _parse_classdef output, so # downstream (ClassGenEnum._translate_enum_def -> EnumTable) has a single implementation path. # ============================================================ def _enum_member_annassign(ps: Parser*, owner: AST*, name_node: AST*, lineno: int, col: int, end_lineno: int, end_col: int) -> AST*: """Rewrite a single member-name node into a `Name: State` AnnAssign (the uniform enum member form). target=Name(id, Store), annotation=Name('State'), value=None, simple=1 -- field-identical to a hand-written enum member annotation. """ ann: AST* = Name(ps.pool, "State", ASTCtx.Load) _set_pos(ann, lineno, col, end_lineno, end_col) aa: AST* = AnnAssign(ps.pool, name_node, ann, None, 1) _fix_store_ctx(name_node) _set_pos(aa, lineno, col, end_lineno, end_col) aa.parent = owner # maintain the parent pointer like AST.append does return aa def _convert_enum_bare_members(ps: Parser*, node: ClassDef*): """Rewrite bare-name / comma-grouped member lines in an enum body into `Name: t.State` AnnAssigns. Member collection only recognizes Assign / AnnAssign Name targets, so bare names and comma groups (`A, B, C`) were silently dropped; now `A` -> `A: t.State` and `A, B, C` -> three AnnAssigns. Valued or annotated groups are not rewritten here and are caught by the registration-site fail-fast. """ children: list[AST*]* = node.children if children is None: return cn: sizet = len(children) # Rebuild the child list: one comma-group line -> several AnnAssigns. list has no insert, so rebuild # entirely; the assignment form matches AST.append in base.vp. node.children = list[AST*](ps.pool, 8) out: list[AST*]* = node.children for i in range(int(cn)): stmt: AST* = children.get(i) handled: int = 0 if stmt is not None and stmt.kind() == ASTKind.Expr: ex: Expr* = (Expr*)(stmt) if ex.value is not None and ex.value.kind() == ASTKind.Name: out.append(_enum_member_annassign(ps, node, ex.value, ex.lineno, ex.col_offset, ex.end_lineno, ex.end_col_offset)) handled = 1 elif ex.value is not None and ex.value.kind() == ASTKind.Tuple: tup: Tuple* = (Tuple*)(ex.value) if tup.elts is not None and len(tup.elts) >= 2: all_names: int = 1 k: sizet = 0 while k < len(tup.elts): el: AST* = tup.elts.get(k) if el is None or el.kind() != ASTKind.Name: all_names = 0 break k += 1 if all_names == 1: for k in range(int(len(tup.elts))): el2: AST* = tup.elts.get(k) out.append(_enum_member_annassign(ps, node, el2, el2.lineno, el2.col_offset, el2.end_lineno, el2.end_col_offset)) handled = 1 if handled == 0 and stmt is not None: out.append(stmt) def _parse_define_const(ps: Parser*) -> AST*: """`define NAME[: T] = ` -> an AnnAssign field-identical to `NAME: [T |] t.DefineT = `. Same DefineT registration path as hand-written `NAME: t.DefineT = value`. With a type, T goes before the union (`T | t.DefineT`), matching TPC's expansion. Must end with newline/EOF (fail-fast otherwise). """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # define if _cur_type(ps) != TokenType.Name: ps.error_count += 1 _record_parse_err(ps) return None name: str = _cur_str(ps) _RejectReservedIdent(ps, name, "a constant name", _cur_lineno(ps), _cur_col(ps)) _advance(ps) explicit_ann: AST* = None if _accept_op(ps, TokOp.Colon): explicit_ann = _parse_type_ann(ps) if explicit_ann is None: return None if _expect_op(ps, TokOp.Equal) == 0: return None value: AST* = _parse_test(ps) if value is None: ps.error_count += 1 _record_parse_err(ps) return None cdefine_marker: AST* = Name(ps.pool, "DefineT", ASTCtx.Load) annotation: AST* = cdefine_marker if explicit_ann is not None: annotation = _MakeTypeWrapper(ps, cdefine_marker, explicit_ann) target: AST* = Name(ps.pool, name, ASTCtx.Store) _set_pos(target, lineno, col, 0, 0) node: AST* = AnnAssign(ps.pool, target, annotation, value, 1) _set_pos(node, lineno, col, 0, 0) _skip_nl(ps) if _cur_type(ps) != TokenType.NewLine and _cur_type(ps) != TokenType.EndMarker: _record_parse_err(ps) ps.error_count += 1 return None if _cur_type(ps) == TokenType.NewLine: _advance(ps) return node def _parse_typedef(ps: Parser*) -> AST*: """`typedef T1 = T2` -> an AnnAssign field-identical to `T1: t.TypedefT = T2`. Same typedef registration/lookup path as the hand-written form (TypedefT branch -> register_ctypedef); no second implementation. Invalid forms (missing name / '=' / RHS) fail loudly; must end with newline/EOF. """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # typedef if _cur_type(ps) != TokenType.Name: # Missing alias (e.g. `typedef = 1` / `typedef : x`): fail loudly. _record_parse_err(ps) ps.error_count += 1 return None name: str = _cur_str(ps) _RejectReservedIdent(ps, name, "a type alias name", _cur_lineno(ps), _cur_col(ps)) _advance(ps) if _expect_op(ps, TokOp.Equal) == 0: # Missing '=' (e.g. `typedef A`): fail loudly. return None # The sugar's RHS is the same type position as `T1: t.TypedefT = T2`: parse it with the # annotation rule so `typedef X = const str` / `= uint16*` match the hand-written form. value: AST* = _parse_type_ann(ps) if value is None: # Missing RHS (e.g. `typedef A = `): fail loudly. ps.error_count += 1 _record_parse_err(ps) return None ctypedef_marker: AST* = Name(ps.pool, "TypedefT", ASTCtx.Load) target: AST* = Name(ps.pool, name, ASTCtx.Store) _set_pos(target, lineno, col, 0, 0) node: AST* = AnnAssign(ps.pool, target, ctypedef_marker, value, 1) _set_pos(node, lineno, col, 0, 0) _skip_nl(ps) if _cur_type(ps) != TokenType.NewLine and _cur_type(ps) != TokenType.EndMarker: _record_parse_err(ps) ps.error_count += 1 return None if _cur_type(ps) == TokenType.NewLine: _advance(ps) return node def _parse_export_extern_var(ps: Parser*) -> AST*: """`export NAME: T [= v]` / `extern NAME: T` -> a module-level AnnAssign with storage-class markers. export rewrites the annotation to `T | t.ExportT` (same marker/path as `export def`: bare name, no module SHA1 header) and allows an initializer; extern rewrites to `T | t.State` (declaration + bare name) and forbids an initializer (`extern a: int = 1` fails loudly). Must end with newline/EOF. """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) is_extern: int = 0 if _cur_kw(ps) == Keyword.Extern: is_extern = 1 _advance(ps) # export / extern if _cur_type(ps) != TokenType.Name: # Missing variable name (e.g. `export : int`): fail loudly. _record_parse_err(ps) ps.error_count += 1 return None name: str = _cur_str(ps) _RejectReservedIdent(ps, name, "a variable name", _cur_lineno(ps), _cur_col(ps)) _advance(ps) if _expect_op(ps, TokOp.Colon) == 0: # Missing ':' (e.g. `export x`): fail loudly. return None ann: AST* = _parse_type_ann(ps) if ann is None: ps.error_count += 1 _record_parse_err(ps) return None # Storage-class marker: export -> ExportT[T] (bare-name export); extern -> State[T] (declaration + bare name) storage_attr: AST* = Name(ps.pool, "State" if is_extern != 0 else "ExportT", ASTCtx.Load) annotation: AST* = _MakeTypeWrapper(ps, storage_attr, ann) value: AST* = None if _match_op(ps, TokOp.Equal): if is_extern != 0: # An extern variable is externally defined: no initializer allowed in this module. _RecordDescriptorErr("extern variable declaration cannot have an initializer (the symbol is defined externally)", lineno, col) ps.error_count += 1 return None _advance(ps) value = _parse_test(ps) if value is None: ps.error_count += 1 _record_parse_err(ps) return None target: AST* = Name(ps.pool, name, ASTCtx.Store) _set_pos(target, lineno, col, 0, 0) node: AST* = AnnAssign(ps.pool, target, annotation, value, 1) _set_pos(node, lineno, col, 0, 0) _skip_nl(ps) if _cur_type(ps) != TokenType.NewLine and _cur_type(ps) != TokenType.EndMarker: _record_parse_err(ps) ps.error_count += 1 return None if _cur_type(ps) == TokenType.NewLine: _advance(ps) return node def _parse_enumdef(ps: Parser*) -> AST*: """enum NAME: sugar -> a ClassDef field-identical to `class NAME(t.EnumBase):`. Member forms (same as TPC's _ExpandEnumBlocks): bare `A` -> `A: t.State` (auto-numbered, i32 width); comma group `A, B` -> several `A: t.State` / `B: t.State`; annotated `A: T` and valued `A = 0` as-is. Base is always Attribute(Name('t'), 'EnumBase'), field-identical to `class X(t.EnumBase):`. Line/col inherited from each member statement. """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # enum if _cur_type(ps) != TokenType.Name: ps.error_count += 1 return None name: str = _cur_str(ps) _RejectReservedIdent(ps, name, "a type name", _cur_lineno(ps), _cur_col(ps)) _advance(ps) bases: list[AST*]* = list[AST*](ps.pool, 8) bases.append(Name(ps.pool, "EnumBase", ASTCtx.Load)) keywords: list[AST*]* = list[AST*](ps.pool, 8) dec_list: list[AST*]* = ps.pending_decorators ps.pending_decorators = None _expect_op(ps, TokOp.Colon) node: ClassDef* = ClassDef(ps.pool, name, bases, keywords, dec_list, None) _parse_suite_into_children(ps, node) _convert_enum_bare_members(ps, node) _set_pos(node, lineno, col, 0, 0) return node def _parse_uniondef(ps: Parser*) -> AST*: """union NAME: sugar -> a ClassDef field-identical to `class NAME(t.UnionBase):`. Members are parsed as a normal class body (AnnAssign). Base is always Attribute(Name('t'), 'UnionBase'), field-identical to `class X(t.UnionBase):`; TPC detects unions via `issubclass(cls, t.UnionBase)`, so both sides share the existing union path (offset 0, size = widest member). No bare-name conversion. """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # union if _cur_type(ps) != TokenType.Name: ps.error_count += 1 return None name: str = _cur_str(ps) _RejectReservedIdent(ps, name, "a type name", _cur_lineno(ps), _cur_col(ps)) _advance(ps) bases: list[AST*]* = list[AST*](ps.pool, 8) bases.append(Name(ps.pool, "UnionBase", ASTCtx.Load)) keywords: list[AST*]* = list[AST*](ps.pool, 8) dec_list: list[AST*]* = ps.pending_decorators ps.pending_decorators = None _expect_op(ps, TokOp.Colon) node: ClassDef* = ClassDef(ps.pool, name, bases, keywords, dec_list, None) _parse_suite_into_children(ps, node) _set_pos(node, lineno, col, 0, 0) return node def _parse_renumdef(ps: Parser*) -> AST*: """renum NAME: sugar -> a ClassDef field-identical to `class NAME(t.REnum):`. Members are parsed as a normal class body: a variant is a nested ClassDef (`class Some:` + field annotations) and a payload-less variant is a plain assignment (`None_ = 0`). Base is always Attribute(Name('t'), 'REnum'); TPC detects REnum via `issubclass(cls, t.REnum)`. No bare-name conversion. """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # renum if _cur_type(ps) != TokenType.Name: ps.error_count += 1 return None name: str = _cur_str(ps) _RejectReservedIdent(ps, name, "a type name", _cur_lineno(ps), _cur_col(ps)) _advance(ps) # Type parameters [T] / [T, U] (same as class/struct). type_params: list[str]* = None if _accept_op(ps, TokOp.Lsqb): type_params = list[str](ps.pool, 8) while not _match_op(ps, TokOp.Rsqb): if _cur_type(ps) == TokenType.EndMarker: break if _cur_type(ps) == TokenType.Name: tp_name: str = _cur_str(ps) _advance(ps) if _match_op(ps, TokOp.Colon): _advance(ps) _parse_test(ps) type_params.append(tp_name) if not _accept_op(ps, TokOp.Comma): break _expect_op(ps, TokOp.Rsqb) bases: list[AST*]* = list[AST*](ps.pool, 8) bases.append(Name(ps.pool, "REnum", ASTCtx.Load)) keywords: list[AST*]* = list[AST*](ps.pool, 8) dec_list: list[AST*]* = ps.pending_decorators ps.pending_decorators = None _expect_op(ps, TokOp.Colon) node: ClassDef* = ClassDef(ps.pool, name, bases, keywords, dec_list, type_params) _parse_suite_into_children(ps, node) _set_pos(node, lineno, col, 0, 0) return node def _RecordStructErr(detail: str, lineno: int, col: int, extra: str): """Record an invalid struct form (fail-fast; first only), same reporting path as _RecordVirtualErr. Messages always use f-string interpolation: this language's str is i8*, so `str + str` degrades to raw pointer add and illegal IR. """ global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col buf: str = stdlib.malloc(1536) if buf is not None: stdio.snprintf(buf, 1536, "[VPC] invalid struct definition (%s). A struct has the same shape as a class but **can only represent a data type**:" "(1) the virtual / novirtual modifier is forbidden (a struct has no vtable by nature, @t.NoVTable is built in);" "(2) defining a def inside a struct is forbidden (use class if methods are needed);" "(3) it may appear as a variant inside REnum, and a struct may contain another struct." " Correct example: struct Point: x: int / struct Point: x: int, then a newline with 4-space indented y: int.%s" " (struct constraints are never silently ignored: illegal forms all abort compilation)", detail, extra) _g_parse_err_msg = buf else: _g_parse_err_msg = detail def _parse_structdef(ps: Parser*) -> AST*: """struct NAME: / struct NAME(Base): sugar -> a ClassDef field-identical to `@t.NoVTable class NAME(t.StructBase):`. Data-only: forces no vtable via the pseudo decorator @t.NoVTable, appends Attribute(Name('t'), 'StructBase') after the user bases as a data-class marker, and forbids def in the body (fail-fast via _RecordStructErr). Nestable in REnum and in another struct. """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # struct if _cur_type(ps) != TokenType.Name: _RecordStructErr("missing type name after `struct`", lineno, col, "Correct form: struct Point: (the type name follows struct immediately).") ps.error_count += 1 return None name: str = _cur_str(ps) _RejectReservedIdent(ps, name, "a type name", _cur_lineno(ps), _cur_col(ps)) _advance(ps) # Type parameters [T] / [T, U] (same as class, PEP 695 generics) type_params: list[str]* = None if _accept_op(ps, TokOp.Lsqb): type_params = list[str](ps.pool, 8) while not _match_op(ps, TokOp.Rsqb): if _cur_type(ps) == TokenType.EndMarker: break if _cur_type(ps) == TokenType.Name: tp_name: str = _cur_str(ps) _advance(ps) if _match_op(ps, TokOp.Colon): _advance(ps) _parse_test(ps) type_params.append(tp_name) if not _accept_op(ps, TokOp.Comma): break _expect_op(ps, TokOp.Rsqb) bases: list[AST*]* = list[AST*](ps.pool, 8) keywords: list[AST*]* = list[AST*](ps.pool, 8) # bases / keywords (same parse path as _parse_classdef, including cross-line and keyword=value) if _accept_op(ps, TokOp.LPar): while not _match_op(ps, TokOp.RPar): _skip_nl(ps) if _cur_type(ps) == TokenType.EndMarker: break if _cur_type(ps) == TokenType.Name: e: AST* = _parse_or_test(ps) if _match_op(ps, TokOp.Equal): _advance(ps) v: AST* = _parse_test(ps) kw_node: AST* = KwNode(ps.pool, e, v) keywords.append(kw_node) else: bases.append(e) if not _accept_op(ps, TokOp.Comma): break _skip_nl(ps) _expect_op(ps, TokOp.RPar) # Data-class marker base: appended after the user bases (real parent stays at bases[0]) bases.append(Name(ps.pool, "StructBase", ASTCtx.Load)) _expect_op(ps, TokOp.Colon) # Pseudo decorator @NoVTable: appended after the user decorators (same merge as _ParseVirtualModified) dec_list: list[AST*]* = ps.pending_decorators ps.pending_decorators = None marker: AST* = Name(ps.pool, "NoVTable", ASTCtx.Load) _set_pos(marker, lineno, col, 0, 0) merged: list[AST*]* = list[AST*](ps.pool, 8) if merged is not None: if dec_list is not None: dn_s: sizet = len(dec_list) for di_s in range(int(dn_s)): merged.append(dec_list.get(di_s)) merged.append(marker) node: ClassDef* = ClassDef(ps.pool, name, bases, keywords, merged, type_params) # owner_is_class=1: same as class (ps.in_class_body=1 during the class body) _parse_suite_into_children(ps, node, 1) # struct forbids def: check the direct children chk: list[AST*]* = node.children if chk is not None: cn_s: sizet = len(chk) for ci_s in range(int(cn_s)): it_s: AST* = chk.get(ci_s) if it_s is not None and it_s.kind() == ASTKind.FunctionDef: fd_s: FunctionDef* = (FunctionDef*)(it_s) fname_s: str = "(anonymous)" if fd_s is not None and fd_s.name is not None: fname_s = fd_s.name _RecordStructErr(f"def '{fname_s}' defined inside struct '{name}'", it_s.lineno, it_s.col_offset, "a struct represents a data type only (same as C's struct);" " if methods and a vtable are really needed, write the type as a class.") ps.error_count += 1 break _set_pos(node, lineno, col, 0, 0) return node def _parse_match(ps: Parser*) -> AST*: """match statement: match subject ':' NewLine Indent case+ Dedent""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # match subject: AST* = _parse_test(ps) _expect_op(ps, TokOp.Colon) cases: list[AST*]* = list[AST*](ps.pool, 8) node: AST* = Match(ps.pool, subject, cases, 0) # Parsing body: Same NewLine/Indent/Dedent pattern as _parse_suite_into_children, # but parses case clauses into a cases list instead of normal statements into children _skip_nl(ps) if _cur_type(ps) == TokenType.NewLine: _advance(ps) _skip_nl(ps) if _cur_type(ps) == TokenType.Indent: _advance(ps) while _cur_type(ps) != TokenType.Dedent and _cur_type(ps) != TokenType.EndMarker: if _cur_kw(ps) == Keyword.Case: case_node: AST* = _parse_match_case(ps) if case_node is not None: cases.append(case_node) else: _advance(ps) # Skip non-case token to avoid infinite loop _expect_type(ps, TokenType.Dedent) _set_pos(node, lineno, col, 0, 0) return node # ============================================================ # switch statement (C switch semantics, desugared to Match(is_switch=1)): `switch :` with # `case :` / `default:` arms. C switch rules: no fall-through unless # `fallthrough` is written in the case body. Case labels allow only constants / enum variants / # multi-value `case 1 | 2:`, not structured patterns. # ============================================================ def _is_switch_default_kw(ps: Parser*) -> int: """`default` is a context keyword inside a switch suite: matches `default:` or `default if :` (the latter supports a guard like `case _ if `).""" if _cur_type(ps) != TokenType.Name: return 0 s: str = _cur_str(ps) if s is None or string.strcmp(s, "default") != 0: return 0 nxt: Token* = ps.cur.next if nxt is None: return 0 if nxt.type == TokenType.Op and nxt.op_subtype == TokOp.Colon: return 1 if nxt.type == TokenType.Name and nxt.kw_subtype == Keyword.If: return 1 return 0 def _RecordSwitchCaseErr(detail: str, lineno: int, col: int): """Record an invalid switch case label (fail-fast; first only)""" global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg if _g_parse_err_line != 0: return _g_parse_err_line = lineno _g_parse_err_col = col buf: str = stdlib.malloc(512) if buf is not None: stdio.snprintf(buf, 512, "invalid switch case label (%s). switch supports only constant literals or enum variant labels" " (multi-value `case 1 | 2:` allowed); it does not accept structural patterns such as sequences/mappings/captures." " Correct examples: case 1: / case \"a\": / case Color.Red: / case Opt.Some():", detail) _g_parse_err_msg = buf else: _g_parse_err_msg = "invalid switch case label" def _CheckSwitchCasePattern(ps: Parser*, pat: AST*) -> int: """Validate a switch case label (only constants / enum variants / multi-value patterns allowed)""" if pat is None: return 0 k: int = pat.kind() if k == ASTKind.MatchValue or k == ASTKind.MatchSingleton or k == ASTKind.MatchClass: return 1 if k == ASTKind.MatchAs: # Only the wildcard (`case _`, equivalent to default) is legal; as-bindings with a subpattern are not ma_w: MatchAs* = MatchAs*(pat) if ma_w.pattern is None: return 1 return 0 if k == ASTKind.MatchOr: mo: MatchOr* = MatchOr*(pat) pats: list[AST*]* = mo.patterns if pats is None or len(pats) == 0: return 0 i: sizet = 0 n: sizet = len(pats) while i < n: sub: AST* = pats.get(i) if sub is None: return 0 sk: int = sub.kind() if sk != ASTKind.MatchValue and sk != ASTKind.MatchSingleton \ and sk != ASTKind.MatchClass: return 0 i = i + 1 return 1 return 0 def _parse_switch_case(ps: Parser*) -> AST*: """switch `case [if ] ':' suite` (guard supported). A bare name is a capture pattern (MatchAs) in match but a value reference in switch (like C's `case NAME:`), so `MatchAs(pattern=None, name=X)` is normalized to `MatchValue(Name('X'))`; `_` stays a wildcard. The guard is parsed like _parse_match_case and passed to MatchCase.guard. """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # case pattern: AST* = _parse_pattern(ps) if pattern is not None and pattern.kind() == ASTKind.MatchAs: ma: MatchAs* = MatchAs*(pattern) if ma.pattern is None and ma.name is not None: ref: AST* = Name(ps.pool, ma.name, ASTCtx.Load) _set_pos(ref, pattern.lineno, pattern.col_offset, 0, 0) mv: AST* = MatchValue(ps.pool, ref) _set_pos(mv, pattern.lineno, pattern.col_offset, 0, 0) pattern = mv if _CheckSwitchCasePattern(ps, pattern) == 0: _RecordSwitchCaseErr("case label is not a constant/enum variant", lineno, col) ps.error_count += 1 # guard: `case if ':'` (same form as _parse_match_case) guard: AST* = None if _cur_kw(ps) == Keyword.If: _advance(ps) guard = _parse_or_test(ps) _expect_op(ps, TokOp.Colon) node: AST* = MatchCase(ps.pool, pattern, guard) _parse_suite_into_children(ps, node) _set_pos(node, lineno, col, 0, 0) return node def _parse_switch_default(ps: Parser*) -> AST*: """switch `default [if ] ':' suite` -- desugared to `case []` (MatchAs wildcard). `default` is switch's spelling of `case _`; it supports a guard like `case _ if `, passed to MatchCase. Without if the behavior is unchanged (guard=None). """ lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # default guard: AST* = None if _cur_kw(ps) == Keyword.If: _advance(ps) guard = _parse_or_test(ps) _expect_op(ps, TokOp.Colon) wild: AST* = MatchAs(ps.pool, None, None) _set_pos(wild, lineno, col, 0, 0) node: AST* = MatchCase(ps.pool, wild, guard) _parse_suite_into_children(ps, node) _set_pos(node, lineno, col, 0, 0) return node def _parse_switch(ps: Parser*) -> AST*: """switch statement: switch subject ':' NewLine Indent (case|default)+ Dedent""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # switch subject: AST* = _parse_test(ps) _expect_op(ps, TokOp.Colon) cases: list[AST*]* = list[AST*](ps.pool, 8) node: AST* = Match(ps.pool, subject, cases, 1) _skip_nl(ps) if _cur_type(ps) == TokenType.NewLine: _advance(ps) _skip_nl(ps) if _cur_type(ps) == TokenType.Indent: _advance(ps) while _cur_type(ps) != TokenType.Dedent and _cur_type(ps) != TokenType.EndMarker: if _cur_kw(ps) == Keyword.Case: case_node: AST* = _parse_switch_case(ps) if case_node is not None: cases.append(case_node) elif _is_switch_default_kw(ps) != 0: def_node: AST* = _parse_switch_default(ps) if def_node is not None: cases.append(def_node) else: _advance(ps) # Skip non-case token to avoid infinite loop _expect_type(ps, TokenType.Dedent) _set_pos(node, lineno, col, 0, 0) return node # ============================================================ # Match pattern parsing # ============================================================ def _make_name_ast(pool: memhub.MemManager*, node: Name*) -> AST*: # Analysis: "Convert Name* to AST* (Solving the TransPyC Type Inference Problem)" return node def _parse_pattern(ps: Parser*) -> AST*: """pattern: or_pattern ['as' NAME] or_pattern: closed_pattern ('|' closed_pattern)*""" pat: AST* = _parse_closed_pattern(ps) if pat is None: return None # OR pattern if _match_op(ps, TokOp.VBar): patterns: list[AST*]* = list[AST*](ps.pool, 8) patterns.append(pat) while _accept_op(ps, TokOp.VBar): p: AST* = _parse_closed_pattern(ps) if p is not None: patterns.append(p) or_node: AST* = MatchOr(ps.pool, patterns) _set_pos(or_node, pat.lineno, pat.col_offset, 0, 0) pat = or_node # AS pattern if _cur_kw(ps) == Keyword.As: _advance(ps) if _cur_type(ps) != TokenType.Name: ps.error_count += 1 return pat name_str: str = _cur_str(ps) _advance(ps) as_node: AST* = MatchAs(ps.pool, pat, name_str) _set_pos(as_node, pat.lineno, pat.col_offset, 0, 0) return as_node return pat def _parse_closed_pattern(ps: Parser*) -> AST*: """closed_pattern: literal | capture | wildcard | value | group | sequence | mapping | class""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) ttype: int = _cur_type(ps) val: AST* node: AST* tok: Token* sval: str # Number literal pattern if ttype == TokenType.Number: tok = ps.cur _advance(ps) if tok.is_float: val = _new_const_float(ps.pool, tok.float_val, lineno, col) else: val = _new_const_int(ps.pool, tok.int_val, lineno, col, tok.int_width, tok.int_unsigned) node = MatchValue(ps.pool, val) _set_pos(node, lineno, col, 0, 0) return node # String literal pattern if ttype == TokenType.String: sval = _cur_str(ps) is_dquote_m: int = ps.cur.kw_subtype & 16 _advance(ps) val = _new_const_str(ps.pool, sval, lineno, col, is_dquote_m) node = MatchValue(ps.pool, val) _set_pos(node, lineno, col, 0, 0) return node # Negative number literal pattern: -1, -1.5 if _match_op(ps, TokOp.Minus): _advance(ps) if _cur_type(ps) == TokenType.Number: tok = ps.cur # Combined unary-minus + integer-literal overflow check (same rule as _parse_factor): # a negative literal with magnitude > 2^63 yields < -2^63 -> fail-fast, no silent wrap. if tok.is_float == 0 and _lex_int_neg_exceeds_i64(_cur_str(ps)) != 0: _record_neg_literal_overflow(_cur_str(ps), lineno, col) ps.error_count += 1 return None _advance(ps) if tok.is_float: val = _new_const_float(ps.pool, -tok.float_val, lineno, col) else: val = _new_const_int(ps.pool, -tok.int_val, lineno, col, tok.int_width, tok.int_unsigned) node = MatchValue(ps.pool, val) _set_pos(node, lineno, col, 0, 0) return node ps.error_count += 1 return None # Name: None/True/False/_ /capture/value/class if ttype == TokenType.Name: kw: int = _cur_kw(ps) # None/True/False → MatchSingleton if kw == Keyword.None_ or kw == Keyword.True_ or kw == Keyword.False_: _advance(ps) if kw == Keyword.None_: val = _new_const_none(ps.pool, lineno, col) elif kw == Keyword.True_: val = _new_const_bool(ps.pool, 1, lineno, col) else: val = _new_const_bool(ps.pool, 0, lineno, col) node = MatchSingleton(ps.pool, val) _set_pos(node, lineno, col, 0, 0) return node name_str: str = _cur_str(ps) # Wildcard pattern: _ if string.strcmp(name_str, "_") == 0: _advance(ps) node = MatchAs(ps.pool, None, None) _set_pos(node, lineno, col, 0, 0) return node _advance(ps) name_node: Name* = Name(ps.pool, name_str, ASTCtx.Load) _set_pos(name_node, lineno, col, 0, 0) base: AST* = _make_name_ast(ps.pool, name_node) # Generic qualifier: Option[int].Some(v) if _match_op(ps, TokOp.Lsqb): base = _parse_subscript(ps, base) # Check dotted name (value pattern or class pattern) if _match_op(ps, TokOp.Dot): while _match_op(ps, TokOp.Dot): _advance(ps) if _cur_type(ps) != TokenType.Name: ps.error_count += 1 break attr_name: str = _cur_str(ps) attr_lineno: int = _cur_lineno(ps) attr_col: int = _cur_col(ps) _advance(ps) base = Attribute(ps.pool, base, attr_name, ASTCtx.Load) _set_pos(base, attr_lineno, attr_col, 0, 0) # class pattern: dotted.name(...) if _match_op(ps, TokOp.LPar): return _parse_class_pattern(ps, base, lineno, col) # value pattern: dotted.name node = MatchValue(ps.pool, base) _set_pos(node, lineno, col, 0, 0) return node # class pattern: Name(...) if _match_op(ps, TokOp.LPar): return _parse_class_pattern(ps, base, lineno, col) # capture pattern: NAME node = MatchAs(ps.pool, None, name_str) _set_pos(node, lineno, col, 0, 0) return node # group pattern / sequence pattern (tuple): (...) if _match_op(ps, TokOp.LPar): _advance(ps) inner: AST* = _parse_pattern(ps) if _accept_op(ps, TokOp.Comma): # sequence pattern (tuple) patterns: list[AST*]* = list[AST*](ps.pool, 8) if inner is not None: patterns.append(inner) while not _match_op(ps, TokOp.RPar) and _cur_type(ps) != TokenType.EndMarker: p: AST* = _parse_pattern(ps) if p is not None: patterns.append(p) if not _accept_op(ps, TokOp.Comma): break _expect_op(ps, TokOp.RPar) node = MatchSequence(ps.pool, patterns) _set_pos(node, lineno, col, 0, 0) return node _expect_op(ps, TokOp.RPar) return inner # group pattern # sequence pattern (list): [...] if _match_op(ps, TokOp.Lsqb): return _parse_sequence_pattern(ps, TokOp.Rsqb, lineno, col) # mapping pattern: {...} if _match_op(ps, TokOp.LBrace): return _parse_mapping_pattern(ps, lineno, col) ps.error_count += 1 return None def _parse_sequence_pattern(ps: Parser*, close_op: int, lineno: int, col: int) -> AST*: """sequence_pattern: '[' [pattern_list] ']'""" patterns: list[AST*]* = list[AST*](ps.pool, 8) _advance(ps) # [ while not _match_op(ps, close_op) and _cur_type(ps) != TokenType.EndMarker: # *name (star pattern) if _match_op(ps, TokOp.Star): _advance(ps) if _cur_type(ps) == TokenType.Name: star_name: str = _cur_str(ps) _advance(ps) star_node: AST* if string.strcmp(star_name, "_") == 0: star_node = MatchStar(ps.pool, None) else: star_node = MatchStar(ps.pool, star_name) _set_pos(star_node, lineno, col, 0, 0) patterns.append(star_node) else: p: AST* = _parse_pattern(ps) if p is not None: patterns.append(p) if not _accept_op(ps, TokOp.Comma): break _expect_op(ps, close_op) node: AST* = MatchSequence(ps.pool, patterns) _set_pos(node, lineno, col, 0, 0) return node def _parse_mapping_pattern(ps: Parser*, lineno: int, col: int) -> AST*: """mapping_pattern: '{' [items] '}' items: key ':' pattern (',' key ':' pattern)* [',' '**' NAME] | '**' NAME""" keys: list[AST*]* = list[AST*](ps.pool, 8) patterns: list[AST*]* = list[AST*](ps.pool, 8) rest: str = None node: AST* key: AST* val_pat: AST* _advance(ps) # { if _match_op(ps, TokOp.RBrace): _advance(ps) node = MatchMapping(ps.pool, keys, patterns, rest) _set_pos(node, lineno, col, 0, 0) return node while _cur_type(ps) != TokenType.EndMarker: if _match_op(ps, TokOp.StarStar): _advance(ps) if _cur_type(ps) == TokenType.Name: rest = _cur_str(ps) _advance(ps) break key = _parse_expr(ps) _expect_op(ps, TokOp.Colon) val_pat = _parse_pattern(ps) if key is not None: keys.append(key) if val_pat is not None: patterns.append(val_pat) if not _accept_op(ps, TokOp.Comma): break _expect_op(ps, TokOp.RBrace) node = MatchMapping(ps.pool, keys, patterns, rest) _set_pos(node, lineno, col, 0, 0) return node def _parse_class_pattern(ps: Parser*, cls: AST*, lineno: int, col: int) -> AST*: """class_pattern: cls '(' [pattern_list] ')' pattern_list: positional_pattern (',' positional_pattern)* [',' keyword_pattern]* | keyword_pattern (',' keyword_pattern)* keyword_pattern: NAME '=' pattern""" patterns: list[AST*]* = list[AST*](ps.pool, 8) kwd_attrs: list[AST*]* = list[AST*](ps.pool, 8) kwd_patterns: list[AST*]* = list[AST*](ps.pool, 8) p: AST* _advance(ps) # ( if not _match_op(ps, TokOp.RPar): while _cur_type(ps) != TokenType.EndMarker: # keyword pattern: NAME '=' pattern if _cur_type(ps) == TokenType.Name and ps.cur is not None: nxt: Token* = ps.cur.next if nxt is not None and nxt.type == TokenType.Op and nxt.op_subtype == TokOp.Equal: attr_name: str = _cur_str(ps) _advance(ps) # NAME _advance(ps) # = kwd_pat: AST* = _parse_pattern(ps) attr_n: Name* = Name(ps.pool, attr_name, ASTCtx.Load) _set_pos(attr_n, lineno, col, 0, 0) attr_node: AST* = _make_name_ast(ps.pool, attr_n) kwd_attrs.append(attr_node) if kwd_pat is not None: kwd_patterns.append(kwd_pat) else: p = _parse_pattern(ps) if p is not None: patterns.append(p) else: p = _parse_pattern(ps) if p is not None: patterns.append(p) if not _accept_op(ps, TokOp.Comma): break _expect_op(ps, TokOp.RPar) node: AST* = MatchClass(ps.pool, cls, patterns, kwd_attrs, kwd_patterns) _set_pos(node, lineno, col, 0, 0) return node def _parse_match_case(ps: Parser*) -> AST*: """case pattern [if guard] ':' suite""" lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # case guard: AST* = None pattern: AST* = _parse_pattern(ps) if _cur_kw(ps) == Keyword.If: _advance(ps) guard = _parse_or_test(ps) _expect_op(ps, TokOp.Colon) node: AST* = MatchCase(ps.pool, pattern, guard) _parse_suite_into_children(ps, node) _set_pos(node, lineno, col, 0, 0) return node # ============================================================ # __attribute__ decorator desugar (C-style attribute syntax): `@__attribute__(section(".text.wo"), # always_inline)` equals hand-written `@c.Attribute(t.attr.section(...), t.attr.always_inline)`. # Arguments use unprefixed names, each desugared to `t.attr.` (calls keep their args) with the # outer call becoming `c.Attribute(...)`, so the result is field-identical and recognized as-is. # Works for class and def; arguments already in `t.attr.X` / `c.Attribute(...)` form are kept. # ============================================================ def _IsMagicAttributeCallee(node: AST*) -> int: """Whether the node is `__attribute__` (Name form, or the trailing X.__attribute__ form)""" if node is None: return 0 k: int = node.kind() if k == ASTKind.Name: nm: Name* = Name*(node) if nm.id is not None and string.strcmp(nm.id, "__attribute__") == 0: return 1 return 0 if k == ASTKind.Attribute: at: Attribute* = Attribute*(node) if at.attr is not None and string.strcmp(at.attr, "__attribute__") == 0: return 1 return 0 def _MakeTAttrMarker(ps: Parser*, arg: AST*) -> AST*: """Desugar one __attribute__ argument into `t.attr.` (calls keep their arguments). `always_inline` (Name) -> `t.attr.always_inline`; `section(".text.wo")` (Call) -> `t.attr.section(...)`; an argument already in `t.attr.X` form is returned as-is. """ if arg is None: return None k: int = arg.kind() if k == ASTKind.Attribute: at0: Attribute* = Attribute*(arg) if at0.value is not None and at0.value.kind() == ASTKind.Attribute: return arg return arg attr_root: AST* = Name(ps.pool, "attr", ASTCtx.Load) if k == ASTKind.Name: nm2: Name* = Name*(arg) if nm2.id is None: return arg mk: AST* = Attribute(ps.pool, attr_root, nm2.id, ASTCtx.Load) _set_pos(mk, nm2.lineno, nm2.col_offset, 0, 0) return mk if k == ASTKind.Call: cl2: Call* = Call*(arg) if cl2.func is None or cl2.func.kind() != ASTKind.Name: return arg fnm: Name* = Name*(cl2.func) if fnm.id is None: return arg inner: AST* = Attribute(ps.pool, attr_root, fnm.id, ASTCtx.Load) _set_pos(inner, fnm.lineno, fnm.col_offset, 0, 0) mk2: AST* = Call(ps.pool, inner, cl2.args, cl2.keywords) _set_pos(mk2, cl2.lineno, cl2.col_offset, 0, 0) return mk2 return arg def _DesugarMagicAttribute(ps: Parser*, expr: AST*) -> AST*: """`@__attribute__(...)` -> `@Attribute(attr.x, ...)`; other decorators are returned as-is""" if expr is None or expr.kind() != ASTKind.Call: return expr cl: Call* = Call*(expr) if _IsMagicAttributeCallee(cl.func) == 0: return expr c_func: AST* = Name(ps.pool, "Attribute", ASTCtx.Load) new_args: list[AST*]* = list[AST*](ps.pool, 8) if new_args is None: return expr if cl.args is not None: an: sizet = len(cl.args) for ai in range(int(an)): mk: AST* = _MakeTAttrMarker(ps, cl.args.get(ai)) if mk is not None: new_args.append(mk) new_call: AST* = Call(ps.pool, c_func, new_args, None) _set_pos(new_call, cl.lineno, cl.col_offset, 0, 0) return new_call def _parse_decorators(ps: Parser*) -> list[AST*]*: """Parse a decorator chain, returning a list.""" decs: list[AST*]* = list[AST*](ps.pool, 8) while _match_op(ps, TokOp.At): lineno: int = _cur_lineno(ps) col: int = _cur_col(ps) _advance(ps) # @ expr: AST* = _parse_namedexpr(ps) # C-style `@__attribute__(...)` desugars into the existing `@c.Attribute(t.attr....)` expr = _DesugarMagicAttribute(ps, expr) _skip_nl(ps) if _cur_type(ps) == TokenType.NewLine: _advance(ps) decs.append(expr) return decs # ============================================================ # Module entry # ============================================================ def _parse_module_body(ps: Parser*, module_node: AST*): """Parse the module body, appending to module_node.""" _skip_nl(ps) stmt_idx: int = 0 while _cur_type(ps) != TokenType.EndMarker: stmt_idx += 1 # fail-fast anti-hang: over-budget steps (cyclic token list / cursor spinning) report and return if ps.step_count > ps.step_limit: _record_parse_err(ps) if ps.recover != 0: _recover_add_err(ps) return if ps.recover != 0: _recover_reset_err() # Record current token pointer to check for infinite loop before_cur: Token* = ps.cur before_err: int = ps.error_count before_errors: int = 0 if ps.recover != 0: before_errors = int(len(ps.errors)) # Decorator if _match_op(ps, TokOp.At): ps.pending_decorators = _parse_decorators(ps) _parse_statement_into(ps, module_node) if ps.recover != 0: if int(len(ps.errors)) > before_errors: # A nested suite already recorded and synchronized the error. pass elif ps.error_count > before_err: _recover_add_err(ps) ps.cur = before_cur _recover_sync(ps, before_cur.col_offset, 1) elif ps.cur is before_cur: _record_parse_err(ps) _recover_add_err(ps) _advance(ps) else: # Defense: If parsing doesn't advance the token (like when _parse_atom encounters an unrecognized token and returns None), # force it to move forward to avoid an infinite loop in the main loop; accumulate error_count for the caller to diagnose if ps.cur is before_cur: ps.error_count += 1 _advance(ps) _skip_nl(ps) def parse_tokens(pool: memhub.MemManager*, tokens: Token*, recover: int = 0) -> AST*: """Parse an AST Module from a token list.""" if pool is None: return None global _g_parse_err_line, _g_parse_err_col, _g_parse_err_msg, _g_parse_errors _g_parse_err_line = 0 _g_parse_err_col = 0 _g_parse_err_msg = None _g_parse_errors = None # Lexer fail-fast takes priority: overflows recorded during tokenize (e.g. integer literal > 2^64-1) # are forwarded to the parse error channel here; lexer has no independent fatal exit. lex_line: int = get_lex_err_line() lex_col: int = get_lex_err_col() lex_len: int = get_lex_err_len() if lex_len < 1: lex_len = 1 if tokens is None: if recover == 0: return None ps_none: Parser* = new_parser(pool) if ps_none is None: return None _parser_init(ps_none, None, pool, 1) _g_parse_err_line = lex_line _g_parse_err_col = lex_col _g_parse_err_msg = get_lex_err_msg() _recover_add_err(ps_none, lex_line, lex_col + lex_len) _g_parse_errors = ps_none.errors return None if lex_line != 0: _g_parse_err_line = lex_line _g_parse_err_col = lex_col _g_parse_err_msg = get_lex_err_msg() if recover == 0: return None ps: Parser* = new_parser(pool) if ps is None: return None _parser_init(ps, tokens, pool, recover) # Recovery keeps parsing after a lexer fail-fast: the language server still needs symbols and # semantic tokens for the rest of the file. The error is recorded first, then the globals are # cleared so later statement errors use their own position. if lex_line != 0 and recover != 0: _recover_add_err(ps, lex_line, lex_col + lex_len) _g_parse_err_line = 0 _g_parse_err_col = 0 _g_parse_err_msg = None # fail-fast anti-hang: count the token list first, detecting a cycle. A cyclic list (Token appended # twice or aliased) would hang the statement loop; exceeding the cap before the tail means a cycle. tok_count: sizet = 0 walk_t: Token* = tokens while walk_t is not None: walk_t = walk_t.next tok_count += 1 if tok_count > PARSER_MAX_TOKENS: _g_parse_err_line = _cur_lineno(ps) _g_parse_err_col = _cur_col(ps) _g_parse_err_msg = "token list is cyclic: the token list emitted by the lexer has no terminator (_emit appended the same Token twice or aliased it); parsing aborted" return None # Budget: the watchdog counts "query current token" calls (well below 256 per token in valid parses, # so it cannot trip falsely) while catching cycles/spins; it also bounds _advance and statement loops. ps.step_limit = tok_count * 256 + 262144 module_node: Module* = Module(pool) _parse_module_body(ps, module_node) # Recovery mode returns the partial tree and the collected statement errors. if ps.error_count > 0 and recover != 0: _g_parse_errors = ps.errors return module_node # fail-fast: on a parse error (error_count > 0) return None so the caller aborts, avoiding silently # compiling broken code (e.g. a function definition left without its def keyword). if ps.error_count > 0: # Fallback position/reason: the parser has 40+ `ps.error_count += 1` sites and not all call # _record_*. Without a record, dump_parse_traceback can only print `File "..."` + # `SyntaxError` with no line or reason. Fill in a locatable message from the current token so the # report always has file:line:col + reason. if _g_parse_err_line == 0: _g_parse_err_line = _cur_lineno(ps) _g_parse_err_col = _cur_col(ps) err_s_e: str = _cur_str(ps) if err_s_e is None: err_s_e = "(none)" buf_e: str = stdlib.malloc(256) if buf_e is not None: stdio.snprintf(buf_e, 256, "syntax error: the parser recorded %d error(s) but no specific reason;" " token near the error position='%s' (type=%d)", ps.error_count, err_s_e, _cur_type(ps)); _g_parse_err_msg = buf_e else: _g_parse_err_msg = "syntax error: the parser recorded no specific reason" return None _g_parse_errors = ps.errors return module_node