@pineforge/codegen-pyodide 0.10.3 → 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.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +767 -77
- package/pineforge_codegen/analyzer/call_handlers.py +268 -42
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +907 -159
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +961 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1198 -101
- package/pineforge_codegen/codegen/tables.py +205 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +2055 -62
- package/pineforge_codegen/codegen/visit_call.py +929 -131
- package/pineforge_codegen/codegen/visit_expr.py +765 -63
- package/pineforge_codegen/codegen/visit_stmt.py +610 -52
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.3.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>"
|
|
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.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
567
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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
|
|
574
|
-
self.
|
|
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
|
-
|
|
579
|
-
|
|
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()
|
|
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.
|
|
659
|
+
self._read_escape(buf)
|
|
660
|
+
continue
|
|
584
661
|
buf.append(self._advance())
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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] = []
|