@pineforge/codegen-pyodide 0.10.4 → 1.0.0-rc.1

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.
Files changed (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -21,6 +21,9 @@ from pineforge_codegen.errors import (
21
21
  Phase,
22
22
  SourceLocation,
23
23
  )
24
+ from pineforge_codegen.limits import (
25
+ MAX_NESTING_DEPTH, TimeBudget, check_source_size, limit_error,
26
+ )
24
27
 
25
28
 
26
29
  class TokenType(Enum):
@@ -173,7 +176,8 @@ class Lexer:
173
176
  TokenType.PERCENT_EQUALS,
174
177
  }
175
178
 
176
- def __init__(self, source: str, filename: str = "<input>") -> None:
179
+ def __init__(self, source: str, filename: str = "<input>",
180
+ budget: TimeBudget | None = None) -> None:
177
181
  self.source = source
178
182
  self.filename = filename
179
183
  self.pos = 0
@@ -184,6 +188,7 @@ class Lexer:
184
188
  self.paren_depth = 0 # Track () and [] nesting to suppress NEWLINE/INDENT/DEDENT
185
189
  self._in_continuation = False # True when current line is a continuation
186
190
  self._diagnostics: list[Diagnostic] = []
191
+ self._budget = budget
187
192
 
188
193
  def _peek(self, offset: int = 0) -> str:
189
194
  idx = self.pos + offset
@@ -197,6 +202,11 @@ class Lexer:
197
202
  self.col = 1
198
203
  else:
199
204
  self.col += 1
205
+ if self._budget is not None and self.pos % 1024 == 0:
206
+ self._budget.check(
207
+ SourceLocation(self.filename, self.line, self.col, self.col + 1),
208
+ Phase.LEXER,
209
+ )
200
210
  return ch
201
211
 
202
212
  def _at_end(self) -> bool:
@@ -223,6 +233,7 @@ class Lexer:
223
233
  self._advance()
224
234
 
225
235
  def tokenize(self) -> list[Token]:
236
+ check_source_size(self.source, self.filename)
226
237
  while not self._at_end():
227
238
  self._tokenize_line()
228
239
 
@@ -318,6 +329,11 @@ class Lexer:
318
329
  current_indent = self.indent_stack[-1]
319
330
  if indent_level > current_indent:
320
331
  self.indent_stack.append(indent_level)
332
+ if len(self.indent_stack) - 1 > MAX_NESTING_DEPTH:
333
+ raise limit_error(
334
+ f"Block nesting depth exceeds {MAX_NESTING_DEPTH} levels.",
335
+ SourceLocation(self.filename, self.line, 1, 2), Phase.LEXER,
336
+ )
321
337
  self._emit(TokenType.INDENT, "", self.line, 1)
322
338
  elif indent_level < current_indent:
323
339
  while len(self.indent_stack) > 1 and self.indent_stack[-1] > indent_level:
@@ -417,12 +433,12 @@ class Lexer:
417
433
  self._read_leading_dot_number(start_line, start_col)
418
434
  return
419
435
 
420
- # Strings
421
- if ch == '"':
422
- self._read_string(start_line, start_col)
423
- return
424
- if ch == "'":
425
- self._read_string_single(start_line, start_col)
436
+ # Strings: three quotes of one kind open a multiline string.
437
+ if ch in "\"'":
438
+ if self.source.startswith(ch * 3, self.pos):
439
+ self._read_multiline(ch, start_line, start_col)
440
+ else:
441
+ self._read_quoted(ch, start_line, start_col)
426
442
  return
427
443
 
428
444
  # Color literals (#rrggbb or #rrggbbaa)
@@ -492,6 +508,12 @@ class Lexer:
492
508
  tt = singles[ch]
493
509
  if tt in (TokenType.LPAREN, TokenType.LBRACKET):
494
510
  self.paren_depth += 1
511
+ if self.paren_depth > MAX_NESTING_DEPTH:
512
+ raise limit_error(
513
+ f"Delimiter nesting depth exceeds {MAX_NESTING_DEPTH} levels.",
514
+ SourceLocation(self.filename, start_line, start_col,
515
+ start_col + 1), Phase.LEXER,
516
+ )
495
517
  elif tt in (TokenType.RPAREN, TokenType.RBRACKET):
496
518
  self.paren_depth = max(0, self.paren_depth - 1)
497
519
  self._emit(tt, ch, start_line, start_col, start_col + 1)
@@ -563,29 +585,89 @@ class Lexer:
563
585
  value = "0." + frac
564
586
  self._emit(TokenType.NUMBER, value, start_line, start_col, start_col + len(value))
565
587
 
566
- def _read_string(self, start_line: int, start_col: int) -> None:
567
- self._advance() # consume opening "
588
+ # Pine v6 User Manual, Strings, "Escape sequences": a backslash makes a
589
+ # quotation mark or a backslash literal, "The \\n sequence represents the
590
+ # newline character (U+000A)", "The \\t sequence represents the horizontal
591
+ # tab character (U+0009)", and "If a backslash applied to a character does
592
+ # not form a supported escape sequence, the character's meaning does not
593
+ # change" -- the backslash is dropped and the character kept.
594
+ _STRING_ESCAPES = {"n": "\n", "t": "\t"}
595
+
596
+ def _read_escape(self, buf: list[str]) -> None:
597
+ self._advance() # skip backslash
598
+ ch = self._advance()
599
+ buf.append(self._STRING_ESCAPES.get(ch, ch))
600
+
601
+ # Pine v6 User Manual, Strings: "In Pine v6, programmers can use line
602
+ # wrapping to define single-line literal strings across multiple code
603
+ # lines, where each wrapped line has an indentation of one or more spaces.
604
+ # However, the resulting character sequence adds only one space to the
605
+ # start of the wrapped lines, and it does not automatically add line
606
+ # terminators." A line break inside a single-line string therefore reads
607
+ # as one space when the next line is indented; otherwise the string is
608
+ # unterminated.
609
+ def _read_quoted(self, quote: str, start_line: int, start_col: int) -> None:
610
+ self._advance() # consume the opening quote
568
611
  buf: list[str] = []
569
- while not self._at_end() and self.source[self.pos] != '"':
570
- if self.source[self.pos] == "\\" and self.pos + 1 < len(self.source):
571
- self._advance() # skip backslash
612
+ while not self._at_end() and self.source[self.pos] != quote:
613
+ ch = self.source[self.pos]
614
+ if ch == "\\" and self.pos + 1 < len(self.source):
615
+ self._read_escape(buf)
616
+ continue
617
+ if ch == "\n":
618
+ nxt = self.pos + 1
619
+ indent = nxt
620
+ while indent < len(self.source) and self.source[indent] in " \t":
621
+ indent += 1
622
+ if indent == nxt or indent >= len(self.source) or self.source[indent] == "\n":
623
+ break # not a wrapped line: the literal is unterminated
624
+ self._advance_to(indent)
625
+ buf.append(" ")
626
+ continue
572
627
  buf.append(self._advance())
573
- if not self._at_end():
574
- self._advance() # consume closing "
628
+ if self._at_end() or self.source[self.pos] != quote:
629
+ self._emit_diagnostic(
630
+ "Unterminated string literal: the line ends before its closing quote "
631
+ "and the next line is not indented as a wrapped line.",
632
+ start_line, start_col, start_col + 1,
633
+ hint=('Close the string on its line, indent the line it wraps onto, '
634
+ 'or use a multiline string (\"\"\"...\"\"\").'),
635
+ )
636
+ else:
637
+ self._advance() # consume the closing quote
575
638
  value = "".join(buf)
576
639
  self._emit(TokenType.STRING, value, start_line, start_col, self.col)
577
640
 
578
- def _read_string_single(self, start_line: int, start_col: int) -> None:
579
- self._advance() # consume opening '
641
+ # Pine v6 User Manual, Strings, "Multiline strings": "A multiline string
642
+ # is a literal character sequence enclosed by three pairs of ASCII
643
+ # quotation marks (e.g. \"\"\"...\"\"\") or apostrophes (e.g.,
644
+ # '''...''')." Everything between the delimiters is literal text -- line
645
+ # breaks become U+000A, "including any space characters used for
646
+ # indentation" -- and a quote needs no backslash unless three in a row
647
+ # would end the string; escapes read as in any Pine string. The string
648
+ # ends at the first run of three unescaped delimiter quotes.
649
+ def _read_multiline(self, quote: str, start_line: int, start_col: int) -> None:
650
+ delimiter = quote * 3
651
+ self._advance_to(self.pos + 3) # consume the opening delimiter
580
652
  buf: list[str] = []
581
- while not self._at_end() and self.source[self.pos] != "'":
653
+ while not self._at_end():
654
+ if self.source.startswith(delimiter, self.pos):
655
+ self._advance_to(self.pos + 3)
656
+ self._emit(TokenType.STRING, "".join(buf), start_line, start_col, self.col)
657
+ return
582
658
  if self.source[self.pos] == "\\" and self.pos + 1 < len(self.source):
583
- self._advance()
659
+ self._read_escape(buf)
660
+ continue
584
661
  buf.append(self._advance())
585
- if not self._at_end():
586
- self._advance() # consume closing '
587
- value = "".join(buf)
588
- self._emit(TokenType.STRING, value, start_line, start_col, self.col)
662
+ self._emit_diagnostic(
663
+ f"Unterminated multiline string literal: no closing {delimiter} before "
664
+ "the end of the script.",
665
+ start_line, start_col, start_col + 3,
666
+ hint=(f"A multiline string ends at the next {chr(34) * 3} (or {chr(39) * 3} "
667
+ f"for one opened with {chr(39) * 3}); escape a quote that would end "
668
+ "it early."),
669
+ )
670
+ self._emit(TokenType.STRING, "".join(buf), start_line, start_col, self.col)
589
671
 
590
672
  def _read_ident(self, start_line: int, start_col: int) -> None:
591
673
  buf: list[str] = []