python-cc 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pcc/cli_bootstrap.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import sys
3
3
 
4
- from .py_frontend import pipeline as _py_pipeline
4
+ from .py_frontend.pipeline import compile_python as _compile_python
5
5
 
6
6
 
7
7
  _DEFAULT_EMIT_LL = "__PCC_DEFAULT_LL__"
@@ -280,7 +280,7 @@ def bootstrap_cli_main(argv=None) -> int:
280
280
  ll_out = output_path if output_path else path[:-3] + ".ll"
281
281
  else:
282
282
  ll_out = output_path if output_path else emit_llvm
283
- _py_pipeline.compile_python(
283
+ _compile_python(
284
284
  path,
285
285
  ll_out,
286
286
  verbose=verbose,
@@ -291,7 +291,7 @@ def bootstrap_cli_main(argv=None) -> int:
291
291
  recursive_stdlib=False,
292
292
  )
293
293
  else:
294
- _py_pipeline.compile_python(
294
+ _compile_python(
295
295
  path,
296
296
  output_path,
297
297
  verbose=verbose,
pcc/cli_core.py CHANGED
@@ -1166,3 +1166,18 @@ def cli_main_sys_argv_exit() -> None:
1166
1166
  from sys import exit as _exit
1167
1167
 
1168
1168
  _exit(code)
1169
+
1170
+
1171
+ def cli_main_strict_sys_argv_exit() -> None:
1172
+ """Entry point for the ``pcc-static`` console script.
1173
+
1174
+ Same as ``pcc`` but with the no-libpython flag combination
1175
+ (``--python-libpython=off`` + ``--ir-scaffold=on``) wired in as
1176
+ the default. An explicitly-passed ``--python-libpython`` /
1177
+ ``--ir-scaffold`` CLI flag, or a pre-set ``PCC_PYTHON_LIBPYTHON``
1178
+ / ``PCC_IR_SCAFFOLD`` env var, still takes precedence.
1179
+ """
1180
+ import os
1181
+ os.environ.setdefault("PCC_PYTHON_LIBPYTHON", "off")
1182
+ os.environ.setdefault("PCC_IR_SCAFFOLD", "on")
1183
+ cli_main_sys_argv_exit()
pcc/llvm_capi/ir.py CHANGED
@@ -799,11 +799,11 @@ class Function:
799
799
  # stay clean.
800
800
  self._name_counter = len(self.args)
801
801
  self._block_counter = 0
802
- # Track all named locals that have been emitted so far
803
- # collisions get a ``.N`` suffix appended. Matches llvmlite's
804
- # auto-deduplication; without it, code like
805
- # ``builder.call(..., name="m.int_unbox")`` in a loop would
806
- # emit conflicting SSA names.
802
+ # Track explicit names used by blocks and the small number of
803
+ # callers that require llvmlite-style stable deduplication. SSA
804
+ # instruction names are allocated from ``_name_counter`` instead;
805
+ # pcc-Python's dict is too expensive for per-instruction use in
806
+ # self-hosted codegen.
807
807
  self._name_registry: dict[str, int] = {}
808
808
  self.linkage = ""
809
809
  self.attributes = FunctionAttributes()
@@ -1209,25 +1209,15 @@ class IRBuilder:
1209
1209
  fn = self._fn
1210
1210
  if fn is None:
1211
1211
  raise ValueError("IRBuilder has no current function")
1212
+ fn._name_counter += 1
1212
1213
  if name == "":
1213
- fn._name_counter += 1
1214
1214
  return f".{fn._name_counter}"
1215
- n = fn._name_registry.get(name, 0)
1216
- fn._name_registry[name] = n + 1
1217
- if n == 0:
1218
- return name
1219
- candidate = f"{name}.{n}"
1220
- while candidate in fn._name_registry:
1221
- n += 1
1222
- candidate = f"{name}.{n}"
1223
- fn._name_registry[candidate] = 1
1224
- return candidate
1215
+ return f"{name}.{fn._name_counter}"
1225
1216
 
1226
1217
  def _next(self, name: str, ty: Type) -> Value:
1227
1218
  """Produce a new local SSA value with ``name`` (or a fresh temp
1228
- if empty). Uniqueifies against the function's name registry
1229
- so repeated ``name="x"`` calls get ``%x``, ``%x.1``, ``%x.2``
1230
- — same as llvmlite."""
1219
+ if empty). Named temporaries use a function-wide serial suffix,
1220
+ trading llvmlite text parity for O(1) self-hosted codegen."""
1231
1221
  return Value(ty, f"%{self._next_name(name)}")
1232
1222
 
1233
1223
  # ------------- return / branch / terminator -------------
@@ -1924,7 +1914,7 @@ def Module___init___named(name):
1924
1914
  return Module(name=name)
1925
1915
 
1926
1916
 
1927
- def Module___init__():
1917
+ def scaffold_Module___init__():
1928
1918
  return Module(name="")
1929
1919
 
1930
1920
 
pcc/parse/py_lex.py CHANGED
@@ -43,6 +43,65 @@ _OPS_MULTI = (
43
43
  _OPS_SINGLE = "+-*/%@&|^~<>=()[]{},:.;"
44
44
 
45
45
 
46
+ def _is_digit_code(c: int) -> bool:
47
+ return c >= 48 and c <= 57
48
+
49
+
50
+ def _is_alpha_code(c: int) -> bool:
51
+ return (c >= 65 and c <= 90) or (c >= 97 and c <= 122)
52
+
53
+
54
+ def _is_alnum_code(c: int) -> bool:
55
+ return _is_alpha_code(c) or _is_digit_code(c)
56
+
57
+
58
+ def _lower_code(c: int) -> int:
59
+ if c >= 65 and c <= 90:
60
+ return c + 32
61
+ return c
62
+
63
+
64
+ def _is_prefix_code(c: int) -> bool:
65
+ lc = _lower_code(c)
66
+ return lc == 98 or lc == 102 or lc == 114 or lc == 117
67
+
68
+
69
+ def _is_quote_code(c: int) -> bool:
70
+ return c == 34 or c == 39
71
+
72
+
73
+ def _is_single_op_code(c: int) -> bool:
74
+ return (
75
+ c == 37 or c == 38 or c == 40 or c == 41 or c == 42
76
+ or c == 43 or c == 44 or c == 45 or c == 46 or c == 47
77
+ or c == 58 or c == 59 or c == 60 or c == 61 or c == 62
78
+ or c == 64 or c == 91 or c == 93 or c == 94 or c == 123
79
+ or c == 124 or c == 125 or c == 126
80
+ )
81
+
82
+
83
+ def _is_open_paren_code(c: int) -> bool:
84
+ return c == 40 or c == 91 or c == 123
85
+
86
+
87
+ def _is_close_paren_code(c: int) -> bool:
88
+ return c == 41 or c == 93 or c == 125
89
+
90
+
91
+ def _pcc_str_byte_len(s: str) -> int:
92
+ return len(s)
93
+
94
+
95
+ def _pcc_str_byte_at(s: str, pos: int) -> int:
96
+ if pos < len(s):
97
+ return ord(s[pos])
98
+ return -1
99
+
100
+
101
+ def _pcc_str_byte_slice(s: str, lo: int, hi: int) -> str:
102
+ return s[lo:hi]
103
+
104
+
46
105
  @dataclass(frozen=True)
47
106
  class Token:
48
107
  kind: str
@@ -70,6 +129,7 @@ class Lexer:
70
129
 
71
130
  def __init__(self, src: str, filename: str = "<input>") -> None:
72
131
  self.src = src
132
+ self._src_len = _pcc_str_byte_len(src)
73
133
  self.filename = filename
74
134
  self.pos = 0
75
135
  self.line = 1
@@ -84,51 +144,51 @@ class Lexer:
84
144
  """Run the lexer to completion, returning all tokens."""
85
145
  out: list[Token] = []
86
146
  indent_stack: list[int] = self._indent_stack
87
- while self.pos < len(self.src):
147
+ while self.pos < self._src_len:
88
148
  if self._at_line_start and self._paren_depth == 0:
89
149
  self._emit_indent(out)
90
150
  self._at_line_start = False
91
- if self.pos >= len(self.src):
151
+ if self.pos >= self._src_len:
92
152
  break
93
- ch = self.src[self.pos]
94
- if ch == "\n":
153
+ ch = self._peek_code(0)
154
+ if ch == 10:
95
155
  self._emit_newline(out)
96
156
  continue
97
- if ch in " \t":
157
+ if ch == 32 or ch == 9:
98
158
  self.pos += 1
99
159
  self.col += 1
100
160
  continue
101
- if ch == "#":
102
- while self.pos < len(self.src) and self.src[self.pos] != "\n":
161
+ if ch == 35:
162
+ while self.pos < self._src_len and self._peek_code(0) != 10:
103
163
  self.pos += 1
104
164
  continue
105
- if ch == "\\" and self._peek(1) == "\n":
165
+ if ch == 92 and self._peek_code(1) == 10:
106
166
  self.pos += 2
107
167
  self.line += 1
108
168
  self.col = 1
109
169
  continue
110
170
  if (
111
- ch.lower() in "bfru"
112
- and self._peek(1) in ('"', "'")
171
+ _is_prefix_code(ch)
172
+ and _is_quote_code(self._peek_code(1))
113
173
  ):
114
174
  out.append(self._read_string())
115
175
  continue
116
176
  if (
117
- ch.lower() in "bfru"
118
- and self._peek(1).lower() in "bfru"
119
- and self._peek(2) in ('"', "'")
177
+ _is_prefix_code(ch)
178
+ and _is_prefix_code(self._peek_code(1))
179
+ and _is_quote_code(self._peek_code(2))
120
180
  ):
121
181
  out.append(self._read_string())
122
182
  continue
123
- if ch.isalpha() or ch == "_":
183
+ if _is_alpha_code(ch) or ch == 95:
124
184
  out.append(self._read_name())
125
185
  continue
126
- if ch.isdigit() or (
127
- ch == "." and self._peek(1).isdigit()
186
+ if _is_digit_code(ch) or (
187
+ ch == 46 and _is_digit_code(self._peek_code(1))
128
188
  ):
129
189
  out.append(self._read_number())
130
190
  continue
131
- if ch in ('"', "'"):
191
+ if _is_quote_code(ch):
132
192
  out.append(self._read_string())
133
193
  continue
134
194
  out.append(self._read_op())
@@ -142,18 +202,31 @@ class Lexer:
142
202
 
143
203
  # ------------------------------------------------------ helpers
144
204
 
205
+ def _code_at(self, pos: int) -> int:
206
+ if pos < self._src_len:
207
+ return _pcc_str_byte_at(self.src, pos)
208
+ return -1
209
+
210
+ def _slice(self, lo: int, hi: int) -> str:
211
+ return _pcc_str_byte_slice(self.src, lo, hi)
212
+
213
+ def _peek_code(self, off: int) -> int:
214
+ return self._code_at(self.pos + off)
215
+
145
216
  def _peek(self, off: int) -> str:
146
217
  p = self.pos + off
147
- return self.src[p] if p < len(self.src) else ""
218
+ return self._slice(p, p + 1) if p < self._src_len else ""
148
219
 
149
220
  def _emit_indent(self, out: list[Token]) -> None:
150
221
  indent_stack: list[int] = self._indent_stack
151
222
  depth = 0
152
223
  p = self.pos
153
- while p < len(self.src) and self.src[p] in " \t":
154
- depth += 1 if self.src[p] == " " else 8
224
+ pc = self._code_at(p)
225
+ while p < self._src_len and (pc == 32 or pc == 9):
226
+ depth += 1 if pc == 32 else 8
155
227
  p += 1
156
- if p >= len(self.src) or self.src[p] in ("\n", "#"):
228
+ pc = self._code_at(p)
229
+ if p >= self._src_len or pc == 10 or pc == 35:
157
230
  self.pos = p
158
231
  self.col = depth + 1
159
232
  return
@@ -183,12 +256,15 @@ class Lexer:
183
256
  start = self.pos
184
257
  start_col = self.col
185
258
  while (
186
- self.pos < len(self.src)
187
- and (self.src[self.pos].isalnum() or self.src[self.pos] == "_")
259
+ self.pos < self._src_len
260
+ and (
261
+ _is_alnum_code(self._peek_code(0))
262
+ or self._peek_code(0) == 95
263
+ )
188
264
  ):
189
265
  self.pos += 1
190
266
  self.col += 1
191
- text = self.src[start:self.pos]
267
+ text = self._slice(start, self.pos)
192
268
  kind = TK_KEYWORD if text in KEYWORDS else TK_NAME
193
269
  return Token(kind, text, self.line, start_col)
194
270
 
@@ -197,47 +273,62 @@ class Lexer:
197
273
  start_col = self.col
198
274
  # Hex / octal / binary: ``0x…`` / ``0o…`` / ``0b…`` (PEP 3127).
199
275
  if (
200
- self.src[self.pos] == "0"
201
- and self.pos + 1 < len(self.src)
202
- and self.src[self.pos + 1] in "xXoObB"
276
+ self._peek_code(0) == 48
277
+ and self.pos + 1 < self._src_len
278
+ and (
279
+ _lower_code(self._code_at(self.pos + 1)) == 98
280
+ or _lower_code(self._code_at(self.pos + 1)) == 111
281
+ or _lower_code(self._code_at(self.pos + 1)) == 120
282
+ )
203
283
  ):
204
- base = self.src[self.pos + 1].lower()
284
+ base = _lower_code(self._code_at(self.pos + 1))
205
285
  self.pos += 2
206
286
  self.col += 2
207
- if base == "x":
208
- valid = "0123456789abcdefABCDEF_"
209
- elif base == "o":
210
- valid = "01234567_"
211
- else:
212
- valid = "01_"
213
- while self.pos < len(self.src) and self.src[self.pos] in valid:
287
+ while self.pos < self._src_len:
288
+ c = self._peek_code(0)
289
+ valid = False
290
+ if c == 95:
291
+ valid = True
292
+ elif base == 120:
293
+ valid = (
294
+ _is_digit_code(c)
295
+ or (c >= 65 and c <= 70)
296
+ or (c >= 97 and c <= 102)
297
+ )
298
+ elif base == 111:
299
+ valid = c >= 48 and c <= 55
300
+ else:
301
+ valid = c == 48 or c == 49
302
+ if not valid:
303
+ break
214
304
  self.pos += 1
215
305
  self.col += 1
216
306
  return Token(
217
- TK_NUMBER, self.src[start:self.pos], self.line, start_col,
307
+ TK_NUMBER, self._slice(start, self.pos), self.line, start_col,
218
308
  )
219
309
  has_dot = False
220
310
  has_exp = False
221
- while self.pos < len(self.src):
222
- ch = self.src[self.pos]
223
- if ch.isdigit():
311
+ while self.pos < self._src_len:
312
+ ch = self._peek_code(0)
313
+ if _is_digit_code(ch):
224
314
  self.pos += 1
225
315
  self.col += 1
226
- elif ch == "." and not has_dot and not has_exp:
316
+ elif ch == 46 and not has_dot and not has_exp:
227
317
  has_dot = True
228
318
  self.pos += 1
229
319
  self.col += 1
230
- elif ch in ("e", "E") and not has_exp:
320
+ elif (ch == 101 or ch == 69) and not has_exp:
231
321
  has_exp = True
232
322
  self.pos += 1
233
323
  self.col += 1
234
- if self.pos < len(self.src) and self.src[self.pos] in "+-":
324
+ nxt = self._peek_code(0)
325
+ if self.pos < self._src_len and (nxt == 43 or nxt == 45):
235
326
  self.pos += 1
236
327
  self.col += 1
237
- elif ch in ("_",):
328
+ elif ch == 95:
238
329
  self.pos += 1
239
330
  self.col += 1
240
- elif ch in ("j", "J"):
331
+ elif ch == 106 or ch == 74:
241
332
  # Imaginary literal suffix — we accept the char so the
242
333
  # tokenizer doesn't choke, the frontend doesn't use them.
243
334
  self.pos += 1
@@ -245,7 +336,7 @@ class Lexer:
245
336
  break
246
337
  else:
247
338
  break
248
- return Token(TK_NUMBER, self.src[start:self.pos], self.line, start_col)
339
+ return Token(TK_NUMBER, self._slice(start, self.pos), self.line, start_col)
249
340
 
250
341
  def _read_string(self) -> Token:
251
342
  start = self.pos
@@ -253,31 +344,34 @@ class Lexer:
253
344
  # Consume the optional prefix chars (``b``, ``r``, ``f``, ``u``
254
345
  # and their 2-char combinations).
255
346
  while (
256
- self.pos < len(self.src)
257
- and self.src[self.pos].lower() in "bfru"
258
- and self._peek(0) not in ('"', "'")
347
+ self.pos < self._src_len
348
+ and _is_prefix_code(self._peek_code(0))
349
+ and not _is_quote_code(self._peek_code(0))
259
350
  ):
260
351
  self.pos += 1
261
352
  self.col += 1
262
- quote = self.src[self.pos]
353
+ quote_code = self._peek_code(0)
263
354
  # Handle triple-quoted.
264
- triple = self._peek(1) == quote and self._peek(2) == quote
355
+ triple = (
356
+ self._peek_code(1) == quote_code
357
+ and self._peek_code(2) == quote_code
358
+ )
265
359
  if triple:
266
360
  self.pos += 3
267
361
  self.col += 3
268
- while self.pos < len(self.src):
362
+ while self.pos < self._src_len:
269
363
  if (
270
- self.src[self.pos] == quote
271
- and self._peek(1) == quote
272
- and self._peek(2) == quote
364
+ self._peek_code(0) == quote_code
365
+ and self._peek_code(1) == quote_code
366
+ and self._peek_code(2) == quote_code
273
367
  ):
274
368
  self.pos += 3
275
369
  self.col += 3
276
370
  return Token(
277
- TK_STRING, self.src[start:self.pos],
371
+ TK_STRING, self._slice(start, self.pos),
278
372
  self.line, start_col,
279
373
  )
280
- if self.src[self.pos] == "\n":
374
+ if self._peek_code(0) == 10:
281
375
  self.line += 1
282
376
  self.col = 1
283
377
  else:
@@ -289,20 +383,20 @@ class Lexer:
289
383
  # Single-line.
290
384
  self.pos += 1
291
385
  self.col += 1
292
- while self.pos < len(self.src):
293
- ch = self.src[self.pos]
294
- if ch == "\\":
386
+ while self.pos < self._src_len:
387
+ ch = self._peek_code(0)
388
+ if ch == 92:
295
389
  self.pos += 2
296
390
  self.col += 2
297
391
  continue
298
- if ch == quote:
392
+ if ch == quote_code:
299
393
  self.pos += 1
300
394
  self.col += 1
301
395
  return Token(
302
- TK_STRING, self.src[start:self.pos],
396
+ TK_STRING, self._slice(start, self.pos),
303
397
  self.line, start_col,
304
398
  )
305
- if ch == "\n":
399
+ if ch == 10:
306
400
  raise LexError(
307
401
  f"{self.filename}:{self.line}: unterminated string"
308
402
  )
@@ -317,19 +411,21 @@ class Lexer:
317
411
  # Try 3-, 2-, then 1-char operators.
318
412
  for op in _OPS_MULTI:
319
413
  op_len = len(op)
320
- if src[pos:pos + op_len] == op:
414
+ if self._slice(pos, pos + op_len) == op:
321
415
  self.pos = pos + op_len
322
416
  self.col += op_len
323
417
  return Token(TK_OP, op, self.line, start_col)
324
- ch = self.src[self.pos]
325
- if ch in _OPS_SINGLE:
326
- if ch in "([{":
418
+ ch_code = self._peek_code(0)
419
+ if _is_single_op_code(ch_code):
420
+ ch = self._slice(self.pos, self.pos + 1)
421
+ if _is_open_paren_code(ch_code):
327
422
  self._paren_depth += 1
328
- elif ch in ")]}":
423
+ elif _is_close_paren_code(ch_code):
329
424
  self._paren_depth = max(0, self._paren_depth - 1)
330
425
  self.pos += 1
331
426
  self.col += 1
332
427
  return Token(TK_OP, ch, self.line, start_col)
428
+ ch = self._slice(self.pos, self.pos + 1)
333
429
  raise LexError(
334
430
  f"{self.filename}:{self.line}:{self.col}: stray character {ch!r}"
335
431
  )
pcc/parse/py_lift.py CHANGED
@@ -34,6 +34,42 @@ class LiftError(Exception):
34
34
  """Raised when a parser node has no py_ast equivalent yet."""
35
35
 
36
36
 
37
+ def _pow10f_lift(exp: int) -> float:
38
+ out = float(int("1", 10))
39
+ ten = float(int("10", 10))
40
+ i = 0
41
+ while i < exp:
42
+ out = out * ten
43
+ i += 1
44
+ return out
45
+
46
+
47
+ def _parse_float_literal_lift(text: str) -> float:
48
+ exp = 0
49
+ mantissa = text
50
+ lower = text.lower()
51
+ e_idx = lower.find("e")
52
+ if e_idx >= 0:
53
+ mantissa = text[:e_idx]
54
+ exp = int(text[e_idx + 1:], 10)
55
+ dot_idx = mantissa.find(".")
56
+ frac_len = 0
57
+ digits = mantissa
58
+ if dot_idx >= 0:
59
+ frac_len = len(mantissa) - dot_idx - 1
60
+ digits = mantissa[:dot_idx] + mantissa[dot_idx + 1:]
61
+ if not digits:
62
+ digits = "0"
63
+ value = float(int(digits, 10))
64
+ if frac_len > 0:
65
+ value = value / _pow10f_lift(frac_len)
66
+ if exp > 0:
67
+ value = value * _pow10f_lift(exp)
68
+ elif exp < 0:
69
+ value = value / _pow10f_lift(-exp)
70
+ return value
71
+
72
+
37
73
  def lift_module(mod: pp._Module, filename: str, module_name: str) -> pa.Module:
38
74
  L = _Lifter(filename)
39
75
  body = tuple(L.lift_stmt(s) for s in mod.body)
@@ -355,12 +391,12 @@ class _Lifter:
355
391
  return pa.IntLit(
356
392
  span=span,
357
393
  ty=pa.IntType(name="int", width=64, signed=True),
358
- value=e.value,
394
+ value=int(e.text, 0),
359
395
  )
360
396
  return pa.FloatLit(
361
397
  span=span,
362
398
  ty=pa.FloatType(name="float", width=64),
363
- value=e.value,
399
+ value=_parse_float_literal_lift(e.text),
364
400
  )
365
401
 
366
402
  def _e_Str(self, e: pp._Str) -> pa.StrLit:
@@ -686,6 +722,8 @@ def _lift_type(node) -> pa.Type:
686
722
  key=_lift_type(inner.elems[0]),
687
723
  value=_lift_type(inner.elems[1]),
688
724
  )
725
+ if base.ident in ("set", "frozenset"):
726
+ return pa.DynType(name="set")
689
727
  return _DYN
690
728
  if isinstance(node, pp._BinOp) and node.op == "|":
691
729
  # PEP 604 union — pcc has no Union type, fall back to Dyn.
pcc/parse/py_parse.py CHANGED
@@ -49,10 +49,11 @@ def _join_strings(parts: list[str], sep: str) -> str:
49
49
 
50
50
 
51
51
  def _pow10f(exp: int) -> float:
52
- out = 1.0
52
+ out = float(int("1", 10))
53
+ ten = float(int("10", 10))
53
54
  i = 0
54
55
  while i < exp:
55
- out = out * 10.0
56
+ out = out * ten
56
57
  i += 1
57
58
  return out
58
59
 
@@ -107,7 +108,7 @@ class _Expr:
107
108
 
108
109
  @dataclass
109
110
  class _Num:
110
- value: "int | float"
111
+ text: str
111
112
  line: int
112
113
  is_int: bool
113
114
 
@@ -1193,8 +1194,11 @@ class Parser:
1193
1194
 
1194
1195
  def _parse_compare(self):
1195
1196
  lhs = self._parse_bitor()
1197
+ comparisons = []
1198
+ prev = lhs
1196
1199
  while True:
1197
1200
  t = self._peek()
1201
+ line = t.line
1198
1202
  if t.kind == TK_OP and t.text in (
1199
1203
  "<", ">", "<=", ">=", "==", "!=",
1200
1204
  ):
@@ -1220,8 +1224,13 @@ class Parser:
1220
1224
  else:
1221
1225
  break
1222
1226
  rhs = self._parse_bitor()
1223
- lhs = _Compare(op=op, lhs=lhs, rhs=rhs, line=self._peek().line)
1224
- return lhs
1227
+ comparisons.append(_Compare(op=op, lhs=prev, rhs=rhs, line=line))
1228
+ prev = rhs
1229
+ if not comparisons:
1230
+ return lhs
1231
+ if len(comparisons) == 1:
1232
+ return comparisons[0]
1233
+ return _BoolOp(op="and", values=comparisons, line=comparisons[0].line)
1225
1234
 
1226
1235
  def _parse_bitor(self):
1227
1236
  lhs = self._parse_bitxor()
@@ -1596,8 +1605,8 @@ class Parser:
1596
1605
  self._advance()
1597
1606
  clean = t.text.replace("_", "")
1598
1607
  if "." in clean or "e" in clean.lower():
1599
- return _Num(_parse_float_literal(clean), t.line, False)
1600
- return _Num(int(clean, 0), t.line, True)
1608
+ return _Num(clean, t.line, False)
1609
+ return _Num(clean, t.line, True)
1601
1610
  if t.kind == TK_STRING:
1602
1611
  self._advance()
1603
1612
  strings = [t]
@@ -138,6 +138,11 @@ class ClassInfo:
138
138
  # call-site lookups (e.g. ``_find_method_def``) see synthetic
139
139
  # methods, not just what the user wrote.
140
140
  self.expanded_cd: "ClassDef | None" = None
141
+ # Cross-module extern classes synthesize FuncDef stubs from export
142
+ # metadata so kwargs/default resolution can use normal method
143
+ # lowering. Keep this as a declared pcc field; dynamic post-init
144
+ # attributes are not reliable in the native object layout.
145
+ self.extern_method_defs: dict[str, FuncDef] = {}
141
146
 
142
147
 
143
148
  class ClassLowering: