fix grammar again
Browse files- .claude/settings.json +9 -0
- Backend/_smoke_test.py +0 -3
- Backend/ai/__init__.py +0 -11
- Backend/ai/fallback.py +7 -156
- Backend/cfg/__init__.py +0 -6
- Backend/cfg/grammar.py +233 -474
- Backend/icg/__init__.py +0 -7
- Backend/icg/generator.py +21 -172
- Backend/interpreter/__init__.py +0 -8
- Backend/interpreter/errors.py +0 -13
- Backend/interpreter/interpreter.py +35 -324
- Backend/lexer/__init__.py +1 -11
- Backend/lexer/delimiters.py +36 -47
- Backend/lexer/errors.py +3 -11
- Backend/lexer/positions.py +5 -15
- Backend/lexer/scanner.py +184 -400
- Backend/parser/__init__.py +0 -10
- Backend/parser/builder.py +91 -313
- Backend/parser/parser.py +14 -301
- Backend/semantic/__init__.py +0 -6
- Backend/semantic/analyzer.py +0 -43
- Backend/semantic/errors.py +0 -7
- Backend/semantic/symbol_table.py +5 -32
- Backend/server.py +32 -227
- Backend/shared/__init__.py +0 -16
- Backend/shared/ast_nodes.py +8 -38
- Backend/shared/tokens.py +80 -104
- UI/main.js +1 -3
.claude/settings.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"permissions": {
|
| 3 |
+
"allow": [
|
| 4 |
+
"Bash(python)",
|
| 5 |
+
"Bash(rm -rf __pycache__ */__pycache__)",
|
| 6 |
+
"Bash(python _smoke_test.py)"
|
| 7 |
+
]
|
| 8 |
+
}
|
| 9 |
+
}
|
Backend/_smoke_test.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
"""Smoke test runner for restructure. Run after each phase: python _smoke_test.py"""
|
| 2 |
import sys, os
|
| 3 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 4 |
|
|
@@ -39,7 +38,6 @@ PROGRAMS = [
|
|
| 39 |
['age=20']),
|
| 40 |
('nestedreturn', 'root() { branch stop = frost; spring (stop) { reclaim; } plant("continued"); reclaim; }',
|
| 41 |
['continued']),
|
| 42 |
-
# ── increment/decrement on indexed/member operands (added by Part A) ──
|
| 43 |
('arr_postfix', 'root() { seed arr[3]; arr[0] = 1; arr[0]++; plant("v={}", arr[0]); reclaim; }',
|
| 44 |
['v=2']),
|
| 45 |
('arr_prefix', 'root() { seed arr[3]; arr[0] = 5; ++arr[0]; plant("v={}", arr[0]); reclaim; }',
|
|
@@ -54,7 +52,6 @@ PROGRAMS = [
|
|
| 54 |
['v=9']),
|
| 55 |
('arr_in_loop', 'root() { seed arr[3]; arr[0]=0; arr[1]=0; arr[2]=0; cultivate(seed i = 0; i < 3; i++) { arr[i]++; } plant("a={} b={} c={}", arr[0], arr[1], arr[2]); reclaim; }',
|
| 56 |
['a=1 b=1 c=1']),
|
| 57 |
-
# ── exponent-assign (added separately) ──
|
| 58 |
('exp_assign_seed', 'root() { seed x = 2; x **= 3; plant("{}", x); reclaim; }',
|
| 59 |
['8']),
|
| 60 |
('exp_assign_chain', 'root() { seed x = 2; x **= 3; x **= 2; plant("{}", x); reclaim; }',
|
|
|
|
|
|
|
| 1 |
import sys, os
|
| 2 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 3 |
|
|
|
|
| 38 |
['age=20']),
|
| 39 |
('nestedreturn', 'root() { branch stop = frost; spring (stop) { reclaim; } plant("continued"); reclaim; }',
|
| 40 |
['continued']),
|
|
|
|
| 41 |
('arr_postfix', 'root() { seed arr[3]; arr[0] = 1; arr[0]++; plant("v={}", arr[0]); reclaim; }',
|
| 42 |
['v=2']),
|
| 43 |
('arr_prefix', 'root() { seed arr[3]; arr[0] = 5; ++arr[0]; plant("v={}", arr[0]); reclaim; }',
|
|
|
|
| 52 |
['v=9']),
|
| 53 |
('arr_in_loop', 'root() { seed arr[3]; arr[0]=0; arr[1]=0; arr[2]=0; cultivate(seed i = 0; i < 3; i++) { arr[i]++; } plant("a={} b={} c={}", arr[0], arr[1], arr[2]); reclaim; }',
|
| 54 |
['a=1 b=1 c=1']),
|
|
|
|
| 55 |
('exp_assign_seed', 'root() { seed x = 2; x **= 3; plant("{}", x); reclaim; }',
|
| 56 |
['8']),
|
| 57 |
('exp_assign_chain', 'root() { seed x = 2; x **= 3; x **= 2; plant("{}", x); reclaim; }',
|
Backend/ai/__init__.py
CHANGED
|
@@ -1,13 +1,2 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# AI PACKAGE - Chat-helper layer (Gemini integration + regex fallback)
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Contents:
|
| 5 |
-
# fallback.py - rule-based regex/keyword responder used when Gemini is
|
| 6 |
-
# unavailable or rate-limited.
|
| 7 |
-
# prompt.txt - system prompt that teaches the LLM how to talk about GAL.
|
| 8 |
-
#
|
| 9 |
-
# This package is NOT part of the compiler pipeline; server.py consumes it
|
| 10 |
-
# only for the in-IDE "Ask" chat helper.
|
| 11 |
-
# ============================================================================
|
| 12 |
|
| 13 |
from .fallback import fallback_reply # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
from .fallback import fallback_reply # noqa: F401
|
Backend/ai/fallback.py
CHANGED
|
@@ -1,28 +1,10 @@
|
|
| 1 |
-
"""
|
| 2 |
-
NLP-based fallback responder for the GAL AI chat.
|
| 3 |
-
|
| 4 |
-
Three-layer hybrid architecture:
|
| 5 |
-
Layer 1 — Rule Engine: regex matches compiler error messages → structured explanations
|
| 6 |
-
Layer 2 — Retriever: sentence-transformers (gal-mpnet-finetuned) semantic search over 50+ KB topics
|
| 7 |
-
Layer 3 — Default: help menu when nothing matches
|
| 8 |
-
|
| 9 |
-
Plus: synonym expansion, greeting detection, follow-up context, multi-topic blending.
|
| 10 |
-
Uses the #1 ranked sentence embedding model for best semantic matching accuracy.
|
| 11 |
-
All heavy imports are deferred so the server binds its port immediately.
|
| 12 |
-
"""
|
| 13 |
|
| 14 |
import re as _re
|
| 15 |
|
| 16 |
-
# Minimum cosine similarity score to return a topic match (else use default)
|
| 17 |
_THRESHOLD = 0.35
|
| 18 |
|
| 19 |
|
| 20 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 21 |
-
# STRUCTURED RESPONSE BUILDER
|
| 22 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 23 |
-
|
| 24 |
def _structured_error(stage, cause, rule="", fix="", explanation=""):
|
| 25 |
-
"""Build a structured error explanation from labeled slots."""
|
| 26 |
parts = [f"**Stage:** {stage}", f"**Cause:** {cause}"]
|
| 27 |
if rule:
|
| 28 |
parts.append(f"**Rule:** {rule}")
|
|
@@ -46,14 +28,7 @@ def _runtime_err(cause, rule, fix, explanation=""):
|
|
| 46 |
return _structured_error("Runtime (Interpreter)", cause, rule, fix, explanation)
|
| 47 |
|
| 48 |
|
| 49 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 50 |
-
# LAYER 1: RULE ENGINE — regex-based compiler error pattern matching
|
| 51 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 52 |
-
# Each entry: (compiled_regex, response_string_or_callable)
|
| 53 |
-
# If callable, it receives the re.Match object and returns a string.
|
| 54 |
-
|
| 55 |
_ERROR_PATTERNS = [
|
| 56 |
-
# ── Lexer Errors ──────────────────────────────────────────────
|
| 57 |
|
| 58 |
(_re.compile(r"Identifier exceeds maximum length of 15 characters", _re.I),
|
| 59 |
_lexer_err(
|
|
@@ -125,7 +100,6 @@ _ERROR_PATTERNS = [
|
|
| 125 |
"/* This is a comment */ // Correct\n/* Unclosed comment // ERROR",
|
| 126 |
)),
|
| 127 |
|
| 128 |
-
# ── Parser: Operator Errors ───────────────────────────────────
|
| 129 |
|
| 130 |
(_re.compile(r"'===' is not valid", _re.I),
|
| 131 |
_parser_err(
|
|
@@ -149,7 +123,6 @@ _ERROR_PATTERNS = [
|
|
| 149 |
"// BAD: spring (a | b) { ... }\n// GOOD: spring (a || b) { ... }",
|
| 150 |
)),
|
| 151 |
|
| 152 |
-
# ── Parser: Keyword Mistakes (generic — catches all 20+) ─────
|
| 153 |
|
| 154 |
(_re.compile(r"'(\w+)' is not a GAL keyword\.\s*Use '(\w+)' instead", _re.I),
|
| 155 |
lambda m: _parser_err(
|
|
@@ -159,7 +132,6 @@ _ERROR_PATTERNS = [
|
|
| 159 |
"GAL uses botanical-themed keywords. See the keyword reference for all mappings.",
|
| 160 |
)),
|
| 161 |
|
| 162 |
-
# ── Parser: Missing Delimiters ────────────────────────────────
|
| 163 |
|
| 164 |
(_re.compile(r"Expected\s*['\"]?;['\"]?|Unexpected token.*Expected\s*['\"]?;['\"]?", _re.I),
|
| 165 |
_parser_err(
|
|
@@ -249,7 +221,6 @@ _ERROR_PATTERNS = [
|
|
| 249 |
"// BAD: leaf c = '';\n// GOOD: leaf c = 'A';",
|
| 250 |
)),
|
| 251 |
|
| 252 |
-
# ── Semantic Errors ───────────────────────────────────────────
|
| 253 |
|
| 254 |
(_re.compile(r"Variable '(\w+)' already declared", _re.I),
|
| 255 |
lambda m: _semantic_err(
|
|
@@ -414,7 +385,6 @@ _ERROR_PATTERNS = [
|
|
| 414 |
"seed arr[] = {10, 20, 30};\nseed i = 1;\nplant(arr[i]); // OK: seed index",
|
| 415 |
)),
|
| 416 |
|
| 417 |
-
# ── Runtime Errors ────────────────────────────────────────────
|
| 418 |
|
| 419 |
(_re.compile(r"Division by zero", _re.I),
|
| 420 |
_runtime_err(
|
|
@@ -463,8 +433,6 @@ _ERROR_PATTERNS = [
|
|
| 463 |
|
| 464 |
|
| 465 |
def _rule_engine_match(msg):
|
| 466 |
-
"""Try each regex pattern against the user message.
|
| 467 |
-
Return a structured response string if matched, else None."""
|
| 468 |
for pattern, response in _ERROR_PATTERNS:
|
| 469 |
m = pattern.search(msg)
|
| 470 |
if m:
|
|
@@ -472,13 +440,7 @@ def _rule_engine_match(msg):
|
|
| 472 |
return None
|
| 473 |
|
| 474 |
|
| 475 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 476 |
-
# LAYER 2: EXPANDED KNOWLEDGE BASE (50+ topics)
|
| 477 |
-
# ═══════════════════════════════════════���═══════════════════════════════
|
| 478 |
-
# Each entry: (training_phrases_list, response_string)
|
| 479 |
-
|
| 480 |
_KNOWLEDGE_BASE = [
|
| 481 |
-
# ── 1. Data types ─────────────────────────────────────────────
|
| 482 |
([
|
| 483 |
"what are the data types",
|
| 484 |
"data types in GAL",
|
|
@@ -526,7 +488,6 @@ leaf ch = 'A';
|
|
| 526 |
branch flag = sunshine;
|
| 527 |
```"""),
|
| 528 |
|
| 529 |
-
# ── 2. Variables / declarations ───────────────────────────────
|
| 530 |
([
|
| 531 |
"how to declare a variable",
|
| 532 |
"variable declaration",
|
|
@@ -558,7 +519,6 @@ Constants use `fertile`:
|
|
| 558 |
fertile seed MAX = 100;
|
| 559 |
```"""),
|
| 560 |
|
| 561 |
-
# ── 3. Arrays ─────────────────────────────────────────────────
|
| 562 |
([
|
| 563 |
"how to use arrays",
|
| 564 |
"array declaration",
|
|
@@ -593,7 +553,6 @@ seed grid[][] = {{1, 2}, {3, 4}}; // nested init
|
|
| 593 |
```
|
| 594 |
Arrays are 0-indexed."""),
|
| 595 |
|
| 596 |
-
# ── 4. For loop (cultivate) ───────────────────────────────────
|
| 597 |
([
|
| 598 |
"for loop",
|
| 599 |
"cultivate loop",
|
|
@@ -631,7 +590,6 @@ cultivate(seed i = 0; i < TS(arr); i++) {
|
|
| 631 |
}
|
| 632 |
```"""),
|
| 633 |
|
| 634 |
-
# ── 5. While loop (grow) ──────────────────────────────────────
|
| 635 |
([
|
| 636 |
"while loop",
|
| 637 |
"grow loop",
|
|
@@ -664,7 +622,6 @@ grow (count < 3) {
|
|
| 664 |
- Use `prune;` (break) to exit early
|
| 665 |
- Use `skip;` (continue) to skip to next iteration"""),
|
| 666 |
|
| 667 |
-
# ── 6. Do-while loop (tend...grow) ────────────────────────────
|
| 668 |
([
|
| 669 |
"do while loop",
|
| 670 |
"do-while loop",
|
|
@@ -698,7 +655,6 @@ tend {
|
|
| 698 |
- Use `prune;` (break) to exit early
|
| 699 |
- Use `skip;` (continue) to skip to next iteration"""),
|
| 700 |
|
| 701 |
-
# ── 7. Loops overview ─────────────────────────────────────────
|
| 702 |
([
|
| 703 |
"how to make a loop",
|
| 704 |
"loop types in GAL",
|
|
@@ -739,7 +695,6 @@ tend {
|
|
| 739 |
|
| 740 |
Use `prune;` (break) and `skip;` (continue) inside loops."""),
|
| 741 |
|
| 742 |
-
# ── 8. If / else ──────────────────────────────────────────────
|
| 743 |
([
|
| 744 |
"if else condition",
|
| 745 |
"conditional statement",
|
|
@@ -778,7 +733,6 @@ spring (x > 0) {
|
|
| 778 |
- `bud` = else if
|
| 779 |
- `wither` = else"""),
|
| 780 |
|
| 781 |
-
# ── 9. Switch ─────────────────────────────────────────────────
|
| 782 |
([
|
| 783 |
"switch case statement",
|
| 784 |
"harvest variety soil",
|
|
@@ -811,7 +765,6 @@ harvest (choice) {
|
|
| 811 |
- `soil` = default
|
| 812 |
- `prune` = break"""),
|
| 813 |
|
| 814 |
-
# ── 10. Functions ─────────────────────────────────────────────
|
| 815 |
([
|
| 816 |
"how to create a function",
|
| 817 |
"function declaration definition",
|
|
@@ -856,7 +809,6 @@ root() {
|
|
| 856 |
}
|
| 857 |
```"""),
|
| 858 |
|
| 859 |
-
# ── 11. Input (water) ─────────────────────────────────────────
|
| 860 |
([
|
| 861 |
"how to read input",
|
| 862 |
"get user input",
|
|
@@ -885,7 +837,6 @@ water(arr[i][j]); // read into 2D array element
|
|
| 885 |
```
|
| 886 |
`water(seed x)` is WRONG — don't combine type + variable name."""),
|
| 887 |
|
| 888 |
-
# ── 12. Output (plant) ────────────────────────────────────────
|
| 889 |
([
|
| 890 |
"how to print output",
|
| 891 |
"display show output",
|
|
@@ -917,7 +868,6 @@ Backtick concatenation may join `vine` and `leaf` values in plant():
|
|
| 917 |
- `plant("Hello {}", name);` is valid formatting
|
| 918 |
- `plant("Hello " ` name);` is valid string concatenation"""),
|
| 919 |
|
| 920 |
-
# ── 13. Bundles (structs) ─────────────────────────────────────
|
| 921 |
([
|
| 922 |
"struct bundle record",
|
| 923 |
"create a struct",
|
|
@@ -963,7 +913,6 @@ p.addr.zip = 1000;
|
|
| 963 |
|
| 964 |
No inline init: `bundle Point p = {5, 10};` is NOT supported."""),
|
| 965 |
|
| 966 |
-
# ── 14. Operators ─────────────────────────────────────────────
|
| 967 |
([
|
| 968 |
"operators in GAL",
|
| 969 |
"arithmetic comparison logical",
|
|
@@ -986,7 +935,6 @@ No inline init: `bundle Point p = {5, 10};` is NOT supported."""),
|
|
| 986 |
|
| 987 |
`**` (exponent) is NOT supported."""),
|
| 988 |
|
| 989 |
-
# ── 15. Errors / debugging ────────────────────────────────────
|
| 990 |
([
|
| 991 |
"error bug debug fix",
|
| 992 |
"why is my code not working",
|
|
@@ -1017,7 +965,6 @@ No inline init: `bundle Point p = {5, 10};` is NOT supported."""),
|
|
| 1017 |
|
| 1018 |
Tip: Check the OUTPUT panel for line numbers to locate errors."""),
|
| 1019 |
|
| 1020 |
-
# ── 16. Keywords reference ────────────────────────────────────
|
| 1021 |
([
|
| 1022 |
"keyword reference list",
|
| 1023 |
"all GAL keywords",
|
|
@@ -1054,7 +1001,6 @@ Tip: Check the OUTPUT panel for line numbers to locate errors."""),
|
|
| 1054 |
| `frost` | false |
|
| 1055 |
| `~` | unary minus |"""),
|
| 1056 |
|
| 1057 |
-
# ── 17. Example / template ────────────────────────────────────
|
| 1058 |
([
|
| 1059 |
"example program template",
|
| 1060 |
"hello world starter code",
|
|
@@ -1083,7 +1029,6 @@ root() {
|
|
| 1083 |
```
|
| 1084 |
Every GAL program needs a `root()` function as the entry point."""),
|
| 1085 |
|
| 1086 |
-
# ── 18. Convert from C ────────────────────────────────────────
|
| 1087 |
([
|
| 1088 |
"convert C to GAL",
|
| 1089 |
"translate from C language",
|
|
@@ -1105,7 +1050,6 @@ Every GAL program needs a `root()` function as the entry point."""),
|
|
| 1105 |
- `return` -> `reclaim`, `void` -> `empty`
|
| 1106 |
- `-x` -> `~x` (unary negation uses tilde)"""),
|
| 1107 |
|
| 1108 |
-
# ── 19. Comments ──────────────────────────────────────────────
|
| 1109 |
([
|
| 1110 |
"how to write comments in GAL code",
|
| 1111 |
"comment syntax slash slash",
|
|
@@ -1125,7 +1069,6 @@ Every GAL program needs a `root()` function as the entry point."""),
|
|
| 1125 |
```
|
| 1126 |
Same syntax as C/Java."""),
|
| 1127 |
|
| 1128 |
-
# ── 20. Identifier rules ──────────────────────────────────────
|
| 1129 |
([
|
| 1130 |
"identifier rules naming",
|
| 1131 |
"variable name rules",
|
|
@@ -1144,7 +1087,6 @@ Same syntax as C/Java."""),
|
|
| 1144 |
Valid: `x`, `count`, `myVar`, `total_sum`, `playerScore1`
|
| 1145 |
Invalid: `2count`, `_name`, `thisIsWayTooLong`"""),
|
| 1146 |
|
| 1147 |
-
# ── 21. Type casting ──────────────────────────────────────────
|
| 1148 |
([
|
| 1149 |
"type casting conversion",
|
| 1150 |
"convert between types",
|
|
@@ -1171,7 +1113,6 @@ Supported casts:
|
|
| 1171 |
- `(vine)` — converts anything to string
|
| 1172 |
- `(branch)` — converts to boolean"""),
|
| 1173 |
|
| 1174 |
-
# ── 22. Array built-in operations ─────────────────────────────
|
| 1175 |
([
|
| 1176 |
"array built-in methods operations",
|
| 1177 |
"append to array add element",
|
|
@@ -1210,7 +1151,6 @@ seed slen = TS("hello"); // 5
|
|
| 1210 |
leaf chars[] = taper("abc"); // chars = ['a','b','c']
|
| 1211 |
```"""),
|
| 1212 |
|
| 1213 |
-
# ── 23. Escape sequences ──────────────────────────────────────
|
| 1214 |
([
|
| 1215 |
"escape sequences special characters",
|
| 1216 |
"newline tab in string",
|
|
@@ -1239,7 +1179,6 @@ plant("Name:\\tAlice");
|
|
| 1239 |
plant("She said \\"hi\\"");
|
| 1240 |
```"""),
|
| 1241 |
|
| 1242 |
-
# ── 24. String concatenation ──────────────────────────────────
|
| 1243 |
([
|
| 1244 |
"string concatenation combine join",
|
| 1245 |
"concatenate two strings",
|
|
@@ -1262,11 +1201,7 @@ For output, prefer format strings:
|
|
| 1262 |
plant("{} {}", first, second); // cleaner
|
| 1263 |
```"""),
|
| 1264 |
|
| 1265 |
-
# ══════════════════════════════════════════════════════════════
|
| 1266 |
-
# NEW TOPICS (25–54) — expanded coverage
|
| 1267 |
-
# ══════════════════════════════════════════════════════════════
|
| 1268 |
|
| 1269 |
-
# ── 25. Program structure & root() ────────────────────────────
|
| 1270 |
([
|
| 1271 |
"program structure organization",
|
| 1272 |
"where does root go",
|
|
@@ -1302,7 +1237,6 @@ root() {
|
|
| 1302 |
- `root()` must end with `reclaim;`
|
| 1303 |
- No code is allowed after `root()`'s closing `}`"""),
|
| 1304 |
|
| 1305 |
-
# ── 26. Scope rules ───────────────────────────────────────────
|
| 1306 |
([
|
| 1307 |
"scope rules variable visibility",
|
| 1308 |
"local variable global variable",
|
|
@@ -1346,7 +1280,6 @@ pollinate empty test() {
|
|
| 1346 |
|
| 1347 |
Variables in inner scopes can see outer scopes, but not vice versa."""),
|
| 1348 |
|
| 1349 |
-
# ── 27. Constants (fertile) ───────────────────────────────────
|
| 1350 |
([
|
| 1351 |
"how to make constant",
|
| 1352 |
"fertile keyword constant",
|
|
@@ -1372,7 +1305,6 @@ fertile branch DEBUG = frost;
|
|
| 1372 |
- Multiple `fertile` declarations on one line are **not** allowed
|
| 1373 |
- `fertile` goes before the type: `fertile seed`, not `seed fertile`"""),
|
| 1374 |
|
| 1375 |
-
# ── 28. Boolean values ────────────────────────────────────────
|
| 1376 |
([
|
| 1377 |
"boolean values in GAL",
|
| 1378 |
"sunshine and frost meaning",
|
|
@@ -1403,7 +1335,6 @@ branch equal = (x == y); // depends on x, y
|
|
| 1403 |
|
| 1404 |
Use `!` to negate: `!sunshine` = `frost`"""),
|
| 1405 |
|
| 1406 |
-
# ── 29. Function return types ─────────────────────────────────
|
| 1407 |
([
|
| 1408 |
"function return type options",
|
| 1409 |
"empty function void return",
|
|
@@ -1442,7 +1373,6 @@ pollinate empty sayHi() { // returns nothing
|
|
| 1442 |
- Non-empty functions must provide a value in their required final `reclaim`
|
| 1443 |
- The CFG requires every function to end with `reclaim`"""),
|
| 1444 |
|
| 1445 |
-
# ── 30. Recursive functions ───────────────────────────────────
|
| 1446 |
([
|
| 1447 |
"recursive function in GAL",
|
| 1448 |
"recursion example",
|
|
@@ -1471,7 +1401,6 @@ root() {
|
|
| 1471 |
- Each recursive call should move toward the base case
|
| 1472 |
- Be mindful of the 10,000 iteration limit (applies to deep recursion too)"""),
|
| 1473 |
|
| 1474 |
-
# ── 31. Nested conditionals ───────────────────────────────────
|
| 1475 |
([
|
| 1476 |
"nested if statements",
|
| 1477 |
"if inside if nested",
|
|
@@ -1510,7 +1439,6 @@ bud (condition3) { ... }
|
|
| 1510 |
wither { ... }
|
| 1511 |
```"""),
|
| 1512 |
|
| 1513 |
-
# ── 32. Prune (break) & skip (continue) ──────────────────────
|
| 1514 |
([
|
| 1515 |
"break statement prune",
|
| 1516 |
"continue statement skip",
|
|
@@ -1544,7 +1472,6 @@ cultivate (seed i = 0; i < 10; i++) {
|
|
| 1544 |
- `skip` can only be used in loops (not in `harvest`)
|
| 1545 |
- Using them outside their valid context gives a semantic error"""),
|
| 1546 |
|
| 1547 |
-
# ── 33. Nested loops ──────────────────────────────────────────
|
| 1548 |
([
|
| 1549 |
"nested loop inside loop",
|
| 1550 |
"double loop two loops",
|
|
@@ -1574,7 +1501,6 @@ cultivate (seed r = 0; r < 3; r++) {
|
|
| 1574 |
|
| 1575 |
Note: `prune` only exits the **innermost** loop. To exit an outer loop, use a flag variable."""),
|
| 1576 |
|
| 1577 |
-
# ── 34. 2D arrays detailed ────────────────────────────────────
|
| 1578 |
([
|
| 1579 |
"two dimensional array detailed",
|
| 1580 |
"matrix operations rows columns",
|
|
@@ -1616,7 +1542,6 @@ cultivate (seed i = 0; i < 3; i++) {
|
|
| 1616 |
|
| 1617 |
Arrays are 0-indexed: valid indices for `arr[M][N]` are `[0..M-1][0..N-1]`."""),
|
| 1618 |
|
| 1619 |
-
# ── 35. Array of bundles ──────────────────────────────────────
|
| 1620 |
([
|
| 1621 |
"array of bundles structs",
|
| 1622 |
"list of struct objects",
|
|
@@ -1654,7 +1579,6 @@ root() {
|
|
| 1654 |
**Syntax:** `bundle <Type> <name>[<size>];`
|
| 1655 |
Then access: `name[index].member`"""),
|
| 1656 |
|
| 1657 |
-
# ── 36. Nested bundles ────────────────────────────────────────
|
| 1658 |
([
|
| 1659 |
"nested bundle struct inside struct",
|
| 1660 |
"bundle with bundle member",
|
|
@@ -1690,7 +1614,6 @@ root() {
|
|
| 1690 |
|
| 1691 |
Define inner bundles **before** the outer bundle that uses them."""),
|
| 1692 |
|
| 1693 |
-
# ── 37. TS() function ─────────────────────────────────────────
|
| 1694 |
([
|
| 1695 |
"ts function get length",
|
| 1696 |
"array length size",
|
|
@@ -1719,7 +1642,6 @@ cultivate (seed i = 0; i < TS(arr); i++) {
|
|
| 1719 |
- Does NOT work on `seed`, `tree`, `leaf`, or `branch` scalars
|
| 1720 |
- `TS()` is GAL's equivalent of `len()` / `.length`"""),
|
| 1721 |
|
| 1722 |
-
# ── 38. Taper() function ──────────────────────────────────────
|
| 1723 |
([
|
| 1724 |
"taper function usage",
|
| 1725 |
"split string into characters",
|
|
@@ -1746,7 +1668,6 @@ cultivate (seed i = 0; i < TS(chars); i++) {
|
|
| 1746 |
- Works on `vine` (string) values
|
| 1747 |
- `taper()` is GAL's equivalent of `split('')` / `toCharArray()`"""),
|
| 1748 |
|
| 1749 |
-
# ── 39. Operator precedence ───────────────────────────────────
|
| 1750 |
([
|
| 1751 |
"operator precedence order",
|
| 1752 |
"which operator evaluated first",
|
|
@@ -1772,7 +1693,6 @@ seed result = (2 + 3) * 4; // 20, not 14
|
|
| 1772 |
branch check = (a > 0) && (b < 10);
|
| 1773 |
```"""),
|
| 1774 |
|
| 1775 |
-
# ── 40. Increment / decrement ─────────────────────────────────
|
| 1776 |
([
|
| 1777 |
"prefix postfix increment",
|
| 1778 |
"i++ vs ++i difference",
|
|
@@ -1807,7 +1727,6 @@ count--; // count = 1
|
|
| 1807 |
- Cannot be chained: `x++++` is invalid
|
| 1808 |
- Cannot combine with binary ops: `x++ + 1` needs separate statements"""),
|
| 1809 |
|
| 1810 |
-
# ── 41. Unary negation (tilde) ────────────────────────────────
|
| 1811 |
([
|
| 1812 |
"tilde operator negation",
|
| 1813 |
"negative number in GAL",
|
|
@@ -1837,7 +1756,6 @@ seed neg = ~42; // -42
|
|
| 1837 |
tree negPi = ~3.14; // -3.14
|
| 1838 |
```"""),
|
| 1839 |
|
| 1840 |
-
# ── 42. Compound assignment ─────────────��─────────────────────
|
| 1841 |
([
|
| 1842 |
"compound assignment operators",
|
| 1843 |
"plus equals minus equals",
|
|
@@ -1869,7 +1787,6 @@ score %= 5; // score = 2
|
|
| 1869 |
- `%=` requires `seed` operands (modulo needs integers)
|
| 1870 |
- Cannot use on `fertile` (const) variables"""),
|
| 1871 |
|
| 1872 |
-
# ── 43. Format strings in plant() ─────────────────────────────
|
| 1873 |
([
|
| 1874 |
"format string in plant",
|
| 1875 |
"placeholder curly braces",
|
|
@@ -1900,7 +1817,6 @@ plant("Name: {}, Age: {}", name, 25); // mixed types
|
|
| 1900 |
// GOOD: plant("{} {}", a, b); // 2 placeholders, 2 args
|
| 1901 |
```"""),
|
| 1902 |
|
| 1903 |
-
# ── 44. Limits & constraints ──────────────────────────────────
|
| 1904 |
([
|
| 1905 |
"limits constraints maximum GAL",
|
| 1906 |
"what are the GAL limits",
|
|
@@ -1933,7 +1849,6 @@ plant("Name: {}, Age: {}", name, 25); // mixed types
|
|
| 1933 |
- Arrays are 0-indexed
|
| 1934 |
- `root()` function is required"""),
|
| 1935 |
|
| 1936 |
-
# ── 45. Compiler stages ───────────────────────────────────────
|
| 1937 |
([
|
| 1938 |
"how does compiler work",
|
| 1939 |
"compilation process stages",
|
|
@@ -1969,7 +1884,6 @@ Source Code → Lexer → Parser → Semantic Analyzer → Interpreter
|
|
| 1969 |
**5. ICG** (`icg.py`) — Generates three-address code (parallel to semantic):
|
| 1970 |
- Produces intermediate representation for analysis"""),
|
| 1971 |
|
| 1972 |
-
# ── 46. Error: missing semicolons ─────────────────────────────
|
| 1973 |
([
|
| 1974 |
"forgot semicolon error",
|
| 1975 |
"where do I put semicolons",
|
|
@@ -1998,7 +1912,6 @@ Source Code → Lexer → Parser → Semantic Analyzer → Interpreter
|
|
| 1998 |
- Functions: `pollinate seed fn() { ... }`
|
| 1999 |
- `root()`: `root() { ... }`"""),
|
| 2000 |
|
| 2001 |
-
# ── 47. Error: type mismatches ────────────────────────────────
|
| 2002 |
([
|
| 2003 |
"type mismatch error help",
|
| 2004 |
"cannot assign wrong type",
|
|
@@ -2033,7 +1946,6 @@ branch b = 5; // ERROR: seed → branch
|
|
| 2033 |
- `!` only works on `branch`
|
| 2034 |
- Comparisons require compatible types"""),
|
| 2035 |
|
| 2036 |
-
# ── 48. Error: undeclared variables ────────────────────────────
|
| 2037 |
([
|
| 2038 |
"variable not declared error",
|
| 2039 |
"undefined undeclared variable",
|
|
@@ -2069,7 +1981,6 @@ seed x = 10;
|
|
| 2069 |
// Fix: move declaration before use
|
| 2070 |
```"""),
|
| 2071 |
|
| 2072 |
-
# ── 49. Error: wrong C keywords ───────────────────────────────
|
| 2073 |
([
|
| 2074 |
"used wrong keyword from C",
|
| 2075 |
"if instead of spring",
|
|
@@ -2102,7 +2013,6 @@ GAL uses botanical-themed keywords. If you use C/Java/Python keywords, the compi
|
|
| 2102 |
|
| 2103 |
Tip: Use the keyword reference for the complete mapping."""),
|
| 2104 |
|
| 2105 |
-
# ── 50. Error: fertile (const) issues ─────────────────────────
|
| 2106 |
([
|
| 2107 |
"fertile constant error",
|
| 2108 |
"cannot reassign fertile",
|
|
@@ -2142,7 +2052,6 @@ fertile seed B = 2;
|
|
| 2142 |
- Only literal values (no expressions): `fertile seed X = 2 + 3;` is invalid
|
| 2143 |
- `fertile` goes before the type: `fertile seed`, not `seed fertile`"""),
|
| 2144 |
|
| 2145 |
-
# ── 51. Example: Factorial / recursion ─────────────────────────
|
| 2146 |
([
|
| 2147 |
"factorial example program",
|
| 2148 |
"recursive example code",
|
|
@@ -2184,7 +2093,6 @@ root() {
|
|
| 2184 |
}
|
| 2185 |
```"""),
|
| 2186 |
|
| 2187 |
-
# ── 52. Example: Array operations ─────────────────────────────
|
| 2188 |
([
|
| 2189 |
"array operations example",
|
| 2190 |
"sum of array elements",
|
|
@@ -2241,7 +2149,6 @@ root() {
|
|
| 2241 |
}
|
| 2242 |
```"""),
|
| 2243 |
|
| 2244 |
-
# ── 53. Example: Bubble sort ──────────────────────────────────
|
| 2245 |
([
|
| 2246 |
"sorting example program",
|
| 2247 |
"bubble sort GAL code",
|
|
@@ -2274,7 +2181,6 @@ root() {
|
|
| 2274 |
}
|
| 2275 |
```"""),
|
| 2276 |
|
| 2277 |
-
# ── 54. Example: Bundle usage ─────────────────────────────────
|
| 2278 |
([
|
| 2279 |
"bundle struct example program",
|
| 2280 |
"complete bundle example",
|
|
@@ -2315,7 +2221,6 @@ root() {
|
|
| 2315 |
}
|
| 2316 |
```"""),
|
| 2317 |
|
| 2318 |
-
# ── Topic 55: GAL vs C comparison overview ──────────────────────
|
| 2319 |
([
|
| 2320 |
"difference between GAL and C", "GAL vs C", "how is GAL different from C",
|
| 2321 |
"compare GAL and C", "GAL compared to C", "what makes GAL unique",
|
|
@@ -2354,7 +2259,6 @@ root() {
|
|
| 2354 |
- GAL uses `~` for **unary negation** (not `-`)
|
| 2355 |
- Format strings use `{}` placeholders (like Python), not `%d`/`%s`"""),
|
| 2356 |
|
| 2357 |
-
# ── Topic 56: Common mistakes / pitfalls ────────────────────────
|
| 2358 |
([
|
| 2359 |
"common mistakes in GAL", "GAL pitfalls", "beginners mistakes",
|
| 2360 |
"what mistakes do people make", "things to watch out for in GAL",
|
|
@@ -2394,7 +2298,6 @@ root() {
|
|
| 2394 |
|
| 2395 |
10. **Array index out of bounds** — indices are 0 to `TS(arr)-1`"""),
|
| 2396 |
|
| 2397 |
-
# ── Topic 57: How to debug GAL code ─────────────────────────────
|
| 2398 |
([
|
| 2399 |
"how to debug GAL", "debugging GAL code", "my GAL code doesn't work",
|
| 2400 |
"GAL code not working", "how to fix GAL errors", "debug my code",
|
|
@@ -2428,7 +2331,6 @@ The compiler tells you exactly what's wrong and which line. Error messages inclu
|
|
| 2428 |
|
| 2429 |
**Pro tip:** Paste any error message into this chat — I can explain it in detail!"""),
|
| 2430 |
|
| 2431 |
-
# ── Topic 58: What is GAL? ──────────────────────────────────────
|
| 2432 |
([
|
| 2433 |
"what is GAL", "what is the GAL language", "tell me about GAL",
|
| 2434 |
"GAL programming language", "what is this language", "about GAL",
|
|
@@ -2454,7 +2356,6 @@ root() {
|
|
| 2454 |
```
|
| 2455 |
This prints "Hello, Garden!" — `root()` is like `main()`, `plant()` is like `printf()`, and `reclaim` is like `return`."""),
|
| 2456 |
|
| 2457 |
-
# ── Topic 59: How to run a GAL program ──────────────────────────
|
| 2458 |
([
|
| 2459 |
"how to run GAL", "how to execute", "run my program", "how to compile",
|
| 2460 |
"how to use this IDE", "how to use the editor", "where do I type code",
|
|
@@ -2486,7 +2387,6 @@ root() {
|
|
| 2486 |
|
| 2487 |
Just type this in the editor and hit Run!"""),
|
| 2488 |
|
| 2489 |
-
# ── Topic 60: String operations in GAL ──────────────────────────
|
| 2490 |
([
|
| 2491 |
"string operations", "how to work with strings", "vine operations",
|
| 2492 |
"string manipulation", "string functions", "what can I do with strings",
|
|
@@ -2529,7 +2429,6 @@ plant("Name: {}, Length: {}", name, TS(name));
|
|
| 2529 |
- `\\\\"` — literal double quote
|
| 2530 |
- `\\\\` — literal backslash"""),
|
| 2531 |
|
| 2532 |
-
# ── Topic 61: Number operations / math in GAL ───────────────────
|
| 2533 |
([
|
| 2534 |
"math in GAL", "arithmetic operations", "math operations",
|
| 2535 |
"how to do math", "calculations in GAL", "number operations",
|
|
@@ -2573,19 +2472,14 @@ x %= 4; // x = 2
|
|
| 2573 |
]
|
| 2574 |
|
| 2575 |
|
| 2576 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 2577 |
-
# Sentence-Transformers (gal-mpnet-finetuned) — lazy-loaded on first query
|
| 2578 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 2579 |
-
|
| 2580 |
_st_model = None
|
| 2581 |
_phrase_embeddings = None
|
| 2582 |
_phrase_topic_idx = []
|
| 2583 |
_responses = []
|
| 2584 |
-
_last_topic_idx = None
|
| 2585 |
-
_last_query = ""
|
| 2586 |
|
| 2587 |
|
| 2588 |
-
# ── GAL synonym map: C/common terms → GAL equivalents ────────────────
|
| 2589 |
_SYNONYMS = {
|
| 2590 |
"int": "seed", "integer": "seed",
|
| 2591 |
"float": "tree", "double": "tree", "decimal": "tree",
|
|
@@ -2633,9 +2527,6 @@ _SYNONYMS = {
|
|
| 2633 |
"number": "seed tree integer float arithmetic",
|
| 2634 |
}
|
| 2635 |
|
| 2636 |
-
# ── Reverse map: GAL keywords → concept descriptions ─────────────────
|
| 2637 |
-
# When a user casually says "cultivate" or "tree", we expand it into
|
| 2638 |
-
# a rich query the embedding model can match against KB topics.
|
| 2639 |
_GAL_KEYWORD_MAP = {
|
| 2640 |
"seed": "seed data type integer variable declaration",
|
| 2641 |
"tree": "tree data type float decimal variable declaration",
|
|
@@ -2671,8 +2562,6 @@ _GAL_KEYWORD_MAP = {
|
|
| 2671 |
}
|
| 2672 |
|
| 2673 |
|
| 2674 |
-
# ── Greeting / meta patterns ─────────────────────────────────────────
|
| 2675 |
-
|
| 2676 |
_GREETING_PATTERNS = [
|
| 2677 |
(_re.compile(r"^\s*(hi|hello|hey|howdy|sup|yo|greetings|good\s*(morning|afternoon|evening))\b", _re.I),
|
| 2678 |
"Hey there! I'm the GAL AI Assistant. Ask me anything about GAL — data types, loops, functions, arrays, I/O, and more!"),
|
|
@@ -2681,19 +2570,17 @@ _GREETING_PATTERNS = [
|
|
| 2681 |
(_re.compile(r"^\s*(bye|goodbye|see\s*ya|later|cya)\b", _re.I),
|
| 2682 |
"Goodbye! Happy coding with GAL! 🌱"),
|
| 2683 |
(_re.compile(r"\b(what can you do|help me|what do you know|how can you help)\b", _re.I),
|
| 2684 |
-
None),
|
| 2685 |
(_re.compile(r"^\s*(who are you|what are you)\b", _re.I),
|
| 2686 |
"I'm the GAL AI Assistant — I help with GAL syntax, concepts, and debugging. Ask me about data types, loops, functions, arrays, or anything else in GAL!"),
|
| 2687 |
]
|
| 2688 |
|
| 2689 |
|
| 2690 |
def _encode(texts):
|
| 2691 |
-
"""Encode texts using sentence-transformers (returns L2-normalised embeddings)."""
|
| 2692 |
return _st_model.encode(texts, normalize_embeddings=True, show_progress_bar=False)
|
| 2693 |
|
| 2694 |
|
| 2695 |
def _ensure_model():
|
| 2696 |
-
"""Load sentence-transformers model and encode training phrases on first call."""
|
| 2697 |
global _st_model, _phrase_embeddings, _phrase_topic_idx, _responses
|
| 2698 |
if _st_model is not None:
|
| 2699 |
return
|
|
@@ -2701,7 +2588,6 @@ def _ensure_model():
|
|
| 2701 |
from sentence_transformers import SentenceTransformer
|
| 2702 |
import os
|
| 2703 |
|
| 2704 |
-
# Prefer fine-tuned model, fall back to base
|
| 2705 |
finetuned = os.path.join(os.path.dirname(__file__), "..", "gal-mpnet-finetuned")
|
| 2706 |
if os.path.isdir(finetuned):
|
| 2707 |
_st_model = SentenceTransformer(finetuned)
|
|
@@ -2721,7 +2607,6 @@ def _ensure_model():
|
|
| 2721 |
_phrase_embeddings = _encode(all_phrases)
|
| 2722 |
|
| 2723 |
|
| 2724 |
-
# Default fallback when no topic is similar enough
|
| 2725 |
_DEFAULT_RESPONSE = """I can help with GAL syntax and concepts! Try asking about:
|
| 2726 |
- **Data types**: seed, tree, leaf, vine, branch
|
| 2727 |
- **Variables**: declarations, constants (`fertile`), scope rules
|
|
@@ -2740,8 +2625,6 @@ Or ask for "keyword reference", "example program", or "how does the compiler wor
|
|
| 2740 |
|
| 2741 |
*Note: I'm running in offline mode right now. For more detailed help, try again later when the AI service is available.*"""
|
| 2742 |
|
| 2743 |
-
# ── Conversational wrappers ─────────────────────────────────────────
|
| 2744 |
-
# These make retrieval answers feel more natural (like Gemini would respond)
|
| 2745 |
import random as _random
|
| 2746 |
|
| 2747 |
_CONFIDENT_INTROS = [
|
|
@@ -2751,7 +2634,7 @@ _CONFIDENT_INTROS = [
|
|
| 2751 |
"Absolutely! ",
|
| 2752 |
"Good question — ",
|
| 2753 |
"Here you go:\n\n",
|
| 2754 |
-
"",
|
| 2755 |
"",
|
| 2756 |
]
|
| 2757 |
|
|
@@ -2780,40 +2663,31 @@ _OUTROS = [
|
|
| 2780 |
"\n\n---\n*Feel free to ask follow-up questions!*",
|
| 2781 |
"\n\n---\n*Let me know if you need more details on any part!*",
|
| 2782 |
"\n\n---\n*Want me to explain any part further?*",
|
| 2783 |
-
"",
|
| 2784 |
"",
|
| 2785 |
]
|
| 2786 |
|
| 2787 |
-
|
| 2788 |
-
|
| 2789 |
-
_MAX_HISTORY = 8 # remember last 8 exchanges
|
| 2790 |
|
| 2791 |
|
| 2792 |
def _expand_query(text):
|
| 2793 |
-
"""Inject GAL synonyms into the query so the model sees both vocabularies.
|
| 2794 |
-
Also resolves bare GAL keywords (e.g. 'cultivate') into rich concept
|
| 2795 |
-
descriptions so short/casual queries match KB topics."""
|
| 2796 |
words = text.lower().split()
|
| 2797 |
extras = set()
|
| 2798 |
|
| 2799 |
-
# Forward: C/common terms → GAL equivalents
|
| 2800 |
for w in words:
|
| 2801 |
if w in _SYNONYMS:
|
| 2802 |
extras.add(_SYNONYMS[w])
|
| 2803 |
|
| 2804 |
-
# Also check 2-word combos (e.g. "else if" → "bud")
|
| 2805 |
lower = text.lower()
|
| 2806 |
for phrase, replacement in _SYNONYMS.items():
|
| 2807 |
if " " in phrase and phrase in lower:
|
| 2808 |
extras.add(replacement)
|
| 2809 |
|
| 2810 |
-
# Reverse: GAL keywords → concept descriptions
|
| 2811 |
-
# This is the key for casual queries like "what is cultivate"
|
| 2812 |
for w in words:
|
| 2813 |
if w in _GAL_KEYWORD_MAP:
|
| 2814 |
extras.add(_GAL_KEYWORD_MAP[w])
|
| 2815 |
|
| 2816 |
-
# Also check multi-word GAL constructs in the raw text
|
| 2817 |
for kw, desc in _GAL_KEYWORD_MAP.items():
|
| 2818 |
if " " in kw and kw in lower:
|
| 2819 |
extras.add(desc)
|
|
@@ -2824,7 +2698,6 @@ def _expand_query(text):
|
|
| 2824 |
|
| 2825 |
|
| 2826 |
def _detect_intent(msg):
|
| 2827 |
-
"""Classify user intent to pick better response framing."""
|
| 2828 |
low = msg.lower()
|
| 2829 |
if any(w in low for w in ["how do i", "how to", "how can i", "how would"]):
|
| 2830 |
return "how-to"
|
|
@@ -2844,22 +2717,18 @@ def _detect_intent(msg):
|
|
| 2844 |
|
| 2845 |
|
| 2846 |
def _is_followup(msg):
|
| 2847 |
-
"""Detect if the user is asking a follow-up to the previous topic."""
|
| 2848 |
low = msg.lower().split()
|
| 2849 |
-
# Short pronoun-based queries
|
| 2850 |
if len(low) <= 5 and any(w in msg.lower() for w in [
|
| 2851 |
"it", "that", "this", "those", "them", "more", "also",
|
| 2852 |
"too", "same", "again", "another", "other"
|
| 2853 |
]):
|
| 2854 |
return True
|
| 2855 |
-
# "tell me more" / "explain further" type
|
| 2856 |
if _detect_intent(msg) == "more":
|
| 2857 |
return True
|
| 2858 |
return False
|
| 2859 |
|
| 2860 |
|
| 2861 |
def _pick_intro(score, intent, is_followup):
|
| 2862 |
-
"""Choose a conversational intro based on confidence and context."""
|
| 2863 |
if is_followup:
|
| 2864 |
return _random.choice(_FOLLOWUP_INTROS)
|
| 2865 |
if score > 0.6:
|
|
@@ -2870,21 +2739,17 @@ def _pick_intro(score, intent, is_followup):
|
|
| 2870 |
|
| 2871 |
|
| 2872 |
def _wrap_response(raw_response, score, intent, is_followup, has_blend=False):
|
| 2873 |
-
"""Wrap a raw KB response with conversational framing."""
|
| 2874 |
intro = _pick_intro(score, intent, is_followup)
|
| 2875 |
outro = _random.choice(_OUTROS) if not has_blend else ""
|
| 2876 |
return intro + raw_response + outro
|
| 2877 |
|
| 2878 |
|
| 2879 |
def fallback_reply(user_message):
|
| 2880 |
-
"""Hybrid fallback: greeting → rule engine → semantic retriever → default.
|
| 2881 |
-
Enhanced with conversational wrapping, intent detection, and conversation memory."""
|
| 2882 |
import numpy as np
|
| 2883 |
global _last_topic_idx, _last_query
|
| 2884 |
|
| 2885 |
msg = user_message.strip()
|
| 2886 |
|
| 2887 |
-
# ── Layer 0: Greeting / meta patterns ─────────────────────────
|
| 2888 |
for pattern, response in _GREETING_PATTERNS:
|
| 2889 |
if pattern.search(msg):
|
| 2890 |
return response if response else _DEFAULT_RESPONSE
|
|
@@ -2892,11 +2757,9 @@ def fallback_reply(user_message):
|
|
| 2892 |
if not msg or len(msg) < 2:
|
| 2893 |
return _DEFAULT_RESPONSE
|
| 2894 |
|
| 2895 |
-
# Allow short queries if they contain a known GAL keyword
|
| 2896 |
if len(msg) < 4 and msg.lower() not in _GAL_KEYWORD_MAP:
|
| 2897 |
return _DEFAULT_RESPONSE
|
| 2898 |
|
| 2899 |
-
# ── Layer 1: Rule Engine — exact compiler error matching ──────
|
| 2900 |
rule_match = _rule_engine_match(msg)
|
| 2901 |
if rule_match:
|
| 2902 |
_last_query = msg
|
|
@@ -2905,26 +2768,20 @@ def fallback_reply(user_message):
|
|
| 2905 |
_conv_history.pop(0)
|
| 2906 |
return rule_match
|
| 2907 |
|
| 2908 |
-
# ── Layer 2: Semantic Retriever — similarity search ───────────
|
| 2909 |
_ensure_model()
|
| 2910 |
|
| 2911 |
intent = _detect_intent(msg)
|
| 2912 |
is_followup = _is_followup(msg) and _conv_history
|
| 2913 |
|
| 2914 |
-
# Synonym expansion
|
| 2915 |
expanded = _expand_query(msg)
|
| 2916 |
|
| 2917 |
-
# Follow-up context injection — use conversation history
|
| 2918 |
if is_followup and _conv_history:
|
| 2919 |
-
# Grab last 1-2 queries for context
|
| 2920 |
recent = [h[0] for h in _conv_history[-2:]]
|
| 2921 |
expanded = " ".join(recent) + " " + expanded
|
| 2922 |
|
| 2923 |
-
# Encode and score
|
| 2924 |
query_emb = _encode([expanded])
|
| 2925 |
scores = np.dot(_phrase_embeddings, query_emb.T).flatten()
|
| 2926 |
|
| 2927 |
-
# Get best score per topic (deduped)
|
| 2928 |
topic_best = {}
|
| 2929 |
for i, score in enumerate(scores):
|
| 2930 |
tidx = _phrase_topic_idx[i]
|
|
@@ -2936,7 +2793,6 @@ def fallback_reply(user_message):
|
|
| 2936 |
|
| 2937 |
if best_score < _THRESHOLD:
|
| 2938 |
_last_query = msg
|
| 2939 |
-
# If follow-up failed, try without history context
|
| 2940 |
if is_followup:
|
| 2941 |
bare_expanded = _expand_query(msg)
|
| 2942 |
query_emb2 = _encode([bare_expanded])
|
|
@@ -2955,24 +2811,19 @@ def fallback_reply(user_message):
|
|
| 2955 |
else:
|
| 2956 |
return _DEFAULT_RESPONSE
|
| 2957 |
|
| 2958 |
-
# ── Build response ────────────────────────────────────────────
|
| 2959 |
result = _responses[best_idx]
|
| 2960 |
has_blend = False
|
| 2961 |
|
| 2962 |
-
# Multi-topic blending with smooth transitions
|
| 2963 |
if len(ranked) >= 2:
|
| 2964 |
second_idx, second_score = ranked[1]
|
| 2965 |
gap = best_score - second_score
|
| 2966 |
if second_score >= _THRESHOLD and gap < 0.07:
|
| 2967 |
-
# Avoid blending if both topics are too similar or same category
|
| 2968 |
transition = _random.choice(_BLEND_TRANSITIONS)
|
| 2969 |
result += transition + _responses[second_idx]
|
| 2970 |
has_blend = True
|
| 2971 |
|
| 2972 |
-
# Wrap with conversational framing
|
| 2973 |
result = _wrap_response(result, best_score, intent, is_followup, has_blend)
|
| 2974 |
|
| 2975 |
-
# Update conversation memory
|
| 2976 |
_last_topic_idx = best_idx
|
| 2977 |
_last_query = msg
|
| 2978 |
_conv_history.append((msg, best_idx, best_score))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
import re as _re
|
| 3 |
|
|
|
|
| 4 |
_THRESHOLD = 0.35
|
| 5 |
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
def _structured_error(stage, cause, rule="", fix="", explanation=""):
|
|
|
|
| 8 |
parts = [f"**Stage:** {stage}", f"**Cause:** {cause}"]
|
| 9 |
if rule:
|
| 10 |
parts.append(f"**Rule:** {rule}")
|
|
|
|
| 28 |
return _structured_error("Runtime (Interpreter)", cause, rule, fix, explanation)
|
| 29 |
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
_ERROR_PATTERNS = [
|
|
|
|
| 32 |
|
| 33 |
(_re.compile(r"Identifier exceeds maximum length of 15 characters", _re.I),
|
| 34 |
_lexer_err(
|
|
|
|
| 100 |
"/* This is a comment */ // Correct\n/* Unclosed comment // ERROR",
|
| 101 |
)),
|
| 102 |
|
|
|
|
| 103 |
|
| 104 |
(_re.compile(r"'===' is not valid", _re.I),
|
| 105 |
_parser_err(
|
|
|
|
| 123 |
"// BAD: spring (a | b) { ... }\n// GOOD: spring (a || b) { ... }",
|
| 124 |
)),
|
| 125 |
|
|
|
|
| 126 |
|
| 127 |
(_re.compile(r"'(\w+)' is not a GAL keyword\.\s*Use '(\w+)' instead", _re.I),
|
| 128 |
lambda m: _parser_err(
|
|
|
|
| 132 |
"GAL uses botanical-themed keywords. See the keyword reference for all mappings.",
|
| 133 |
)),
|
| 134 |
|
|
|
|
| 135 |
|
| 136 |
(_re.compile(r"Expected\s*['\"]?;['\"]?|Unexpected token.*Expected\s*['\"]?;['\"]?", _re.I),
|
| 137 |
_parser_err(
|
|
|
|
| 221 |
"// BAD: leaf c = '';\n// GOOD: leaf c = 'A';",
|
| 222 |
)),
|
| 223 |
|
|
|
|
| 224 |
|
| 225 |
(_re.compile(r"Variable '(\w+)' already declared", _re.I),
|
| 226 |
lambda m: _semantic_err(
|
|
|
|
| 385 |
"seed arr[] = {10, 20, 30};\nseed i = 1;\nplant(arr[i]); // OK: seed index",
|
| 386 |
)),
|
| 387 |
|
|
|
|
| 388 |
|
| 389 |
(_re.compile(r"Division by zero", _re.I),
|
| 390 |
_runtime_err(
|
|
|
|
| 433 |
|
| 434 |
|
| 435 |
def _rule_engine_match(msg):
|
|
|
|
|
|
|
| 436 |
for pattern, response in _ERROR_PATTERNS:
|
| 437 |
m = pattern.search(msg)
|
| 438 |
if m:
|
|
|
|
| 440 |
return None
|
| 441 |
|
| 442 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
_KNOWLEDGE_BASE = [
|
|
|
|
| 444 |
([
|
| 445 |
"what are the data types",
|
| 446 |
"data types in GAL",
|
|
|
|
| 488 |
branch flag = sunshine;
|
| 489 |
```"""),
|
| 490 |
|
|
|
|
| 491 |
([
|
| 492 |
"how to declare a variable",
|
| 493 |
"variable declaration",
|
|
|
|
| 519 |
fertile seed MAX = 100;
|
| 520 |
```"""),
|
| 521 |
|
|
|
|
| 522 |
([
|
| 523 |
"how to use arrays",
|
| 524 |
"array declaration",
|
|
|
|
| 553 |
```
|
| 554 |
Arrays are 0-indexed."""),
|
| 555 |
|
|
|
|
| 556 |
([
|
| 557 |
"for loop",
|
| 558 |
"cultivate loop",
|
|
|
|
| 590 |
}
|
| 591 |
```"""),
|
| 592 |
|
|
|
|
| 593 |
([
|
| 594 |
"while loop",
|
| 595 |
"grow loop",
|
|
|
|
| 622 |
- Use `prune;` (break) to exit early
|
| 623 |
- Use `skip;` (continue) to skip to next iteration"""),
|
| 624 |
|
|
|
|
| 625 |
([
|
| 626 |
"do while loop",
|
| 627 |
"do-while loop",
|
|
|
|
| 655 |
- Use `prune;` (break) to exit early
|
| 656 |
- Use `skip;` (continue) to skip to next iteration"""),
|
| 657 |
|
|
|
|
| 658 |
([
|
| 659 |
"how to make a loop",
|
| 660 |
"loop types in GAL",
|
|
|
|
| 695 |
|
| 696 |
Use `prune;` (break) and `skip;` (continue) inside loops."""),
|
| 697 |
|
|
|
|
| 698 |
([
|
| 699 |
"if else condition",
|
| 700 |
"conditional statement",
|
|
|
|
| 733 |
- `bud` = else if
|
| 734 |
- `wither` = else"""),
|
| 735 |
|
|
|
|
| 736 |
([
|
| 737 |
"switch case statement",
|
| 738 |
"harvest variety soil",
|
|
|
|
| 765 |
- `soil` = default
|
| 766 |
- `prune` = break"""),
|
| 767 |
|
|
|
|
| 768 |
([
|
| 769 |
"how to create a function",
|
| 770 |
"function declaration definition",
|
|
|
|
| 809 |
}
|
| 810 |
```"""),
|
| 811 |
|
|
|
|
| 812 |
([
|
| 813 |
"how to read input",
|
| 814 |
"get user input",
|
|
|
|
| 837 |
```
|
| 838 |
`water(seed x)` is WRONG — don't combine type + variable name."""),
|
| 839 |
|
|
|
|
| 840 |
([
|
| 841 |
"how to print output",
|
| 842 |
"display show output",
|
|
|
|
| 868 |
- `plant("Hello {}", name);` is valid formatting
|
| 869 |
- `plant("Hello " ` name);` is valid string concatenation"""),
|
| 870 |
|
|
|
|
| 871 |
([
|
| 872 |
"struct bundle record",
|
| 873 |
"create a struct",
|
|
|
|
| 913 |
|
| 914 |
No inline init: `bundle Point p = {5, 10};` is NOT supported."""),
|
| 915 |
|
|
|
|
| 916 |
([
|
| 917 |
"operators in GAL",
|
| 918 |
"arithmetic comparison logical",
|
|
|
|
| 935 |
|
| 936 |
`**` (exponent) is NOT supported."""),
|
| 937 |
|
|
|
|
| 938 |
([
|
| 939 |
"error bug debug fix",
|
| 940 |
"why is my code not working",
|
|
|
|
| 965 |
|
| 966 |
Tip: Check the OUTPUT panel for line numbers to locate errors."""),
|
| 967 |
|
|
|
|
| 968 |
([
|
| 969 |
"keyword reference list",
|
| 970 |
"all GAL keywords",
|
|
|
|
| 1001 |
| `frost` | false |
|
| 1002 |
| `~` | unary minus |"""),
|
| 1003 |
|
|
|
|
| 1004 |
([
|
| 1005 |
"example program template",
|
| 1006 |
"hello world starter code",
|
|
|
|
| 1029 |
```
|
| 1030 |
Every GAL program needs a `root()` function as the entry point."""),
|
| 1031 |
|
|
|
|
| 1032 |
([
|
| 1033 |
"convert C to GAL",
|
| 1034 |
"translate from C language",
|
|
|
|
| 1050 |
- `return` -> `reclaim`, `void` -> `empty`
|
| 1051 |
- `-x` -> `~x` (unary negation uses tilde)"""),
|
| 1052 |
|
|
|
|
| 1053 |
([
|
| 1054 |
"how to write comments in GAL code",
|
| 1055 |
"comment syntax slash slash",
|
|
|
|
| 1069 |
```
|
| 1070 |
Same syntax as C/Java."""),
|
| 1071 |
|
|
|
|
| 1072 |
([
|
| 1073 |
"identifier rules naming",
|
| 1074 |
"variable name rules",
|
|
|
|
| 1087 |
Valid: `x`, `count`, `myVar`, `total_sum`, `playerScore1`
|
| 1088 |
Invalid: `2count`, `_name`, `thisIsWayTooLong`"""),
|
| 1089 |
|
|
|
|
| 1090 |
([
|
| 1091 |
"type casting conversion",
|
| 1092 |
"convert between types",
|
|
|
|
| 1113 |
- `(vine)` — converts anything to string
|
| 1114 |
- `(branch)` — converts to boolean"""),
|
| 1115 |
|
|
|
|
| 1116 |
([
|
| 1117 |
"array built-in methods operations",
|
| 1118 |
"append to array add element",
|
|
|
|
| 1151 |
leaf chars[] = taper("abc"); // chars = ['a','b','c']
|
| 1152 |
```"""),
|
| 1153 |
|
|
|
|
| 1154 |
([
|
| 1155 |
"escape sequences special characters",
|
| 1156 |
"newline tab in string",
|
|
|
|
| 1179 |
plant("She said \\"hi\\"");
|
| 1180 |
```"""),
|
| 1181 |
|
|
|
|
| 1182 |
([
|
| 1183 |
"string concatenation combine join",
|
| 1184 |
"concatenate two strings",
|
|
|
|
| 1201 |
plant("{} {}", first, second); // cleaner
|
| 1202 |
```"""),
|
| 1203 |
|
|
|
|
|
|
|
|
|
|
| 1204 |
|
|
|
|
| 1205 |
([
|
| 1206 |
"program structure organization",
|
| 1207 |
"where does root go",
|
|
|
|
| 1237 |
- `root()` must end with `reclaim;`
|
| 1238 |
- No code is allowed after `root()`'s closing `}`"""),
|
| 1239 |
|
|
|
|
| 1240 |
([
|
| 1241 |
"scope rules variable visibility",
|
| 1242 |
"local variable global variable",
|
|
|
|
| 1280 |
|
| 1281 |
Variables in inner scopes can see outer scopes, but not vice versa."""),
|
| 1282 |
|
|
|
|
| 1283 |
([
|
| 1284 |
"how to make constant",
|
| 1285 |
"fertile keyword constant",
|
|
|
|
| 1305 |
- Multiple `fertile` declarations on one line are **not** allowed
|
| 1306 |
- `fertile` goes before the type: `fertile seed`, not `seed fertile`"""),
|
| 1307 |
|
|
|
|
| 1308 |
([
|
| 1309 |
"boolean values in GAL",
|
| 1310 |
"sunshine and frost meaning",
|
|
|
|
| 1335 |
|
| 1336 |
Use `!` to negate: `!sunshine` = `frost`"""),
|
| 1337 |
|
|
|
|
| 1338 |
([
|
| 1339 |
"function return type options",
|
| 1340 |
"empty function void return",
|
|
|
|
| 1373 |
- Non-empty functions must provide a value in their required final `reclaim`
|
| 1374 |
- The CFG requires every function to end with `reclaim`"""),
|
| 1375 |
|
|
|
|
| 1376 |
([
|
| 1377 |
"recursive function in GAL",
|
| 1378 |
"recursion example",
|
|
|
|
| 1401 |
- Each recursive call should move toward the base case
|
| 1402 |
- Be mindful of the 10,000 iteration limit (applies to deep recursion too)"""),
|
| 1403 |
|
|
|
|
| 1404 |
([
|
| 1405 |
"nested if statements",
|
| 1406 |
"if inside if nested",
|
|
|
|
| 1439 |
wither { ... }
|
| 1440 |
```"""),
|
| 1441 |
|
|
|
|
| 1442 |
([
|
| 1443 |
"break statement prune",
|
| 1444 |
"continue statement skip",
|
|
|
|
| 1472 |
- `skip` can only be used in loops (not in `harvest`)
|
| 1473 |
- Using them outside their valid context gives a semantic error"""),
|
| 1474 |
|
|
|
|
| 1475 |
([
|
| 1476 |
"nested loop inside loop",
|
| 1477 |
"double loop two loops",
|
|
|
|
| 1501 |
|
| 1502 |
Note: `prune` only exits the **innermost** loop. To exit an outer loop, use a flag variable."""),
|
| 1503 |
|
|
|
|
| 1504 |
([
|
| 1505 |
"two dimensional array detailed",
|
| 1506 |
"matrix operations rows columns",
|
|
|
|
| 1542 |
|
| 1543 |
Arrays are 0-indexed: valid indices for `arr[M][N]` are `[0..M-1][0..N-1]`."""),
|
| 1544 |
|
|
|
|
| 1545 |
([
|
| 1546 |
"array of bundles structs",
|
| 1547 |
"list of struct objects",
|
|
|
|
| 1579 |
**Syntax:** `bundle <Type> <name>[<size>];`
|
| 1580 |
Then access: `name[index].member`"""),
|
| 1581 |
|
|
|
|
| 1582 |
([
|
| 1583 |
"nested bundle struct inside struct",
|
| 1584 |
"bundle with bundle member",
|
|
|
|
| 1614 |
|
| 1615 |
Define inner bundles **before** the outer bundle that uses them."""),
|
| 1616 |
|
|
|
|
| 1617 |
([
|
| 1618 |
"ts function get length",
|
| 1619 |
"array length size",
|
|
|
|
| 1642 |
- Does NOT work on `seed`, `tree`, `leaf`, or `branch` scalars
|
| 1643 |
- `TS()` is GAL's equivalent of `len()` / `.length`"""),
|
| 1644 |
|
|
|
|
| 1645 |
([
|
| 1646 |
"taper function usage",
|
| 1647 |
"split string into characters",
|
|
|
|
| 1668 |
- Works on `vine` (string) values
|
| 1669 |
- `taper()` is GAL's equivalent of `split('')` / `toCharArray()`"""),
|
| 1670 |
|
|
|
|
| 1671 |
([
|
| 1672 |
"operator precedence order",
|
| 1673 |
"which operator evaluated first",
|
|
|
|
| 1693 |
branch check = (a > 0) && (b < 10);
|
| 1694 |
```"""),
|
| 1695 |
|
|
|
|
| 1696 |
([
|
| 1697 |
"prefix postfix increment",
|
| 1698 |
"i++ vs ++i difference",
|
|
|
|
| 1727 |
- Cannot be chained: `x++++` is invalid
|
| 1728 |
- Cannot combine with binary ops: `x++ + 1` needs separate statements"""),
|
| 1729 |
|
|
|
|
| 1730 |
([
|
| 1731 |
"tilde operator negation",
|
| 1732 |
"negative number in GAL",
|
|
|
|
| 1756 |
tree negPi = ~3.14; // -3.14
|
| 1757 |
```"""),
|
| 1758 |
|
|
|
|
| 1759 |
([
|
| 1760 |
"compound assignment operators",
|
| 1761 |
"plus equals minus equals",
|
|
|
|
| 1787 |
- `%=` requires `seed` operands (modulo needs integers)
|
| 1788 |
- Cannot use on `fertile` (const) variables"""),
|
| 1789 |
|
|
|
|
| 1790 |
([
|
| 1791 |
"format string in plant",
|
| 1792 |
"placeholder curly braces",
|
|
|
|
| 1817 |
// GOOD: plant("{} {}", a, b); // 2 placeholders, 2 args
|
| 1818 |
```"""),
|
| 1819 |
|
|
|
|
| 1820 |
([
|
| 1821 |
"limits constraints maximum GAL",
|
| 1822 |
"what are the GAL limits",
|
|
|
|
| 1849 |
- Arrays are 0-indexed
|
| 1850 |
- `root()` function is required"""),
|
| 1851 |
|
|
|
|
| 1852 |
([
|
| 1853 |
"how does compiler work",
|
| 1854 |
"compilation process stages",
|
|
|
|
| 1884 |
**5. ICG** (`icg.py`) — Generates three-address code (parallel to semantic):
|
| 1885 |
- Produces intermediate representation for analysis"""),
|
| 1886 |
|
|
|
|
| 1887 |
([
|
| 1888 |
"forgot semicolon error",
|
| 1889 |
"where do I put semicolons",
|
|
|
|
| 1912 |
- Functions: `pollinate seed fn() { ... }`
|
| 1913 |
- `root()`: `root() { ... }`"""),
|
| 1914 |
|
|
|
|
| 1915 |
([
|
| 1916 |
"type mismatch error help",
|
| 1917 |
"cannot assign wrong type",
|
|
|
|
| 1946 |
- `!` only works on `branch`
|
| 1947 |
- Comparisons require compatible types"""),
|
| 1948 |
|
|
|
|
| 1949 |
([
|
| 1950 |
"variable not declared error",
|
| 1951 |
"undefined undeclared variable",
|
|
|
|
| 1981 |
// Fix: move declaration before use
|
| 1982 |
```"""),
|
| 1983 |
|
|
|
|
| 1984 |
([
|
| 1985 |
"used wrong keyword from C",
|
| 1986 |
"if instead of spring",
|
|
|
|
| 2013 |
|
| 2014 |
Tip: Use the keyword reference for the complete mapping."""),
|
| 2015 |
|
|
|
|
| 2016 |
([
|
| 2017 |
"fertile constant error",
|
| 2018 |
"cannot reassign fertile",
|
|
|
|
| 2052 |
- Only literal values (no expressions): `fertile seed X = 2 + 3;` is invalid
|
| 2053 |
- `fertile` goes before the type: `fertile seed`, not `seed fertile`"""),
|
| 2054 |
|
|
|
|
| 2055 |
([
|
| 2056 |
"factorial example program",
|
| 2057 |
"recursive example code",
|
|
|
|
| 2093 |
}
|
| 2094 |
```"""),
|
| 2095 |
|
|
|
|
| 2096 |
([
|
| 2097 |
"array operations example",
|
| 2098 |
"sum of array elements",
|
|
|
|
| 2149 |
}
|
| 2150 |
```"""),
|
| 2151 |
|
|
|
|
| 2152 |
([
|
| 2153 |
"sorting example program",
|
| 2154 |
"bubble sort GAL code",
|
|
|
|
| 2181 |
}
|
| 2182 |
```"""),
|
| 2183 |
|
|
|
|
| 2184 |
([
|
| 2185 |
"bundle struct example program",
|
| 2186 |
"complete bundle example",
|
|
|
|
| 2221 |
}
|
| 2222 |
```"""),
|
| 2223 |
|
|
|
|
| 2224 |
([
|
| 2225 |
"difference between GAL and C", "GAL vs C", "how is GAL different from C",
|
| 2226 |
"compare GAL and C", "GAL compared to C", "what makes GAL unique",
|
|
|
|
| 2259 |
- GAL uses `~` for **unary negation** (not `-`)
|
| 2260 |
- Format strings use `{}` placeholders (like Python), not `%d`/`%s`"""),
|
| 2261 |
|
|
|
|
| 2262 |
([
|
| 2263 |
"common mistakes in GAL", "GAL pitfalls", "beginners mistakes",
|
| 2264 |
"what mistakes do people make", "things to watch out for in GAL",
|
|
|
|
| 2298 |
|
| 2299 |
10. **Array index out of bounds** — indices are 0 to `TS(arr)-1`"""),
|
| 2300 |
|
|
|
|
| 2301 |
([
|
| 2302 |
"how to debug GAL", "debugging GAL code", "my GAL code doesn't work",
|
| 2303 |
"GAL code not working", "how to fix GAL errors", "debug my code",
|
|
|
|
| 2331 |
|
| 2332 |
**Pro tip:** Paste any error message into this chat — I can explain it in detail!"""),
|
| 2333 |
|
|
|
|
| 2334 |
([
|
| 2335 |
"what is GAL", "what is the GAL language", "tell me about GAL",
|
| 2336 |
"GAL programming language", "what is this language", "about GAL",
|
|
|
|
| 2356 |
```
|
| 2357 |
This prints "Hello, Garden!" — `root()` is like `main()`, `plant()` is like `printf()`, and `reclaim` is like `return`."""),
|
| 2358 |
|
|
|
|
| 2359 |
([
|
| 2360 |
"how to run GAL", "how to execute", "run my program", "how to compile",
|
| 2361 |
"how to use this IDE", "how to use the editor", "where do I type code",
|
|
|
|
| 2387 |
|
| 2388 |
Just type this in the editor and hit Run!"""),
|
| 2389 |
|
|
|
|
| 2390 |
([
|
| 2391 |
"string operations", "how to work with strings", "vine operations",
|
| 2392 |
"string manipulation", "string functions", "what can I do with strings",
|
|
|
|
| 2429 |
- `\\\\"` — literal double quote
|
| 2430 |
- `\\\\` — literal backslash"""),
|
| 2431 |
|
|
|
|
| 2432 |
([
|
| 2433 |
"math in GAL", "arithmetic operations", "math operations",
|
| 2434 |
"how to do math", "calculations in GAL", "number operations",
|
|
|
|
| 2472 |
]
|
| 2473 |
|
| 2474 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2475 |
_st_model = None
|
| 2476 |
_phrase_embeddings = None
|
| 2477 |
_phrase_topic_idx = []
|
| 2478 |
_responses = []
|
| 2479 |
+
_last_topic_idx = None
|
| 2480 |
+
_last_query = ""
|
| 2481 |
|
| 2482 |
|
|
|
|
| 2483 |
_SYNONYMS = {
|
| 2484 |
"int": "seed", "integer": "seed",
|
| 2485 |
"float": "tree", "double": "tree", "decimal": "tree",
|
|
|
|
| 2527 |
"number": "seed tree integer float arithmetic",
|
| 2528 |
}
|
| 2529 |
|
|
|
|
|
|
|
|
|
|
| 2530 |
_GAL_KEYWORD_MAP = {
|
| 2531 |
"seed": "seed data type integer variable declaration",
|
| 2532 |
"tree": "tree data type float decimal variable declaration",
|
|
|
|
| 2562 |
}
|
| 2563 |
|
| 2564 |
|
|
|
|
|
|
|
| 2565 |
_GREETING_PATTERNS = [
|
| 2566 |
(_re.compile(r"^\s*(hi|hello|hey|howdy|sup|yo|greetings|good\s*(morning|afternoon|evening))\b", _re.I),
|
| 2567 |
"Hey there! I'm the GAL AI Assistant. Ask me anything about GAL — data types, loops, functions, arrays, I/O, and more!"),
|
|
|
|
| 2570 |
(_re.compile(r"^\s*(bye|goodbye|see\s*ya|later|cya)\b", _re.I),
|
| 2571 |
"Goodbye! Happy coding with GAL! 🌱"),
|
| 2572 |
(_re.compile(r"\b(what can you do|help me|what do you know|how can you help)\b", _re.I),
|
| 2573 |
+
None),
|
| 2574 |
(_re.compile(r"^\s*(who are you|what are you)\b", _re.I),
|
| 2575 |
"I'm the GAL AI Assistant — I help with GAL syntax, concepts, and debugging. Ask me about data types, loops, functions, arrays, or anything else in GAL!"),
|
| 2576 |
]
|
| 2577 |
|
| 2578 |
|
| 2579 |
def _encode(texts):
|
|
|
|
| 2580 |
return _st_model.encode(texts, normalize_embeddings=True, show_progress_bar=False)
|
| 2581 |
|
| 2582 |
|
| 2583 |
def _ensure_model():
|
|
|
|
| 2584 |
global _st_model, _phrase_embeddings, _phrase_topic_idx, _responses
|
| 2585 |
if _st_model is not None:
|
| 2586 |
return
|
|
|
|
| 2588 |
from sentence_transformers import SentenceTransformer
|
| 2589 |
import os
|
| 2590 |
|
|
|
|
| 2591 |
finetuned = os.path.join(os.path.dirname(__file__), "..", "gal-mpnet-finetuned")
|
| 2592 |
if os.path.isdir(finetuned):
|
| 2593 |
_st_model = SentenceTransformer(finetuned)
|
|
|
|
| 2607 |
_phrase_embeddings = _encode(all_phrases)
|
| 2608 |
|
| 2609 |
|
|
|
|
| 2610 |
_DEFAULT_RESPONSE = """I can help with GAL syntax and concepts! Try asking about:
|
| 2611 |
- **Data types**: seed, tree, leaf, vine, branch
|
| 2612 |
- **Variables**: declarations, constants (`fertile`), scope rules
|
|
|
|
| 2625 |
|
| 2626 |
*Note: I'm running in offline mode right now. For more detailed help, try again later when the AI service is available.*"""
|
| 2627 |
|
|
|
|
|
|
|
| 2628 |
import random as _random
|
| 2629 |
|
| 2630 |
_CONFIDENT_INTROS = [
|
|
|
|
| 2634 |
"Absolutely! ",
|
| 2635 |
"Good question — ",
|
| 2636 |
"Here you go:\n\n",
|
| 2637 |
+
"",
|
| 2638 |
"",
|
| 2639 |
]
|
| 2640 |
|
|
|
|
| 2663 |
"\n\n---\n*Feel free to ask follow-up questions!*",
|
| 2664 |
"\n\n---\n*Let me know if you need more details on any part!*",
|
| 2665 |
"\n\n---\n*Want me to explain any part further?*",
|
| 2666 |
+
"",
|
| 2667 |
"",
|
| 2668 |
]
|
| 2669 |
|
| 2670 |
+
_conv_history = []
|
| 2671 |
+
_MAX_HISTORY = 8
|
|
|
|
| 2672 |
|
| 2673 |
|
| 2674 |
def _expand_query(text):
|
|
|
|
|
|
|
|
|
|
| 2675 |
words = text.lower().split()
|
| 2676 |
extras = set()
|
| 2677 |
|
|
|
|
| 2678 |
for w in words:
|
| 2679 |
if w in _SYNONYMS:
|
| 2680 |
extras.add(_SYNONYMS[w])
|
| 2681 |
|
|
|
|
| 2682 |
lower = text.lower()
|
| 2683 |
for phrase, replacement in _SYNONYMS.items():
|
| 2684 |
if " " in phrase and phrase in lower:
|
| 2685 |
extras.add(replacement)
|
| 2686 |
|
|
|
|
|
|
|
| 2687 |
for w in words:
|
| 2688 |
if w in _GAL_KEYWORD_MAP:
|
| 2689 |
extras.add(_GAL_KEYWORD_MAP[w])
|
| 2690 |
|
|
|
|
| 2691 |
for kw, desc in _GAL_KEYWORD_MAP.items():
|
| 2692 |
if " " in kw and kw in lower:
|
| 2693 |
extras.add(desc)
|
|
|
|
| 2698 |
|
| 2699 |
|
| 2700 |
def _detect_intent(msg):
|
|
|
|
| 2701 |
low = msg.lower()
|
| 2702 |
if any(w in low for w in ["how do i", "how to", "how can i", "how would"]):
|
| 2703 |
return "how-to"
|
|
|
|
| 2717 |
|
| 2718 |
|
| 2719 |
def _is_followup(msg):
|
|
|
|
| 2720 |
low = msg.lower().split()
|
|
|
|
| 2721 |
if len(low) <= 5 and any(w in msg.lower() for w in [
|
| 2722 |
"it", "that", "this", "those", "them", "more", "also",
|
| 2723 |
"too", "same", "again", "another", "other"
|
| 2724 |
]):
|
| 2725 |
return True
|
|
|
|
| 2726 |
if _detect_intent(msg) == "more":
|
| 2727 |
return True
|
| 2728 |
return False
|
| 2729 |
|
| 2730 |
|
| 2731 |
def _pick_intro(score, intent, is_followup):
|
|
|
|
| 2732 |
if is_followup:
|
| 2733 |
return _random.choice(_FOLLOWUP_INTROS)
|
| 2734 |
if score > 0.6:
|
|
|
|
| 2739 |
|
| 2740 |
|
| 2741 |
def _wrap_response(raw_response, score, intent, is_followup, has_blend=False):
|
|
|
|
| 2742 |
intro = _pick_intro(score, intent, is_followup)
|
| 2743 |
outro = _random.choice(_OUTROS) if not has_blend else ""
|
| 2744 |
return intro + raw_response + outro
|
| 2745 |
|
| 2746 |
|
| 2747 |
def fallback_reply(user_message):
|
|
|
|
|
|
|
| 2748 |
import numpy as np
|
| 2749 |
global _last_topic_idx, _last_query
|
| 2750 |
|
| 2751 |
msg = user_message.strip()
|
| 2752 |
|
|
|
|
| 2753 |
for pattern, response in _GREETING_PATTERNS:
|
| 2754 |
if pattern.search(msg):
|
| 2755 |
return response if response else _DEFAULT_RESPONSE
|
|
|
|
| 2757 |
if not msg or len(msg) < 2:
|
| 2758 |
return _DEFAULT_RESPONSE
|
| 2759 |
|
|
|
|
| 2760 |
if len(msg) < 4 and msg.lower() not in _GAL_KEYWORD_MAP:
|
| 2761 |
return _DEFAULT_RESPONSE
|
| 2762 |
|
|
|
|
| 2763 |
rule_match = _rule_engine_match(msg)
|
| 2764 |
if rule_match:
|
| 2765 |
_last_query = msg
|
|
|
|
| 2768 |
_conv_history.pop(0)
|
| 2769 |
return rule_match
|
| 2770 |
|
|
|
|
| 2771 |
_ensure_model()
|
| 2772 |
|
| 2773 |
intent = _detect_intent(msg)
|
| 2774 |
is_followup = _is_followup(msg) and _conv_history
|
| 2775 |
|
|
|
|
| 2776 |
expanded = _expand_query(msg)
|
| 2777 |
|
|
|
|
| 2778 |
if is_followup and _conv_history:
|
|
|
|
| 2779 |
recent = [h[0] for h in _conv_history[-2:]]
|
| 2780 |
expanded = " ".join(recent) + " " + expanded
|
| 2781 |
|
|
|
|
| 2782 |
query_emb = _encode([expanded])
|
| 2783 |
scores = np.dot(_phrase_embeddings, query_emb.T).flatten()
|
| 2784 |
|
|
|
|
| 2785 |
topic_best = {}
|
| 2786 |
for i, score in enumerate(scores):
|
| 2787 |
tidx = _phrase_topic_idx[i]
|
|
|
|
| 2793 |
|
| 2794 |
if best_score < _THRESHOLD:
|
| 2795 |
_last_query = msg
|
|
|
|
| 2796 |
if is_followup:
|
| 2797 |
bare_expanded = _expand_query(msg)
|
| 2798 |
query_emb2 = _encode([bare_expanded])
|
|
|
|
| 2811 |
else:
|
| 2812 |
return _DEFAULT_RESPONSE
|
| 2813 |
|
|
|
|
| 2814 |
result = _responses[best_idx]
|
| 2815 |
has_blend = False
|
| 2816 |
|
|
|
|
| 2817 |
if len(ranked) >= 2:
|
| 2818 |
second_idx, second_score = ranked[1]
|
| 2819 |
gap = best_score - second_score
|
| 2820 |
if second_score >= _THRESHOLD and gap < 0.07:
|
|
|
|
| 2821 |
transition = _random.choice(_BLEND_TRANSITIONS)
|
| 2822 |
result += transition + _responses[second_idx]
|
| 2823 |
has_blend = True
|
| 2824 |
|
|
|
|
| 2825 |
result = _wrap_response(result, best_score, intent, is_followup, has_blend)
|
| 2826 |
|
|
|
|
| 2827 |
_last_topic_idx = best_idx
|
| 2828 |
_last_query = msg
|
| 2829 |
_conv_history.append((msg, best_idx, best_score))
|
Backend/cfg/__init__.py
CHANGED
|
@@ -1,9 +1,3 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# CFG PACKAGE - Context-free grammar + parse table for the GAL parser
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Re-exports the three public artifacts so `from cfg import cfg, first_sets,
|
| 5 |
-
# predict_sets` keeps working unchanged after the restructure.
|
| 6 |
-
# ============================================================================
|
| 7 |
|
| 8 |
from .grammar import ( # noqa: F401
|
| 9 |
cfg,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
from .grammar import ( # noqa: F401
|
| 3 |
cfg,
|
Backend/cfg/grammar.py
CHANGED
|
@@ -1,73 +1,47 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# CFG GRAMMAR + FIRST/FOLLOW/PREDICT - the static parse table source
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from Backend/cfg.py during the modular restructure.
|
| 5 |
-
# Exports three artifacts consumed by parser/parser.py:
|
| 6 |
-
# cfg : Dict[non_terminal -> List[List[symbol]]]
|
| 7 |
-
# first_sets : Dict[non_terminal -> Set[terminal]]
|
| 8 |
-
# predict_sets : Dict[(non_terminal, production) -> Set[terminal]]
|
| 9 |
-
# ============================================================================
|
| 10 |
import sys
|
| 11 |
from collections import defaultdict
|
| 12 |
|
| 13 |
-
# Set UTF-8 encoding for Windows console (so λ prints correctly during dev)
|
| 14 |
if sys.platform == 'win32':
|
| 15 |
try:
|
| 16 |
sys.stdout.reconfigure(encoding='utf-8')
|
| 17 |
except:
|
| 18 |
pass
|
| 19 |
|
| 20 |
-
# Use λ (lambda) as epsilon symbol - represents empty/null production
|
| 21 |
EPSILON = "λ"
|
| 22 |
|
| 23 |
|
| 24 |
def compute_first(cfg):
|
| 25 |
-
|
| 26 |
-
Compute FIRST sets for all non-terminals in the grammar.
|
| 27 |
-
|
| 28 |
-
FIRST(X) = set of terminals that can appear at the beginning
|
| 29 |
-
of any string derived from X
|
| 30 |
-
|
| 31 |
-
Example: If X -> aB, then 'a' is in FIRST(X)
|
| 32 |
-
"""
|
| 33 |
-
first = defaultdict(set) # Dictionary to store FIRST sets for each non-terminal
|
| 34 |
epsilon = EPSILON
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
for prod in productions: # prod = production (right-hand side)
|
| 39 |
if not prod:
|
| 40 |
continue
|
| 41 |
-
if prod[0] == epsilon:
|
| 42 |
first[lhs].add(epsilon)
|
| 43 |
-
elif prod[0] not in cfg:
|
| 44 |
first[lhs].add(prod[0])
|
| 45 |
|
| 46 |
-
# Iterative pass: Keep updating FIRST sets until no changes occur
|
| 47 |
changed = True
|
| 48 |
while changed:
|
| 49 |
changed = False
|
| 50 |
for lhs, productions in cfg.items():
|
| 51 |
for prod in productions:
|
| 52 |
-
before = len(first[lhs])
|
| 53 |
|
| 54 |
-
# Process each symbol in the production
|
| 55 |
for symbol in prod:
|
| 56 |
-
if symbol in cfg:
|
| 57 |
-
# Add FIRST(symbol) to FIRST(lhs), excluding epsilon
|
| 58 |
first[lhs] |= (first[symbol] - {epsilon})
|
| 59 |
-
# If symbol cannot derive epsilon, stop here
|
| 60 |
if epsilon not in first[symbol]:
|
| 61 |
break
|
| 62 |
-
else:
|
| 63 |
if symbol != epsilon:
|
| 64 |
first[lhs].add(symbol)
|
| 65 |
break
|
| 66 |
else:
|
| 67 |
-
# All symbols can derive epsilon, so this production can too
|
| 68 |
first[lhs].add(epsilon)
|
| 69 |
|
| 70 |
-
# Check if FIRST set grew
|
| 71 |
if len(first[lhs]) > before:
|
| 72 |
changed = True
|
| 73 |
|
|
@@ -75,53 +49,36 @@ def compute_first(cfg):
|
|
| 75 |
|
| 76 |
|
| 77 |
def compute_follow(cfg, first):
|
| 78 |
-
|
| 79 |
-
Compute FOLLOW sets for all non-terminals in the grammar.
|
| 80 |
-
|
| 81 |
-
FOLLOW(X) = set of terminals that can appear immediately after X
|
| 82 |
-
in any derivation
|
| 83 |
-
|
| 84 |
-
Example: If S -> AB, then FOLLOW(A) includes FIRST(B)
|
| 85 |
-
"""
|
| 86 |
-
follow = defaultdict(set) # Dictionary to store FOLLOW sets
|
| 87 |
epsilon = EPSILON
|
| 88 |
|
| 89 |
-
|
| 90 |
-
start_symbol = next(iter(cfg)) # First non-terminal in grammar
|
| 91 |
follow[start_symbol].add("EOF")
|
| 92 |
|
| 93 |
-
# Iterative pass: Keep updating until no changes
|
| 94 |
changed = True
|
| 95 |
while changed:
|
| 96 |
changed = False
|
| 97 |
for lhs, productions in cfg.items():
|
| 98 |
for prod in productions:
|
| 99 |
-
# Check each symbol in the production
|
| 100 |
for i, symbol in enumerate(prod):
|
| 101 |
-
if symbol in cfg:
|
| 102 |
before = len(follow[symbol])
|
| 103 |
|
| 104 |
-
# Look at all symbols that come after this symbol
|
| 105 |
j = i + 1
|
| 106 |
while j < len(prod):
|
| 107 |
next_symbol = prod[j]
|
| 108 |
-
if next_symbol in cfg:
|
| 109 |
-
# Add FIRST(next_symbol) to FOLLOW(symbol), excluding epsilon
|
| 110 |
follow[symbol] |= (first[next_symbol] - {epsilon})
|
| 111 |
-
# If next symbol cannot derive epsilon, stop here
|
| 112 |
if epsilon not in first[next_symbol]:
|
| 113 |
break
|
| 114 |
-
else:
|
| 115 |
if next_symbol != epsilon:
|
| 116 |
follow[symbol].add(next_symbol)
|
| 117 |
break
|
| 118 |
j += 1
|
| 119 |
else:
|
| 120 |
-
# All remaining symbols can derive epsilon (or we reached the end)
|
| 121 |
-
# Add FOLLOW(lhs) to FOLLOW(symbol)
|
| 122 |
follow[symbol] |= follow[lhs]
|
| 123 |
|
| 124 |
-
# Check if FOLLOW set grew
|
| 125 |
if len(follow[symbol]) > before:
|
| 126 |
changed = True
|
| 127 |
|
|
@@ -129,481 +86,353 @@ def compute_follow(cfg, first):
|
|
| 129 |
|
| 130 |
|
| 131 |
def compute_predict(cfg, first, follow):
|
| 132 |
-
|
| 133 |
-
Compute PREDICT sets for all productions in the grammar.
|
| 134 |
-
|
| 135 |
-
PREDICT(A -> α) = set of terminals that indicate when to use this production
|
| 136 |
-
|
| 137 |
-
Used to build parsing table for predictive (LL) parser:
|
| 138 |
-
- If next input token is in PREDICT(A -> α), use that production
|
| 139 |
-
|
| 140 |
-
Rules:
|
| 141 |
-
1. Add FIRST(α) to PREDICT
|
| 142 |
-
2. If α can derive epsilon, also add FOLLOW(A)
|
| 143 |
-
"""
|
| 144 |
-
predict = {} # Dictionary to store PREDICT sets
|
| 145 |
epsilon = EPSILON
|
| 146 |
|
| 147 |
for lhs, productions in cfg.items():
|
| 148 |
for prod in productions:
|
| 149 |
-
# Key is (non-terminal, production tuple)
|
| 150 |
key = (lhs, tuple(prod))
|
| 151 |
predict[key] = set()
|
| 152 |
|
| 153 |
-
# Check if production is epsilon (empty or just epsilon symbol)
|
| 154 |
if not prod or (len(prod) == 1 and prod[0] == epsilon):
|
| 155 |
-
# Epsilon production: PREDICT = FOLLOW(lhs)
|
| 156 |
predict[key] = follow[lhs].copy()
|
| 157 |
continue
|
| 158 |
|
| 159 |
-
# Compute FIRST set of this production
|
| 160 |
first_set = set()
|
| 161 |
for symbol in prod:
|
| 162 |
-
if symbol in cfg:
|
| 163 |
-
# Add FIRST(symbol) excluding epsilon
|
| 164 |
first_set |= (first[symbol] - {epsilon})
|
| 165 |
-
# If symbol cannot derive epsilon, stop
|
| 166 |
if epsilon not in first[symbol]:
|
| 167 |
break
|
| 168 |
-
else:
|
| 169 |
if symbol != epsilon:
|
| 170 |
first_set.add(symbol)
|
| 171 |
break
|
| 172 |
else:
|
| 173 |
-
# All symbols can derive epsilon
|
| 174 |
first_set.add(epsilon)
|
| 175 |
|
| 176 |
-
# PREDICT set starts with FIRST set
|
| 177 |
predict[key] = first_set
|
| 178 |
-
# If production can derive epsilon, add FOLLOW(lhs)
|
| 179 |
if epsilon in first_set:
|
| 180 |
predict[key] |= follow[lhs]
|
| 181 |
|
| 182 |
return predict
|
| 183 |
|
| 184 |
|
| 185 |
-
# ===============================================================================
|
| 186 |
-
# GAL (Grow A Language) Context-Free Grammar Definition
|
| 187 |
-
# ===============================================================================
|
| 188 |
-
# This dictionary defines the complete grammar for the GAL programming language.
|
| 189 |
-
# Format: { "<non-terminal>": [ [production1], [production2], ... ] }
|
| 190 |
-
#
|
| 191 |
-
# Non-terminals: Enclosed in < >, represent grammar rules (e.g., <program>)
|
| 192 |
-
# Terminals: Actual tokens from the lexer (e.g., "seed", "id", "{")
|
| 193 |
-
# λ (EPSILON): Represents empty production (optional/nullable rules)
|
| 194 |
-
# ===============================================================================
|
| 195 |
-
|
| 196 |
cfg = {
|
| 197 |
-
# ===== PROGRAM STRUCTURE =====
|
| 198 |
-
# Entry point: Every GAL program must have a root() function
|
| 199 |
"<program>": [
|
| 200 |
[
|
| 201 |
-
"<global_declaration>",
|
| 202 |
-
"<function_definition>",
|
| 203 |
-
"root",
|
| 204 |
"(",
|
| 205 |
")",
|
| 206 |
"{",
|
| 207 |
-
"<local_declaration>",
|
| 208 |
-
"<body_statement>",
|
| 209 |
-
"reclaim",
|
| 210 |
";",
|
| 211 |
"}",
|
| 212 |
]
|
| 213 |
],
|
| 214 |
|
| 215 |
-
# ===== GLOBAL DECLARATIONS =====
|
| 216 |
-
# Variables, constants, and structs declared outside functions
|
| 217 |
"<global_declaration>": [
|
| 218 |
-
["bundle", "id", "<bundle_or_var>", "<global_declaration>"],
|
| 219 |
-
["<data_type>", "id", "<array_dec>", "<var_value>", ";", "<global_declaration>"],
|
| 220 |
-
["fertile", "<data_type>", "id", "=", "<init_val>", "<const_next>", ";", "<global_declaration>"],
|
| 221 |
-
[EPSILON],
|
| 222 |
],
|
| 223 |
|
| 224 |
-
# Distinguish between bundle definition and bundle variable after seeing "bundle id"
|
| 225 |
"<bundle_or_var>": [
|
| 226 |
-
["{", "<bundle_members>", "}", ";"],
|
| 227 |
-
["<bundle_mem_dec>", ";"],
|
| 228 |
],
|
| 229 |
|
| 230 |
-
# ===== LOCAL DECLARATIONS =====
|
| 231 |
-
# Variables and constants declared inside functions
|
| 232 |
"<local_declaration>": [
|
| 233 |
-
["<var_dec>", ";", "<local_declaration>"],
|
| 234 |
-
["<const_dec>", ";", "<local_declaration>"],
|
| 235 |
-
[EPSILON],
|
| 236 |
],
|
| 237 |
|
| 238 |
-
# ===== DATA TYPES =====
|
| 239 |
-
# The five primitive types in GAL
|
| 240 |
"<data_type>": [
|
| 241 |
-
["seed"],
|
| 242 |
-
["tree"],
|
| 243 |
-
["leaf"],
|
| 244 |
-
["branch"],
|
| 245 |
-
["vine"],
|
| 246 |
],
|
| 247 |
|
| 248 |
-
# ===== CONSTANT DECLARATIONS =====
|
| 249 |
-
# Example: fertile seed MAX = 100;
|
| 250 |
-
# fertile leaf NEWLINE = '\n', TAB = '\t';
|
| 251 |
"<const_dec>": [
|
| 252 |
["fertile", "<data_type>", "id", "=", "<init_val>", "<const_next>"],
|
| 253 |
],
|
| 254 |
|
| 255 |
"<const_next>": [
|
| 256 |
-
[",", "id", "=", "<init_val>", "<const_next>"],
|
| 257 |
-
[EPSILON],
|
| 258 |
],
|
| 259 |
|
| 260 |
-
# ===== VARIABLE DECLARATIONS =====
|
| 261 |
-
# Examples: seed x = 5;
|
| 262 |
-
# tree arr[10];
|
| 263 |
-
# bundle Person p;
|
| 264 |
-
# bundle Person group[2];
|
| 265 |
"<var_dec>": [
|
| 266 |
-
["<data_type>", "id", "<array_dec>", "<var_value>"],
|
| 267 |
-
["bundle", "id", "<bundle_mem_dec>"],
|
| 268 |
],
|
| 269 |
|
| 270 |
"<bundle_mem_dec>": [
|
| 271 |
-
["id", "<array_dec>"],
|
| 272 |
],
|
| 273 |
|
| 274 |
"<var_value>": [
|
| 275 |
-
["=", "<init_val>", "<var_value_next>"],
|
| 276 |
-
["<var_value_next>"],
|
| 277 |
],
|
| 278 |
|
| 279 |
"<var_value_next>": [
|
| 280 |
-
[",", "id", "<array_dec>", "<var_value>"],
|
| 281 |
[EPSILON],
|
| 282 |
],
|
| 283 |
|
| 284 |
-
# ===== INITIALIZATION VALUES =====
|
| 285 |
"<init_val>": [
|
| 286 |
-
["<array_init_opt>"],
|
| 287 |
-
["water", "(", "<water_arg>", ")"],
|
| 288 |
-
["<expression>"],
|
| 289 |
],
|
| 290 |
|
| 291 |
-
# ===== ARRAY DECLARATIONS =====
|
| 292 |
-
# Examples: seed arr[10]; (1D array - fixed size)
|
| 293 |
-
# tree vals[]; (1D array - dynamic size)
|
| 294 |
-
# seed matrix[2][3]; (2D array)
|
| 295 |
-
# seed cube[2][3][4]; (3D array)
|
| 296 |
"<array_dec>": [
|
| 297 |
-
["[", "<array_dim_opt>", "]", "<array_dec>"],
|
| 298 |
-
[EPSILON],
|
| 299 |
],
|
| 300 |
|
| 301 |
"<array_dim_opt>": [
|
| 302 |
-
["intlit"],
|
| 303 |
-
["dblit"],
|
| 304 |
-
[EPSILON],
|
| 305 |
],
|
| 306 |
|
| 307 |
-
# ===== ARRAY INITIALIZATION =====
|
| 308 |
-
# Examples: {1, 2, 3}
|
| 309 |
-
# {{1, 2}, {3, 4}} (nested arrays)
|
| 310 |
"<array_init_opt>": [
|
| 311 |
-
["{", "<init_vals>", "}"],
|
| 312 |
-
[EPSILON],
|
| 313 |
],
|
| 314 |
|
| 315 |
"<init_vals>": [
|
| 316 |
-
["<init_val_item>", "<init_vals_next>"],
|
| 317 |
-
[EPSILON],
|
| 318 |
],
|
| 319 |
|
| 320 |
"<init_vals_next>": [
|
| 321 |
-
[",", "<init_val_item>", "<init_vals_next>"],
|
| 322 |
-
[EPSILON],
|
| 323 |
],
|
| 324 |
|
| 325 |
"<init_val_item>": [
|
| 326 |
-
["{", "<init_vals>", "}"],
|
| 327 |
-
["<expression>"],
|
| 328 |
],
|
| 329 |
|
| 330 |
-
# ===== STRUCT (BUNDLE) DECLARATIONS =====
|
| 331 |
-
# Example: bundle Person {
|
| 332 |
-
# seed age;
|
| 333 |
-
# branch name;
|
| 334 |
-
# }
|
| 335 |
"<bundle_members>": [
|
| 336 |
-
["<data_type>", "id", ";", "<bundle_members>"],
|
| 337 |
-
["id", "id", ";", "<bundle_members>"],
|
| 338 |
-
[EPSILON],
|
| 339 |
],
|
| 340 |
|
| 341 |
-
# ===== FUNCTION DEFINITIONS =====
|
| 342 |
-
# Example: pollinate seed add(seed a, seed b) {
|
| 343 |
-
# reclaim a + b;
|
| 344 |
-
# }
|
| 345 |
"<function_definition>": [
|
| 346 |
[
|
| 347 |
-
"pollinate",
|
| 348 |
-
"<return_type>",
|
| 349 |
-
"id",
|
| 350 |
"(",
|
| 351 |
-
"<parameters>",
|
| 352 |
")",
|
| 353 |
"{",
|
| 354 |
-
"<local_declaration>",
|
| 355 |
-
"<body_statement>",
|
| 356 |
-
"reclaim",
|
| 357 |
-
"<reclaim_value>",
|
| 358 |
"}",
|
| 359 |
-
"<function_definition>",
|
| 360 |
],
|
| 361 |
-
[EPSILON],
|
| 362 |
],
|
| 363 |
|
| 364 |
-
# ===== RETURN TYPE =====
|
| 365 |
"<return_type>": [
|
| 366 |
-
["<data_type>"],
|
| 367 |
-
["empty"],
|
| 368 |
-
["id"],
|
| 369 |
],
|
| 370 |
|
| 371 |
-
# ===== FUNCTION PARAMETERS =====
|
| 372 |
-
# Examples: (seed x, tree y)
|
| 373 |
-
# () (no parameters)
|
| 374 |
"<parameters>": [
|
| 375 |
-
[EPSILON],
|
| 376 |
-
["<param>", "<param_next>"],
|
| 377 |
],
|
| 378 |
|
| 379 |
"<param>": [
|
| 380 |
-
["<data_type>", "id", "<param_array>"],
|
| 381 |
-
["id", "id"],
|
| 382 |
],
|
| 383 |
|
| 384 |
"<param_array>": [
|
| 385 |
-
[EPSILON],
|
| 386 |
-
["[", "]"],
|
| 387 |
],
|
| 388 |
|
| 389 |
"<param_next>": [
|
| 390 |
-
[EPSILON],
|
| 391 |
-
[",", "<param>", "<param_next>"],
|
| 392 |
],
|
| 393 |
|
| 394 |
-
# ===== RETURN STATEMENT =====
|
| 395 |
-
# Examples: reclaim x + y;
|
| 396 |
-
# reclaim; (for empty functions)
|
| 397 |
"<reclaim_value>": [
|
| 398 |
-
["<expression>", ";"],
|
| 399 |
-
[";"],
|
| 400 |
],
|
| 401 |
|
| 402 |
-
# ===== STATEMENTS =====
|
| 403 |
-
# Function/root body statements before the required final reclaim.
|
| 404 |
-
# Nested blocks still use <statement>, where an early return is legal.
|
| 405 |
"<body_statement>": [
|
| 406 |
["<non_reclaim_stmt>", "<body_statement>"],
|
| 407 |
[EPSILON],
|
| 408 |
],
|
| 409 |
|
| 410 |
"<non_reclaim_stmt>": [
|
| 411 |
-
["id", "<id_stmt>"],
|
| 412 |
-
["<inc_dec_op>", "id", "<id_next>", ";"],
|
| 413 |
-
["<io_stmt>"],
|
| 414 |
-
["<conditional_stmt>"],
|
| 415 |
-
["<loop_stmt>"],
|
| 416 |
-
["<switch_stmt>"],
|
| 417 |
-
["<control_stmt>"],
|
| 418 |
],
|
| 419 |
|
| 420 |
-
# A sequence of executable statements (zero or more).
|
| 421 |
-
# Used for nested blocks, where early reclaim remains allowed.
|
| 422 |
"<statement>": [
|
| 423 |
-
["<simple_stmt>", "<statement>"],
|
| 424 |
-
[EPSILON],
|
| 425 |
],
|
| 426 |
|
| 427 |
-
# ===== TYPES OF STATEMENTS =====
|
| 428 |
"<simple_stmt>": [
|
| 429 |
-
["<non_reclaim_stmt>"],
|
| 430 |
-
["reclaim", "<reclaim_value>"],
|
| 431 |
],
|
| 432 |
|
| 433 |
-
# After seeing id, determine what kind of statement it is.
|
| 434 |
-
# Left-factored so that <id_next> can be followed by either an assignment
|
| 435 |
-
# operator OR an inc/dec operator — this is what enables arr[i]++; and
|
| 436 |
-
# obj.field++; (and x++; via <id_next> -> epsilon).
|
| 437 |
"<id_stmt>": [
|
| 438 |
-
["<id_next>", "<id_stmt_tail>"],
|
| 439 |
-
["(", "<arguments>", ")", ";"],
|
| 440 |
],
|
| 441 |
|
| 442 |
-
# Tail of <id_stmt>: the operator that follows the (possibly-indexed/
|
| 443 |
-
# member-accessed) target. Either '=' (or compound '+=' etc.) followed
|
| 444 |
-
# by an RHS, or a postfix '++' / '--'.
|
| 445 |
"<id_stmt_tail>": [
|
| 446 |
-
["<assign_op>", "<assign_rhs>", ";"],
|
| 447 |
-
["<inc_dec_op>", ";"],
|
| 448 |
],
|
| 449 |
|
| 450 |
-
# ===== ASSIGNMENT STATEMENTS =====
|
| 451 |
-
# Examples: x = 10;
|
| 452 |
-
# arr[0] = 5;
|
| 453 |
-
# person.age = 25;
|
| 454 |
-
# total += 5;
|
| 455 |
-
# Assignment right-hand side: either water() input or a regular expression
|
| 456 |
"<assign_rhs>": [
|
| 457 |
-
["water", "(", "<water_arg>", ")"],
|
| 458 |
-
["<expression>"],
|
| 459 |
],
|
| 460 |
|
| 461 |
-
# Assignment operators
|
| 462 |
"<assign_op>": [
|
| 463 |
-
["="],
|
| 464 |
-
["+="],
|
| 465 |
-
["-="],
|
| 466 |
-
["*="],
|
| 467 |
-
["/="],
|
| 468 |
-
["%="],
|
| 469 |
-
["**="],
|
| 470 |
],
|
| 471 |
|
| 472 |
"<id_next>": [
|
| 473 |
-
["<array_access>", "<post_array_access>"],
|
| 474 |
-
["<struct_access>"],
|
| 475 |
-
[EPSILON],
|
| 476 |
],
|
| 477 |
|
| 478 |
-
# ===== ARRAY ACCESS =====
|
| 479 |
-
# Examples: arr[0]
|
| 480 |
-
# matrix[i][j] (multi-dimensional)
|
| 481 |
"<array_access>": [
|
| 482 |
["[", "<expression>", "]", "<array_access_more>"],
|
| 483 |
],
|
| 484 |
|
| 485 |
"<array_access_more>": [
|
| 486 |
-
["[", "<expression>", "]", "<array_access_more>"],
|
| 487 |
-
[EPSILON],
|
| 488 |
],
|
| 489 |
|
| 490 |
-
# ===== STRUCT/BUNDLE ACCESS =====
|
| 491 |
-
# Examples: person.name
|
| 492 |
-
# person.address.city (nested structs)
|
| 493 |
"<struct_access>": [
|
| 494 |
[".", "id", "<struct_access_more>"],
|
| 495 |
],
|
| 496 |
|
| 497 |
"<struct_access_more>": [
|
| 498 |
-
[".", "id", "<struct_access_more>"],
|
| 499 |
-
[EPSILON],
|
| 500 |
],
|
| 501 |
|
| 502 |
-
# Optional struct access after array access (e.g., p[0].x or p[0].addr.zip)
|
| 503 |
"<post_array_access>": [
|
| 504 |
-
[".", "id", "<post_array_access>"],
|
| 505 |
-
[EPSILON],
|
| 506 |
],
|
| 507 |
|
| 508 |
-
# ===== INPUT/OUTPUT STATEMENTS =====
|
| 509 |
-
# plant() - output/print function
|
| 510 |
-
# water() - input/read function
|
| 511 |
-
# Examples: plant("Hello");
|
| 512 |
-
# water();
|
| 513 |
"<io_stmt>": [
|
| 514 |
-
["plant", "(", "<arguments>", ")", ";"],
|
| 515 |
-
["water", "(", "<water_arg>", ")", ";"],
|
| 516 |
],
|
| 517 |
|
| 518 |
-
# Optional type argument for water()
|
| 519 |
"<water_arg>": [
|
| 520 |
-
["<data_type>"],
|
| 521 |
-
["id", "<water_id_tail>"],
|
| 522 |
-
[EPSILON],
|
| 523 |
],
|
| 524 |
|
| 525 |
"<water_id_tail>": [
|
| 526 |
-
["[", "<expression>", "]", "<water_id_tail>"],
|
| 527 |
-
[EPSILON],
|
| 528 |
],
|
| 529 |
|
| 530 |
-
# Argument list for function calls and I/O
|
| 531 |
"<arguments>": [
|
| 532 |
-
["<expression>", "<arg_next>"],
|
| 533 |
-
[EPSILON],
|
| 534 |
],
|
| 535 |
|
| 536 |
"<arg_next>": [
|
| 537 |
-
[",", "<expression>", "<arg_next>"],
|
| 538 |
-
[EPSILON],
|
| 539 |
-
],
|
| 540 |
-
|
| 541 |
-
# ===== CONDITIONAL STATEMENTS =====
|
| 542 |
-
# spring = if
|
| 543 |
-
# bud = else if
|
| 544 |
-
# wither = else
|
| 545 |
-
#
|
| 546 |
-
# Example: spring (x > 0) {
|
| 547 |
-
# water("positive");
|
| 548 |
-
# }
|
| 549 |
-
# bud (x < 0) {
|
| 550 |
-
# water("negative");
|
| 551 |
-
# }
|
| 552 |
-
# wither {
|
| 553 |
-
# water("zero");
|
| 554 |
-
# }
|
| 555 |
"<conditional_stmt>": [
|
| 556 |
[
|
| 557 |
-
"spring",
|
| 558 |
"(",
|
| 559 |
-
"<expression>",
|
| 560 |
")",
|
| 561 |
"{",
|
| 562 |
-
"<local_declaration>",
|
| 563 |
-
"<statement>",
|
| 564 |
"}",
|
| 565 |
-
"<elseif_chain>",
|
| 566 |
-
"<else_opt>",
|
| 567 |
]
|
| 568 |
],
|
| 569 |
|
| 570 |
"<elseif_chain>": [
|
| 571 |
[
|
| 572 |
-
"bud",
|
| 573 |
"(",
|
| 574 |
-
"<expression>",
|
| 575 |
")",
|
| 576 |
"{",
|
| 577 |
-
"<local_declaration>",
|
| 578 |
-
"<statement>",
|
| 579 |
"}",
|
| 580 |
-
"<elseif_chain>",
|
| 581 |
],
|
| 582 |
-
[EPSILON],
|
| 583 |
],
|
| 584 |
|
| 585 |
"<else_opt>": [
|
| 586 |
-
["wither", "{", "<local_declaration>", "<statement>", "}"],
|
| 587 |
-
[EPSILON],
|
| 588 |
],
|
| 589 |
|
| 590 |
-
# ===== LOOP STATEMENTS =====
|
| 591 |
-
# grow = while loop
|
| 592 |
-
# cultivate = for loop
|
| 593 |
-
# tend = do-while loop
|
| 594 |
"<loop_stmt>": [
|
| 595 |
-
# While loop: grow (x < 10) { ... }
|
| 596 |
["grow", "(", "<expression>", ")", "{", "<local_declaration>", "<statement>", "}"],
|
| 597 |
|
| 598 |
-
# For loop: cultivate (seed i = 0; i < 10; i++) { ... }
|
| 599 |
[
|
| 600 |
"cultivate",
|
| 601 |
"(",
|
| 602 |
-
"<for_init>",
|
| 603 |
";",
|
| 604 |
-
"<expression>",
|
| 605 |
";",
|
| 606 |
-
"<for_update>",
|
| 607 |
")",
|
| 608 |
"{",
|
| 609 |
"<local_declaration>",
|
|
@@ -611,120 +440,79 @@ cfg = {
|
|
| 611 |
"}",
|
| 612 |
],
|
| 613 |
|
| 614 |
-
# Do-while: tend { statements } grow ( condition );
|
| 615 |
["tend", "{", "<local_declaration>", "<statement>", "}", "grow", "(", "<expression>", ")", ";"],
|
| 616 |
],
|
| 617 |
|
| 618 |
"<for_init>": [
|
| 619 |
-
["<data_type>", "id", "<array_dec>", "<var_value>"],
|
| 620 |
-
["id", "<id_next>", "<assign_op>", "<expression>"],
|
| 621 |
-
[EPSILON],
|
| 622 |
],
|
| 623 |
|
| 624 |
"<for_update>": [
|
| 625 |
-
# Left-factored so <id_next> can be followed by EITHER an inc/dec
|
| 626 |
-
# operator OR an assignment operator. This is what lets
|
| 627 |
-
# cultivate(...; ...; arr[i]++) and obj.f += 2 work in for-updates.
|
| 628 |
["id", "<id_next>", "<for_update_tail>"],
|
| 629 |
-
[EPSILON],
|
| 630 |
],
|
| 631 |
|
| 632 |
"<for_update_tail>": [
|
| 633 |
-
["<inc_dec_op>"],
|
| 634 |
-
["<assign_op>", "<expression>"],
|
| 635 |
],
|
| 636 |
|
| 637 |
-
# ===== UNARY STATEMENTS =====
|
| 638 |
-
# Examples: x++; y--;
|
| 639 |
"<inc_dec_op>": [
|
| 640 |
-
["++"],
|
| 641 |
-
["--"],
|
| 642 |
-
],
|
| 643 |
-
|
| 644 |
-
# ===== SWITCH STATEMENT =====
|
| 645 |
-
# harvest = switch
|
| 646 |
-
# variety = case
|
| 647 |
-
# soil = default
|
| 648 |
-
# prune = break (required after each case)
|
| 649 |
-
#
|
| 650 |
-
# Example: harvest (choice) {
|
| 651 |
-
# variety (1): water("One"); prune;
|
| 652 |
-
# variety (2): water("Two"); prune;
|
| 653 |
-
# soil: water("Default");
|
| 654 |
-
# }
|
| 655 |
"<switch_stmt>": [
|
| 656 |
["harvest", "(", "<expression>", ")", "{", "<case_list>", "<default_opt>", "}"],
|
| 657 |
],
|
| 658 |
|
| 659 |
"<case_list>": [
|
| 660 |
[
|
| 661 |
-
"variety",
|
| 662 |
-
"<case_literal>",
|
| 663 |
":",
|
| 664 |
-
"<local_declaration>",
|
| 665 |
-
"<case_statements>",
|
| 666 |
-
"<case_list>",
|
| 667 |
],
|
| 668 |
-
[EPSILON],
|
| 669 |
],
|
| 670 |
|
| 671 |
-
# Only literal values are allowed after variety (case)
|
| 672 |
"<case_literal>": [
|
| 673 |
-
["
|
| 674 |
-
["dblit"], # Double/float literal (e.g., 3.14)
|
| 675 |
-
["chrlit"], # Character literal (e.g., 'a')
|
| 676 |
-
["stringlit"], # String literal (e.g., "hello")
|
| 677 |
-
["sunshine"], # Boolean true
|
| 678 |
-
["frost"], # Boolean false
|
| 679 |
],
|
| 680 |
|
| 681 |
-
# Executable statements inside a case - recursive list of statements
|
| 682 |
"<case_statements>": [
|
| 683 |
-
["<case_statement>", "<case_statements>"],
|
| 684 |
-
[EPSILON],
|
| 685 |
],
|
| 686 |
|
| 687 |
-
# Executable statement types allowed in a case body
|
| 688 |
"<case_statement>": [
|
| 689 |
-
["id", "<id_stmt>"],
|
| 690 |
-
["<inc_dec_op>", "id", "<id_next>", ";"],
|
| 691 |
-
["<io_stmt>"],
|
| 692 |
-
["<conditional_stmt>"],
|
| 693 |
-
["<loop_stmt>"],
|
| 694 |
-
["<switch_stmt>"],
|
| 695 |
-
["{", "<local_declaration>", "<statement>", "}"],
|
| 696 |
-
["prune", ";"],
|
| 697 |
-
["skip", ";"],
|
| 698 |
-
["reclaim", "<reclaim_value>"],
|
| 699 |
],
|
| 700 |
|
| 701 |
"<default_opt>": [
|
| 702 |
-
["soil", ":", "<local_declaration>", "<case_statements>"],
|
| 703 |
-
[EPSILON],
|
| 704 |
],
|
| 705 |
|
| 706 |
-
# ===== CONTROL FLOW STATEMENTS =====
|
| 707 |
"<control_stmt>": [
|
| 708 |
-
["prune", ";"],
|
| 709 |
-
["skip", ";"],
|
| 710 |
-
],
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
# Example: myFunc(x, y, z);
|
| 714 |
-
# ===== EXPRESSIONS =====
|
| 715 |
-
# Expressions follow operator precedence (lowest to highest):
|
| 716 |
-
# 1. Assignment (=, +=, -=, *=, /=, %=), right-associative
|
| 717 |
-
# 2. Logical OR (||)
|
| 718 |
-
# 3. Logical AND (&&)
|
| 719 |
-
# 4. Relational (>, <, >=, <=, ==, !=)
|
| 720 |
-
# 5. Arithmetic (+, -)
|
| 721 |
-
# 6. Term (*, /, %)
|
| 722 |
-
# 7. Power (**)
|
| 723 |
-
# 8. Factor (literals, variables, function calls, parentheses)
|
| 724 |
-
|
| 725 |
-
# Level 1: Assignment expression (lowest precedence).
|
| 726 |
-
# Semantic analysis verifies that the left side is assignable.
|
| 727 |
-
# Examples: a = 5, total += amount, a = b = 10
|
| 728 |
"<expression>": [
|
| 729 |
["<assignment_expression>"],
|
| 730 |
],
|
|
@@ -734,147 +522,118 @@ cfg = {
|
|
| 734 |
],
|
| 735 |
|
| 736 |
"<assignment_expression_next>": [
|
| 737 |
-
["<assign_op>", "<assignment_expression>"],
|
| 738 |
-
[EPSILON],
|
| 739 |
],
|
| 740 |
|
| 741 |
-
# Level 2: Logical OR
|
| 742 |
-
# Example: a || b || c
|
| 743 |
"<logic_or>": [
|
| 744 |
["<logic_and>", "<logic_or_next>"],
|
| 745 |
],
|
| 746 |
|
| 747 |
"<logic_or_next>": [
|
| 748 |
-
["||", "<logic_and>", "<logic_or_next>"],
|
| 749 |
-
[EPSILON],
|
| 750 |
],
|
| 751 |
|
| 752 |
-
# Level 2: Logical AND
|
| 753 |
-
# Example: a && b && c
|
| 754 |
"<logic_and>": [
|
| 755 |
["<relational>", "<logic_and_next>"],
|
| 756 |
],
|
| 757 |
|
| 758 |
"<logic_and_next>": [
|
| 759 |
-
["&&", "<relational>", "<logic_and_next>"],
|
| 760 |
-
[EPSILON],
|
| 761 |
],
|
| 762 |
|
| 763 |
-
# Level 3: Relational operators
|
| 764 |
-
# Example: a > b, x == y
|
| 765 |
"<relational>": [
|
| 766 |
-
["<arithmetic>", "<relational_next>"],
|
| 767 |
],
|
| 768 |
|
| 769 |
"<relational_next>": [
|
| 770 |
-
["<relational_op>", "<arithmetic>"],
|
| 771 |
-
[EPSILON],
|
| 772 |
],
|
| 773 |
|
| 774 |
"<relational_op>": [
|
| 775 |
-
[">"],
|
| 776 |
-
["<"],
|
| 777 |
-
[">="],
|
| 778 |
-
["<="],
|
| 779 |
-
["=="],
|
| 780 |
-
["!="],
|
| 781 |
],
|
| 782 |
|
| 783 |
-
# Level 4: Arithmetic (addition/subtraction)
|
| 784 |
-
# Example: a + b - c
|
| 785 |
"<arithmetic>": [
|
| 786 |
["<term>", "<arithmetic_next>"],
|
| 787 |
],
|
| 788 |
|
| 789 |
"<arithmetic_next>": [
|
| 790 |
-
["+", "<term>", "<arithmetic_next>"],
|
| 791 |
-
["-", "<term>", "<arithmetic_next>"],
|
| 792 |
-
["`", "<term>", "<arithmetic_next>"],
|
| 793 |
-
[EPSILON],
|
| 794 |
],
|
| 795 |
|
| 796 |
-
# String-expression type rules are enforced during semantic analysis:
|
| 797 |
-
# vine/leaf ` vine/leaf -> vine
|
| 798 |
-
# vine == vine and vine != vine -> branch
|
| 799 |
-
# vine cannot be used with <, >, <=, >=, &&, or ||
|
| 800 |
|
| 801 |
-
# Level 5: Term (multiplication/division/modulo)
|
| 802 |
-
# Example: a * b / c % d
|
| 803 |
"<term>": [
|
| 804 |
["<power>", "<term_next>"],
|
| 805 |
],
|
| 806 |
|
| 807 |
"<term_next>": [
|
| 808 |
-
["*", "<power>", "<term_next>"],
|
| 809 |
-
["/", "<power>", "<term_next>"],
|
| 810 |
-
["%", "<power>", "<term_next>"],
|
| 811 |
-
[EPSILON],
|
| 812 |
],
|
| 813 |
|
| 814 |
-
# Level 6: Power (right-associative exponentiation)
|
| 815 |
-
# Example: a ** b ** c
|
| 816 |
"<power>": [
|
| 817 |
["<factor>", "<power_next>"],
|
| 818 |
],
|
| 819 |
|
| 820 |
"<power_next>": [
|
| 821 |
-
["**", "<power>"],
|
| 822 |
-
[EPSILON],
|
| 823 |
],
|
| 824 |
|
| 825 |
-
# Level 7: Factor (highest precedence - literals, variables, etc.)
|
| 826 |
"<factor>": [
|
| 827 |
-
["(", "<paren_expr>"],
|
| 828 |
-
["<unary_op>", "<factor>"],
|
| 829 |
-
["id", "<factor_id_next>"],
|
| 830 |
-
["
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
["
|
| 835 |
-
["
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
|
|
|
| 841 |
"<paren_expr>": [
|
| 842 |
-
["<data_type>", ")", "<factor>"],
|
| 843 |
-
["<expression>", ")"],
|
| 844 |
],
|
| 845 |
|
| 846 |
-
# Unary operators
|
| 847 |
"<unary_op>": [
|
| 848 |
-
["~"],
|
| 849 |
-
["!"],
|
| 850 |
],
|
| 851 |
|
| 852 |
-
# For identifiers in expressions - could be variable, array, struct, or function
|
| 853 |
"<factor_id_next>": [
|
| 854 |
-
["<array_access>", "<post_array_access>"],
|
| 855 |
-
["<struct_access>"],
|
| 856 |
-
["(", "<arguments>", ")"],
|
| 857 |
-
[EPSILON],
|
| 858 |
],
|
| 859 |
}
|
| 860 |
|
| 861 |
|
| 862 |
-
# ===============================================================================
|
| 863 |
-
# COMPUTE FIRST, FOLLOW, AND PREDICT SETS
|
| 864 |
-
# ===============================================================================
|
| 865 |
-
# These sets are used for building a predictive (LL) parser
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
# Compute the FIRST, FOLLOW, and PREDICT sets
|
| 869 |
first_sets = compute_first(cfg)
|
| 870 |
follow_sets = compute_follow(cfg, first_sets)
|
| 871 |
predict_sets = compute_predict(cfg, first_sets, follow_sets)
|
| 872 |
|
| 873 |
|
| 874 |
-
# ===============================================================================
|
| 875 |
-
# OUTPUT: Display the computed sets (only when run directly)
|
| 876 |
-
# ===============================================================================
|
| 877 |
-
|
| 878 |
if __name__ == '__main__':
|
| 879 |
print("=" * 80)
|
| 880 |
print("FIRST SETS")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import sys
|
| 2 |
from collections import defaultdict
|
| 3 |
|
|
|
|
| 4 |
if sys.platform == 'win32':
|
| 5 |
try:
|
| 6 |
sys.stdout.reconfigure(encoding='utf-8')
|
| 7 |
except:
|
| 8 |
pass
|
| 9 |
|
|
|
|
| 10 |
EPSILON = "λ"
|
| 11 |
|
| 12 |
|
| 13 |
def compute_first(cfg):
|
| 14 |
+
first = defaultdict(set)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
epsilon = EPSILON
|
| 16 |
|
| 17 |
+
for lhs, productions in cfg.items():
|
| 18 |
+
for prod in productions:
|
|
|
|
| 19 |
if not prod:
|
| 20 |
continue
|
| 21 |
+
if prod[0] == epsilon:
|
| 22 |
first[lhs].add(epsilon)
|
| 23 |
+
elif prod[0] not in cfg:
|
| 24 |
first[lhs].add(prod[0])
|
| 25 |
|
|
|
|
| 26 |
changed = True
|
| 27 |
while changed:
|
| 28 |
changed = False
|
| 29 |
for lhs, productions in cfg.items():
|
| 30 |
for prod in productions:
|
| 31 |
+
before = len(first[lhs])
|
| 32 |
|
|
|
|
| 33 |
for symbol in prod:
|
| 34 |
+
if symbol in cfg:
|
|
|
|
| 35 |
first[lhs] |= (first[symbol] - {epsilon})
|
|
|
|
| 36 |
if epsilon not in first[symbol]:
|
| 37 |
break
|
| 38 |
+
else:
|
| 39 |
if symbol != epsilon:
|
| 40 |
first[lhs].add(symbol)
|
| 41 |
break
|
| 42 |
else:
|
|
|
|
| 43 |
first[lhs].add(epsilon)
|
| 44 |
|
|
|
|
| 45 |
if len(first[lhs]) > before:
|
| 46 |
changed = True
|
| 47 |
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
def compute_follow(cfg, first):
|
| 52 |
+
follow = defaultdict(set)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
epsilon = EPSILON
|
| 54 |
|
| 55 |
+
start_symbol = next(iter(cfg))
|
|
|
|
| 56 |
follow[start_symbol].add("EOF")
|
| 57 |
|
|
|
|
| 58 |
changed = True
|
| 59 |
while changed:
|
| 60 |
changed = False
|
| 61 |
for lhs, productions in cfg.items():
|
| 62 |
for prod in productions:
|
|
|
|
| 63 |
for i, symbol in enumerate(prod):
|
| 64 |
+
if symbol in cfg:
|
| 65 |
before = len(follow[symbol])
|
| 66 |
|
|
|
|
| 67 |
j = i + 1
|
| 68 |
while j < len(prod):
|
| 69 |
next_symbol = prod[j]
|
| 70 |
+
if next_symbol in cfg:
|
|
|
|
| 71 |
follow[symbol] |= (first[next_symbol] - {epsilon})
|
|
|
|
| 72 |
if epsilon not in first[next_symbol]:
|
| 73 |
break
|
| 74 |
+
else:
|
| 75 |
if next_symbol != epsilon:
|
| 76 |
follow[symbol].add(next_symbol)
|
| 77 |
break
|
| 78 |
j += 1
|
| 79 |
else:
|
|
|
|
|
|
|
| 80 |
follow[symbol] |= follow[lhs]
|
| 81 |
|
|
|
|
| 82 |
if len(follow[symbol]) > before:
|
| 83 |
changed = True
|
| 84 |
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
def compute_predict(cfg, first, follow):
|
| 89 |
+
predict = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
epsilon = EPSILON
|
| 91 |
|
| 92 |
for lhs, productions in cfg.items():
|
| 93 |
for prod in productions:
|
|
|
|
| 94 |
key = (lhs, tuple(prod))
|
| 95 |
predict[key] = set()
|
| 96 |
|
|
|
|
| 97 |
if not prod or (len(prod) == 1 and prod[0] == epsilon):
|
|
|
|
| 98 |
predict[key] = follow[lhs].copy()
|
| 99 |
continue
|
| 100 |
|
|
|
|
| 101 |
first_set = set()
|
| 102 |
for symbol in prod:
|
| 103 |
+
if symbol in cfg:
|
|
|
|
| 104 |
first_set |= (first[symbol] - {epsilon})
|
|
|
|
| 105 |
if epsilon not in first[symbol]:
|
| 106 |
break
|
| 107 |
+
else:
|
| 108 |
if symbol != epsilon:
|
| 109 |
first_set.add(symbol)
|
| 110 |
break
|
| 111 |
else:
|
|
|
|
| 112 |
first_set.add(epsilon)
|
| 113 |
|
|
|
|
| 114 |
predict[key] = first_set
|
|
|
|
| 115 |
if epsilon in first_set:
|
| 116 |
predict[key] |= follow[lhs]
|
| 117 |
|
| 118 |
return predict
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
cfg = {
|
|
|
|
|
|
|
| 122 |
"<program>": [
|
| 123 |
[
|
| 124 |
+
"<global_declaration>",
|
| 125 |
+
"<function_definition>",
|
| 126 |
+
"root",
|
| 127 |
"(",
|
| 128 |
")",
|
| 129 |
"{",
|
| 130 |
+
"<local_declaration>",
|
| 131 |
+
"<body_statement>",
|
| 132 |
+
"reclaim",
|
| 133 |
";",
|
| 134 |
"}",
|
| 135 |
]
|
| 136 |
],
|
| 137 |
|
|
|
|
|
|
|
| 138 |
"<global_declaration>": [
|
| 139 |
+
["bundle", "id", "<bundle_or_var>", "<global_declaration>"],
|
| 140 |
+
["<data_type>", "id", "<array_dec>", "<var_value>", ";", "<global_declaration>"],
|
| 141 |
+
["fertile", "<data_type>", "id", "=", "<init_val>", "<const_next>", ";", "<global_declaration>"],
|
| 142 |
+
[EPSILON],
|
| 143 |
],
|
| 144 |
|
|
|
|
| 145 |
"<bundle_or_var>": [
|
| 146 |
+
["{", "<bundle_members>", "}", ";"],
|
| 147 |
+
["<bundle_mem_dec>", ";"],
|
| 148 |
],
|
| 149 |
|
|
|
|
|
|
|
| 150 |
"<local_declaration>": [
|
| 151 |
+
["<var_dec>", ";", "<local_declaration>"],
|
| 152 |
+
["<const_dec>", ";", "<local_declaration>"],
|
| 153 |
+
[EPSILON],
|
| 154 |
],
|
| 155 |
|
|
|
|
|
|
|
| 156 |
"<data_type>": [
|
| 157 |
+
["seed"],
|
| 158 |
+
["tree"],
|
| 159 |
+
["leaf"],
|
| 160 |
+
["branch"],
|
| 161 |
+
["vine"],
|
| 162 |
],
|
| 163 |
|
|
|
|
|
|
|
|
|
|
| 164 |
"<const_dec>": [
|
| 165 |
["fertile", "<data_type>", "id", "=", "<init_val>", "<const_next>"],
|
| 166 |
],
|
| 167 |
|
| 168 |
"<const_next>": [
|
| 169 |
+
[",", "id", "=", "<init_val>", "<const_next>"],
|
| 170 |
+
[EPSILON],
|
| 171 |
],
|
| 172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
"<var_dec>": [
|
| 174 |
+
["<data_type>", "id", "<array_dec>", "<var_value>"],
|
| 175 |
+
["bundle", "id", "<bundle_mem_dec>"],
|
| 176 |
],
|
| 177 |
|
| 178 |
"<bundle_mem_dec>": [
|
| 179 |
+
["id", "<array_dec>"],
|
| 180 |
],
|
| 181 |
|
| 182 |
"<var_value>": [
|
| 183 |
+
["=", "<init_val>", "<var_value_next>"],
|
| 184 |
+
["<var_value_next>"],
|
| 185 |
],
|
| 186 |
|
| 187 |
"<var_value_next>": [
|
| 188 |
+
[",", "id", "<array_dec>", "<var_value>"],
|
| 189 |
[EPSILON],
|
| 190 |
],
|
| 191 |
|
|
|
|
| 192 |
"<init_val>": [
|
| 193 |
+
["<array_init_opt>"],
|
| 194 |
+
["water", "(", "<water_arg>", ")"],
|
| 195 |
+
["<expression>"],
|
| 196 |
],
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
"<array_dec>": [
|
| 199 |
+
["[", "<array_dim_opt>", "]", "<array_dec>"],
|
| 200 |
+
[EPSILON],
|
| 201 |
],
|
| 202 |
|
| 203 |
"<array_dim_opt>": [
|
| 204 |
+
["intlit"],
|
| 205 |
+
["dblit"],
|
| 206 |
+
[EPSILON],
|
| 207 |
],
|
| 208 |
|
|
|
|
|
|
|
|
|
|
| 209 |
"<array_init_opt>": [
|
| 210 |
+
["{", "<init_vals>", "}"],
|
| 211 |
+
[EPSILON],
|
| 212 |
],
|
| 213 |
|
| 214 |
"<init_vals>": [
|
| 215 |
+
["<init_val_item>", "<init_vals_next>"],
|
| 216 |
+
[EPSILON],
|
| 217 |
],
|
| 218 |
|
| 219 |
"<init_vals_next>": [
|
| 220 |
+
[",", "<init_val_item>", "<init_vals_next>"],
|
| 221 |
+
[EPSILON],
|
| 222 |
],
|
| 223 |
|
| 224 |
"<init_val_item>": [
|
| 225 |
+
["{", "<init_vals>", "}"],
|
| 226 |
+
["<expression>"],
|
| 227 |
],
|
| 228 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
"<bundle_members>": [
|
| 230 |
+
["<data_type>", "id", ";", "<bundle_members>"],
|
| 231 |
+
["id", "id", ";", "<bundle_members>"],
|
| 232 |
+
[EPSILON],
|
| 233 |
],
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
"<function_definition>": [
|
| 236 |
[
|
| 237 |
+
"pollinate",
|
| 238 |
+
"<return_type>",
|
| 239 |
+
"id",
|
| 240 |
"(",
|
| 241 |
+
"<parameters>",
|
| 242 |
")",
|
| 243 |
"{",
|
| 244 |
+
"<local_declaration>",
|
| 245 |
+
"<body_statement>",
|
| 246 |
+
"reclaim",
|
| 247 |
+
"<reclaim_value>",
|
| 248 |
"}",
|
| 249 |
+
"<function_definition>",
|
| 250 |
],
|
| 251 |
+
[EPSILON],
|
| 252 |
],
|
| 253 |
|
|
|
|
| 254 |
"<return_type>": [
|
| 255 |
+
["<data_type>"],
|
| 256 |
+
["empty"],
|
| 257 |
+
["id"],
|
| 258 |
],
|
| 259 |
|
|
|
|
|
|
|
|
|
|
| 260 |
"<parameters>": [
|
| 261 |
+
[EPSILON],
|
| 262 |
+
["<param>", "<param_next>"],
|
| 263 |
],
|
| 264 |
|
| 265 |
"<param>": [
|
| 266 |
+
["<data_type>", "id", "<param_array>"],
|
| 267 |
+
["id", "id"],
|
| 268 |
],
|
| 269 |
|
| 270 |
"<param_array>": [
|
| 271 |
+
[EPSILON],
|
| 272 |
+
["[", "]"],
|
| 273 |
],
|
| 274 |
|
| 275 |
"<param_next>": [
|
| 276 |
+
[EPSILON],
|
| 277 |
+
[",", "<param>", "<param_next>"],
|
| 278 |
],
|
| 279 |
|
|
|
|
|
|
|
|
|
|
| 280 |
"<reclaim_value>": [
|
| 281 |
+
["<expression>", ";"],
|
| 282 |
+
[";"],
|
| 283 |
],
|
| 284 |
|
|
|
|
|
|
|
|
|
|
| 285 |
"<body_statement>": [
|
| 286 |
["<non_reclaim_stmt>", "<body_statement>"],
|
| 287 |
[EPSILON],
|
| 288 |
],
|
| 289 |
|
| 290 |
"<non_reclaim_stmt>": [
|
| 291 |
+
["id", "<id_stmt>"],
|
| 292 |
+
["<inc_dec_op>", "id", "<id_next>", ";"],
|
| 293 |
+
["<io_stmt>"],
|
| 294 |
+
["<conditional_stmt>"],
|
| 295 |
+
["<loop_stmt>"],
|
| 296 |
+
["<switch_stmt>"],
|
| 297 |
+
["<control_stmt>"],
|
| 298 |
],
|
| 299 |
|
|
|
|
|
|
|
| 300 |
"<statement>": [
|
| 301 |
+
["<simple_stmt>", "<statement>"],
|
| 302 |
+
[EPSILON],
|
| 303 |
],
|
| 304 |
|
|
|
|
| 305 |
"<simple_stmt>": [
|
| 306 |
+
["<non_reclaim_stmt>"],
|
| 307 |
+
["reclaim", "<reclaim_value>"],
|
| 308 |
],
|
| 309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
"<id_stmt>": [
|
| 311 |
+
["<id_next>", "<id_stmt_tail>"],
|
| 312 |
+
["(", "<arguments>", ")", ";"],
|
| 313 |
],
|
| 314 |
|
|
|
|
|
|
|
|
|
|
| 315 |
"<id_stmt_tail>": [
|
| 316 |
+
["<assign_op>", "<assign_rhs>", ";"],
|
| 317 |
+
["<inc_dec_op>", ";"],
|
| 318 |
],
|
| 319 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
"<assign_rhs>": [
|
| 321 |
+
["water", "(", "<water_arg>", ")"],
|
| 322 |
+
["<expression>"],
|
| 323 |
],
|
| 324 |
|
|
|
|
| 325 |
"<assign_op>": [
|
| 326 |
+
["="],
|
| 327 |
+
["+="],
|
| 328 |
+
["-="],
|
| 329 |
+
["*="],
|
| 330 |
+
["/="],
|
| 331 |
+
["%="],
|
| 332 |
+
["**="],
|
| 333 |
],
|
| 334 |
|
| 335 |
"<id_next>": [
|
| 336 |
+
["<array_access>", "<post_array_access>"],
|
| 337 |
+
["<struct_access>"],
|
| 338 |
+
[EPSILON],
|
| 339 |
],
|
| 340 |
|
|
|
|
|
|
|
|
|
|
| 341 |
"<array_access>": [
|
| 342 |
["[", "<expression>", "]", "<array_access_more>"],
|
| 343 |
],
|
| 344 |
|
| 345 |
"<array_access_more>": [
|
| 346 |
+
["[", "<expression>", "]", "<array_access_more>"],
|
| 347 |
+
[EPSILON],
|
| 348 |
],
|
| 349 |
|
|
|
|
|
|
|
|
|
|
| 350 |
"<struct_access>": [
|
| 351 |
[".", "id", "<struct_access_more>"],
|
| 352 |
],
|
| 353 |
|
| 354 |
"<struct_access_more>": [
|
| 355 |
+
[".", "id", "<struct_access_more>"],
|
| 356 |
+
[EPSILON],
|
| 357 |
],
|
| 358 |
|
|
|
|
| 359 |
"<post_array_access>": [
|
| 360 |
+
[".", "id", "<post_array_access>"],
|
| 361 |
+
[EPSILON],
|
| 362 |
],
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
"<io_stmt>": [
|
| 365 |
+
["plant", "(", "<arguments>", ")", ";"],
|
| 366 |
+
["water", "(", "<water_arg>", ")", ";"],
|
| 367 |
],
|
| 368 |
|
|
|
|
| 369 |
"<water_arg>": [
|
| 370 |
+
["<data_type>"],
|
| 371 |
+
["id", "<water_id_tail>"],
|
| 372 |
+
[EPSILON],
|
| 373 |
],
|
| 374 |
|
| 375 |
"<water_id_tail>": [
|
| 376 |
+
["[", "<expression>", "]", "<water_id_tail>"],
|
| 377 |
+
[EPSILON],
|
| 378 |
],
|
| 379 |
|
|
|
|
| 380 |
"<arguments>": [
|
| 381 |
+
["<expression>", "<arg_next>"],
|
| 382 |
+
[EPSILON],
|
| 383 |
],
|
| 384 |
|
| 385 |
"<arg_next>": [
|
| 386 |
+
[",", "<expression>", "<arg_next>"],
|
| 387 |
+
[EPSILON],
|
| 388 |
+
],
|
| 389 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
"<conditional_stmt>": [
|
| 391 |
[
|
| 392 |
+
"spring",
|
| 393 |
"(",
|
| 394 |
+
"<expression>",
|
| 395 |
")",
|
| 396 |
"{",
|
| 397 |
+
"<local_declaration>",
|
| 398 |
+
"<statement>",
|
| 399 |
"}",
|
| 400 |
+
"<elseif_chain>",
|
| 401 |
+
"<else_opt>",
|
| 402 |
]
|
| 403 |
],
|
| 404 |
|
| 405 |
"<elseif_chain>": [
|
| 406 |
[
|
| 407 |
+
"bud",
|
| 408 |
"(",
|
| 409 |
+
"<expression>",
|
| 410 |
")",
|
| 411 |
"{",
|
| 412 |
+
"<local_declaration>",
|
| 413 |
+
"<statement>",
|
| 414 |
"}",
|
| 415 |
+
"<elseif_chain>",
|
| 416 |
],
|
| 417 |
+
[EPSILON],
|
| 418 |
],
|
| 419 |
|
| 420 |
"<else_opt>": [
|
| 421 |
+
["wither", "{", "<local_declaration>", "<statement>", "}"],
|
| 422 |
+
[EPSILON],
|
| 423 |
],
|
| 424 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
"<loop_stmt>": [
|
|
|
|
| 426 |
["grow", "(", "<expression>", ")", "{", "<local_declaration>", "<statement>", "}"],
|
| 427 |
|
|
|
|
| 428 |
[
|
| 429 |
"cultivate",
|
| 430 |
"(",
|
| 431 |
+
"<for_init>",
|
| 432 |
";",
|
| 433 |
+
"<expression>",
|
| 434 |
";",
|
| 435 |
+
"<for_update>",
|
| 436 |
")",
|
| 437 |
"{",
|
| 438 |
"<local_declaration>",
|
|
|
|
| 440 |
"}",
|
| 441 |
],
|
| 442 |
|
|
|
|
| 443 |
["tend", "{", "<local_declaration>", "<statement>", "}", "grow", "(", "<expression>", ")", ";"],
|
| 444 |
],
|
| 445 |
|
| 446 |
"<for_init>": [
|
| 447 |
+
["<data_type>", "id", "<array_dec>", "<var_value>"],
|
| 448 |
+
["id", "<id_next>", "<assign_op>", "<expression>"],
|
| 449 |
+
[EPSILON],
|
| 450 |
],
|
| 451 |
|
| 452 |
"<for_update>": [
|
|
|
|
|
|
|
|
|
|
| 453 |
["id", "<id_next>", "<for_update_tail>"],
|
| 454 |
+
[EPSILON],
|
| 455 |
],
|
| 456 |
|
| 457 |
"<for_update_tail>": [
|
| 458 |
+
["<inc_dec_op>"],
|
| 459 |
+
["<assign_op>", "<expression>"],
|
| 460 |
],
|
| 461 |
|
|
|
|
|
|
|
| 462 |
"<inc_dec_op>": [
|
| 463 |
+
["++"],
|
| 464 |
+
["--"],
|
| 465 |
+
],
|
| 466 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
"<switch_stmt>": [
|
| 468 |
["harvest", "(", "<expression>", ")", "{", "<case_list>", "<default_opt>", "}"],
|
| 469 |
],
|
| 470 |
|
| 471 |
"<case_list>": [
|
| 472 |
[
|
| 473 |
+
"variety",
|
| 474 |
+
"<case_literal>",
|
| 475 |
":",
|
| 476 |
+
"<local_declaration>",
|
| 477 |
+
"<case_statements>",
|
| 478 |
+
"<case_list>",
|
| 479 |
],
|
| 480 |
+
[EPSILON],
|
| 481 |
],
|
| 482 |
|
|
|
|
| 483 |
"<case_literal>": [
|
| 484 |
+
["<literal>"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
],
|
| 486 |
|
|
|
|
| 487 |
"<case_statements>": [
|
| 488 |
+
["<case_statement>", "<case_statements>"],
|
| 489 |
+
[EPSILON],
|
| 490 |
],
|
| 491 |
|
|
|
|
| 492 |
"<case_statement>": [
|
| 493 |
+
["id", "<id_stmt>"],
|
| 494 |
+
["<inc_dec_op>", "id", "<id_next>", ";"],
|
| 495 |
+
["<io_stmt>"],
|
| 496 |
+
["<conditional_stmt>"],
|
| 497 |
+
["<loop_stmt>"],
|
| 498 |
+
["<switch_stmt>"],
|
| 499 |
+
["{", "<local_declaration>", "<statement>", "}"],
|
| 500 |
+
["prune", ";"],
|
| 501 |
+
["skip", ";"],
|
| 502 |
+
["reclaim", "<reclaim_value>"],
|
| 503 |
],
|
| 504 |
|
| 505 |
"<default_opt>": [
|
| 506 |
+
["soil", ":", "<local_declaration>", "<case_statements>"],
|
| 507 |
+
[EPSILON],
|
| 508 |
],
|
| 509 |
|
|
|
|
| 510 |
"<control_stmt>": [
|
| 511 |
+
["prune", ";"],
|
| 512 |
+
["skip", ";"],
|
| 513 |
+
],
|
| 514 |
+
|
| 515 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
"<expression>": [
|
| 517 |
["<assignment_expression>"],
|
| 518 |
],
|
|
|
|
| 522 |
],
|
| 523 |
|
| 524 |
"<assignment_expression_next>": [
|
| 525 |
+
["<assign_op>", "<assignment_expression>"],
|
| 526 |
+
[EPSILON],
|
| 527 |
],
|
| 528 |
|
|
|
|
|
|
|
| 529 |
"<logic_or>": [
|
| 530 |
["<logic_and>", "<logic_or_next>"],
|
| 531 |
],
|
| 532 |
|
| 533 |
"<logic_or_next>": [
|
| 534 |
+
["||", "<logic_and>", "<logic_or_next>"],
|
| 535 |
+
[EPSILON],
|
| 536 |
],
|
| 537 |
|
|
|
|
|
|
|
| 538 |
"<logic_and>": [
|
| 539 |
["<relational>", "<logic_and_next>"],
|
| 540 |
],
|
| 541 |
|
| 542 |
"<logic_and_next>": [
|
| 543 |
+
["&&", "<relational>", "<logic_and_next>"],
|
| 544 |
+
[EPSILON],
|
| 545 |
],
|
| 546 |
|
|
|
|
|
|
|
| 547 |
"<relational>": [
|
| 548 |
+
["<arithmetic>", "<relational_next>"],
|
| 549 |
],
|
| 550 |
|
| 551 |
"<relational_next>": [
|
| 552 |
+
["<relational_op>", "<arithmetic>"],
|
| 553 |
+
[EPSILON],
|
| 554 |
],
|
| 555 |
|
| 556 |
"<relational_op>": [
|
| 557 |
+
[">"],
|
| 558 |
+
["<"],
|
| 559 |
+
[">="],
|
| 560 |
+
["<="],
|
| 561 |
+
["=="],
|
| 562 |
+
["!="],
|
| 563 |
],
|
| 564 |
|
|
|
|
|
|
|
| 565 |
"<arithmetic>": [
|
| 566 |
["<term>", "<arithmetic_next>"],
|
| 567 |
],
|
| 568 |
|
| 569 |
"<arithmetic_next>": [
|
| 570 |
+
["+", "<term>", "<arithmetic_next>"],
|
| 571 |
+
["-", "<term>", "<arithmetic_next>"],
|
| 572 |
+
["`", "<term>", "<arithmetic_next>"],
|
| 573 |
+
[EPSILON],
|
| 574 |
],
|
| 575 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
|
|
|
|
|
|
|
| 577 |
"<term>": [
|
| 578 |
["<power>", "<term_next>"],
|
| 579 |
],
|
| 580 |
|
| 581 |
"<term_next>": [
|
| 582 |
+
["*", "<power>", "<term_next>"],
|
| 583 |
+
["/", "<power>", "<term_next>"],
|
| 584 |
+
["%", "<power>", "<term_next>"],
|
| 585 |
+
[EPSILON],
|
| 586 |
],
|
| 587 |
|
|
|
|
|
|
|
| 588 |
"<power>": [
|
| 589 |
["<factor>", "<power_next>"],
|
| 590 |
],
|
| 591 |
|
| 592 |
"<power_next>": [
|
| 593 |
+
["**", "<power>"],
|
| 594 |
+
[EPSILON],
|
| 595 |
],
|
| 596 |
|
|
|
|
| 597 |
"<factor>": [
|
| 598 |
+
["(", "<paren_expr>"],
|
| 599 |
+
["<unary_op>", "<factor>"],
|
| 600 |
+
["id", "<factor_id_next>"],
|
| 601 |
+
["<literal>"],
|
| 602 |
+
],
|
| 603 |
+
|
| 604 |
+
"<literal>": [
|
| 605 |
+
["intlit"],
|
| 606 |
+
["dblit"],
|
| 607 |
+
["chrlit"],
|
| 608 |
+
["stringlit"],
|
| 609 |
+
["sunshine"],
|
| 610 |
+
["frost"],
|
| 611 |
+
],
|
| 612 |
+
|
| 613 |
"<paren_expr>": [
|
| 614 |
+
["<data_type>", ")", "<factor>"],
|
| 615 |
+
["<expression>", ")"],
|
| 616 |
],
|
| 617 |
|
|
|
|
| 618 |
"<unary_op>": [
|
| 619 |
+
["~"],
|
| 620 |
+
["!"],
|
| 621 |
],
|
| 622 |
|
|
|
|
| 623 |
"<factor_id_next>": [
|
| 624 |
+
["<array_access>", "<post_array_access>"],
|
| 625 |
+
["<struct_access>"],
|
| 626 |
+
["(", "<arguments>", ")"],
|
| 627 |
+
[EPSILON],
|
| 628 |
],
|
| 629 |
}
|
| 630 |
|
| 631 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
first_sets = compute_first(cfg)
|
| 633 |
follow_sets = compute_follow(cfg, first_sets)
|
| 634 |
predict_sets = compute_predict(cfg, first_sets, follow_sets)
|
| 635 |
|
| 636 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
if __name__ == '__main__':
|
| 638 |
print("=" * 80)
|
| 639 |
print("FIRST SETS")
|
Backend/icg/__init__.py
CHANGED
|
@@ -1,9 +1,2 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# ICG PACKAGE - Intermediate Code Generation (Three-Address Code, display-only)
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Re-exports generate_icg so external callers (server.py) can keep using:
|
| 5 |
-
# from icg import generate_icg
|
| 6 |
-
# unchanged after the restructure.
|
| 7 |
-
# ============================================================================
|
| 8 |
|
| 9 |
from .generator import generate_icg, ICGenerator, TACInstruction # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
from .generator import generate_icg, ICGenerator, TACInstruction # noqa: F401
|
Backend/icg/generator.py
CHANGED
|
@@ -1,14 +1,3 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# ICG GENERATOR - Three-Address Code (TAC) generator (display-only)
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from Backend/icg.py during the modular restructure.
|
| 5 |
-
# Pipeline position:
|
| 6 |
-
# lex -> parse -> AST -> semantic -> THIS FILE (display) -> interpret(AST)
|
| 7 |
-
# \________________/
|
| 8 |
-
# parallel pass; not on runtime path
|
| 9 |
-
# The output is a list of quad-like TAC instructions for IDE display.
|
| 10 |
-
# The interpreter walks the AST directly and does NOT consume this output.
|
| 11 |
-
# ============================================================================
|
| 12 |
from __future__ import annotations
|
| 13 |
from dataclasses import dataclass
|
| 14 |
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
@@ -23,7 +12,6 @@ class _Tok:
|
|
| 23 |
|
| 24 |
|
| 25 |
def _as_tok(raw: Any) -> _Tok:
|
| 26 |
-
"""Normalise dict / object tokens into a uniform view."""
|
| 27 |
if isinstance(raw, dict):
|
| 28 |
return _Tok(
|
| 29 |
type=str(raw.get("type", "")),
|
|
@@ -39,13 +27,8 @@ def _as_tok(raw: Any) -> _Tok:
|
|
| 39 |
)
|
| 40 |
|
| 41 |
|
| 42 |
-
# ---------------------------------------------------------------------------
|
| 43 |
-
# Three-Address Code (TAC) instruction
|
| 44 |
-
# ---------------------------------------------------------------------------
|
| 45 |
-
|
| 46 |
@dataclass
|
| 47 |
class TACInstruction:
|
| 48 |
-
"""One three-address code quad."""
|
| 49 |
op: str
|
| 50 |
arg1: Optional[str] = None
|
| 51 |
arg2: Optional[str] = None
|
|
@@ -92,7 +75,6 @@ class TACInstruction:
|
|
| 92 |
return f"{self.arg1} = {self.arg1} + 1"
|
| 93 |
if self.op == "DEC":
|
| 94 |
return f"{self.arg1} = {self.arg1} - 1"
|
| 95 |
-
# Binary / unary arithmetic & assignment
|
| 96 |
if self.arg2 is not None:
|
| 97 |
return f"{self.result} = {self.arg1} {self.op} {self.arg2}"
|
| 98 |
if self.op == "=":
|
|
@@ -113,9 +95,6 @@ class TACInstruction:
|
|
| 113 |
}
|
| 114 |
|
| 115 |
|
| 116 |
-
# ---------------------------------------------------------------------------
|
| 117 |
-
# GAL type map
|
| 118 |
-
# ---------------------------------------------------------------------------
|
| 119 |
GAL_TYPE_MAP = {
|
| 120 |
"seed": "int",
|
| 121 |
"tree": "float",
|
|
@@ -130,32 +109,24 @@ DATA_TYPE_TOKENS = set(GAL_TYPE_MAP.keys())
|
|
| 130 |
ASSIGN_OPS = {"=", "+=", "-=", "*=", "/=", "%="}
|
| 131 |
|
| 132 |
|
| 133 |
-
# ---------------------------------------------------------------------------
|
| 134 |
-
# Intermediate Code Generator
|
| 135 |
-
# ---------------------------------------------------------------------------
|
| 136 |
-
|
| 137 |
class ICGenerator:
|
| 138 |
-
"""Generates three-address code from a GAL token stream."""
|
| 139 |
|
| 140 |
def __init__(self, tokens: List[Any]):
|
| 141 |
self.tokens: List[_Tok] = self._prepare(tokens)
|
| 142 |
-
self.pos: int = 0
|
| 143 |
-
self.code: List[TACInstruction] = []
|
| 144 |
self.errors: List[str] = []
|
| 145 |
self._temp_counter: int = 0
|
| 146 |
self._label_counter: int = 0
|
| 147 |
|
| 148 |
-
# -- helpers ------------------------------------------------------------
|
| 149 |
|
| 150 |
def _prepare(self, raw_tokens: List[Any]) -> List[_Tok]:
|
| 151 |
-
"""Normalise and filter (skip newlines)."""
|
| 152 |
toks: List[_Tok] = []
|
| 153 |
for t in raw_tokens:
|
| 154 |
tv = _as_tok(t)
|
| 155 |
if tv.type == "\n":
|
| 156 |
continue
|
| 157 |
toks.append(tv)
|
| 158 |
-
# Ensure EOF
|
| 159 |
if not toks or toks[-1].type != "EOF":
|
| 160 |
last_line = toks[-1].line if toks else 1
|
| 161 |
toks.append(_Tok("EOF", "EOF", last_line))
|
|
@@ -178,12 +149,10 @@ class ICGenerator:
|
|
| 178 |
self.errors.append(
|
| 179 |
f"ICG Line {tok.line}: expected '{token_type}', got '{tok.type}'"
|
| 180 |
)
|
| 181 |
-
# try to continue anyway
|
| 182 |
return tok
|
| 183 |
return self._advance()
|
| 184 |
|
| 185 |
def _match(self, token_type: str) -> bool:
|
| 186 |
-
"""If current token matches, consume and return True."""
|
| 187 |
if self._peek().type == token_type:
|
| 188 |
self._advance()
|
| 189 |
return True
|
|
@@ -206,12 +175,8 @@ class ICGenerator:
|
|
| 206 |
def _is_data_type(self, tok: _Tok) -> bool:
|
| 207 |
return tok.type in DATA_TYPE_TOKENS
|
| 208 |
|
| 209 |
-
# ======================================================================
|
| 210 |
-
# TOP-LEVEL: <program>
|
| 211 |
-
# ======================================================================
|
| 212 |
|
| 213 |
def generate(self) -> Tuple[List[TACInstruction], List[str]]:
|
| 214 |
-
"""Entry point — generate TAC for the whole program."""
|
| 215 |
try:
|
| 216 |
self._program()
|
| 217 |
except Exception as exc:
|
|
@@ -219,11 +184,9 @@ class ICGenerator:
|
|
| 219 |
return self.code, self.errors
|
| 220 |
|
| 221 |
def _program(self):
|
| 222 |
-
"""<program> -> ... root ( ) { <local_declaration> <body_statement> reclaim ; }"""
|
| 223 |
self._global_declaration()
|
| 224 |
self._function_definition()
|
| 225 |
|
| 226 |
-
# root ( ) { ... }
|
| 227 |
self._expect("root")
|
| 228 |
self._expect("(")
|
| 229 |
self._expect(")")
|
|
@@ -240,33 +203,27 @@ class ICGenerator:
|
|
| 240 |
self._expect("}")
|
| 241 |
self._emit("ENDFUNC")
|
| 242 |
|
| 243 |
-
# ======================================================================
|
| 244 |
-
# GLOBAL DECLARATIONS
|
| 245 |
-
# ======================================================================
|
| 246 |
|
| 247 |
def _global_declaration(self):
|
| 248 |
-
"""<global_declaration> → bundle id ... | <data_type> id ... | fertile ... | λ"""
|
| 249 |
while True:
|
| 250 |
tok = self._peek()
|
| 251 |
if tok.type == "bundle":
|
| 252 |
-
self._advance()
|
| 253 |
name_tok = self._expect("id")
|
| 254 |
nxt = self._peek()
|
| 255 |
if nxt.type == "{":
|
| 256 |
-
|
| 257 |
-
self._advance() # {
|
| 258 |
self._bundle_members()
|
| 259 |
self._expect("}")
|
| 260 |
self._expect(";")
|
| 261 |
else:
|
| 262 |
-
# bundle variable: bundle Id varName ... ;
|
| 263 |
self._bundle_mem_dec()
|
| 264 |
self._expect(";")
|
| 265 |
self._global_declaration()
|
| 266 |
return
|
| 267 |
|
| 268 |
elif self._is_data_type(tok):
|
| 269 |
-
dtype = self._advance()
|
| 270 |
id_tok = self._expect("id")
|
| 271 |
arr_dims = self._array_dec()
|
| 272 |
if arr_dims:
|
|
@@ -288,15 +245,10 @@ class ICGenerator:
|
|
| 288 |
return
|
| 289 |
|
| 290 |
else:
|
| 291 |
-
# λ — no more global declarations
|
| 292 |
return
|
| 293 |
|
| 294 |
-
# ======================================================================
|
| 295 |
-
# LOCAL DECLARATIONS
|
| 296 |
-
# ======================================================================
|
| 297 |
|
| 298 |
def _declaration(self):
|
| 299 |
-
"""Consume the <local_declaration> prefix of a block."""
|
| 300 |
while True:
|
| 301 |
tok = self._peek()
|
| 302 |
if self._is_data_type(tok) or tok.type == "bundle":
|
|
@@ -309,7 +261,6 @@ class ICGenerator:
|
|
| 309 |
break
|
| 310 |
|
| 311 |
def _var_dec(self):
|
| 312 |
-
"""<var_dec> → <data_type> id <array_dec> <var_value> | bundle id <bundle_mem_dec>"""
|
| 313 |
tok = self._peek()
|
| 314 |
if tok.type == "bundle":
|
| 315 |
self._advance()
|
|
@@ -317,7 +268,7 @@ class ICGenerator:
|
|
| 317 |
self._bundle_mem_dec()
|
| 318 |
return
|
| 319 |
|
| 320 |
-
dtype = self._advance()
|
| 321 |
id_tok = self._expect("id")
|
| 322 |
arr_dims = self._array_dec()
|
| 323 |
if arr_dims:
|
|
@@ -329,9 +280,8 @@ class ICGenerator:
|
|
| 329 |
self._var_value(id_tok.value)
|
| 330 |
|
| 331 |
def _const_dec(self):
|
| 332 |
-
"""fertile <data_type> id = <init_val> <const_next>"""
|
| 333 |
self._expect("fertile")
|
| 334 |
-
dtype = self._advance()
|
| 335 |
id_tok = self._expect("id")
|
| 336 |
self._expect("=")
|
| 337 |
val = self._init_val()
|
|
@@ -339,7 +289,6 @@ class ICGenerator:
|
|
| 339 |
self._const_next(dtype)
|
| 340 |
|
| 341 |
def _const_next(self, dtype_tok: _Tok):
|
| 342 |
-
"""<const_next> → , id = <init_val> <const_next> | λ"""
|
| 343 |
while self._match(","):
|
| 344 |
id_tok = self._expect("id")
|
| 345 |
self._expect("=")
|
|
@@ -347,9 +296,8 @@ class ICGenerator:
|
|
| 347 |
self._emit("CONST", GAL_TYPE_MAP.get(dtype_tok.type, dtype_tok.type), val, id_tok.value)
|
| 348 |
|
| 349 |
def _var_value(self, var_name: str):
|
| 350 |
-
"""<var_value> → = <init_val> <var_value_next> | <var_value_next>"""
|
| 351 |
if self._peek().type == "=":
|
| 352 |
-
self._advance()
|
| 353 |
val = self._init_val()
|
| 354 |
self._emit("=", val, None, var_name)
|
| 355 |
self._var_value_next()
|
|
@@ -357,7 +305,6 @@ class ICGenerator:
|
|
| 357 |
self._var_value_next()
|
| 358 |
|
| 359 |
def _var_value_next(self):
|
| 360 |
-
"""<var_value_next> → , id <array_dec> <var_value> | λ"""
|
| 361 |
if self._match(","):
|
| 362 |
id_tok = self._expect("id")
|
| 363 |
arr_dims = self._array_dec()
|
|
@@ -369,13 +316,11 @@ class ICGenerator:
|
|
| 369 |
self._var_value(id_tok.value)
|
| 370 |
|
| 371 |
def _init_val(self) -> str:
|
| 372 |
-
"""<init_val> → <array_init_opt> | <expression>"""
|
| 373 |
if self._peek().type == "{":
|
| 374 |
return self._array_init()
|
| 375 |
return self._expression()
|
| 376 |
|
| 377 |
def _array_init(self) -> str:
|
| 378 |
-
"""{ <init_vals> } — emit element-by-element stores, return temp."""
|
| 379 |
self._expect("{")
|
| 380 |
tmp = self._new_temp()
|
| 381 |
idx = 0
|
|
@@ -395,31 +340,25 @@ class ICGenerator:
|
|
| 395 |
return self._array_init()
|
| 396 |
return self._expression()
|
| 397 |
|
| 398 |
-
# ======================================================================
|
| 399 |
-
# ARRAY & STRUCT HELPERS
|
| 400 |
-
# ======================================================================
|
| 401 |
|
| 402 |
def _array_dec(self) -> List[str]:
|
| 403 |
-
"""<array_dec> → [ <array_dim_opt> ] <array_dec> | λ — returns list of dimension sizes."""
|
| 404 |
dims: List[str] = []
|
| 405 |
while self._peek().type == "[":
|
| 406 |
-
self._advance()
|
| 407 |
if self._peek().type == "intlit":
|
| 408 |
dims.append(self._advance().value)
|
| 409 |
else:
|
| 410 |
-
dims.append("0")
|
| 411 |
self._expect("]")
|
| 412 |
return dims
|
| 413 |
|
| 414 |
def _bundle_members(self):
|
| 415 |
-
"""<bundle_members> → <data_type> id ; <bundle_members> | λ"""
|
| 416 |
while self._is_data_type(self._peek()):
|
| 417 |
dtype = self._advance()
|
| 418 |
id_tok = self._expect("id")
|
| 419 |
self._expect(";")
|
| 420 |
|
| 421 |
def _bundle_mem_dec(self):
|
| 422 |
-
"""<bundle_mem_dec> → id <array_dec>"""
|
| 423 |
id_tok = self._expect("id")
|
| 424 |
arr_dims = self._array_dec()
|
| 425 |
if arr_dims:
|
|
@@ -428,22 +367,16 @@ class ICGenerator:
|
|
| 428 |
else:
|
| 429 |
self._emit("DECLARE", "bundle", None, id_tok.value)
|
| 430 |
|
| 431 |
-
# ======================================================================
|
| 432 |
-
# FUNCTION DEFINITIONS
|
| 433 |
-
# ======================================================================
|
| 434 |
|
| 435 |
def _function_definition(self):
|
| 436 |
-
"""<function_definition> → pollinate <return_type> id ( <parameters> ) { ... } <function_definition> | λ"""
|
| 437 |
while self._peek().type == "pollinate":
|
| 438 |
-
self._advance()
|
| 439 |
|
| 440 |
-
# return type
|
| 441 |
if self._peek().type == "empty":
|
| 442 |
ret_type = self._advance().type
|
| 443 |
elif self._is_data_type(self._peek()):
|
| 444 |
ret_type = self._advance().type
|
| 445 |
elif self._peek().type == "id":
|
| 446 |
-
# User-defined bundle type as return type
|
| 447 |
ret_type = self._advance().value
|
| 448 |
else:
|
| 449 |
ret_type = "void"
|
|
@@ -451,7 +384,6 @@ class ICGenerator:
|
|
| 451 |
func_name = self._expect("id")
|
| 452 |
self._expect("(")
|
| 453 |
|
| 454 |
-
# parameters
|
| 455 |
params = self._parameters()
|
| 456 |
|
| 457 |
self._expect(")")
|
|
@@ -480,7 +412,6 @@ class ICGenerator:
|
|
| 480 |
self._emit("ENDFUNC")
|
| 481 |
|
| 482 |
def _parameters(self):
|
| 483 |
-
"""<parameters> → <param> <param_next> | λ"""
|
| 484 |
params = []
|
| 485 |
if self._is_data_type(self._peek()) or self._peek().type == "id":
|
| 486 |
p = self._param()
|
|
@@ -493,22 +424,16 @@ class ICGenerator:
|
|
| 493 |
def _param(self) -> Tuple[str, str]:
|
| 494 |
dtype = self._advance()
|
| 495 |
id_tok = self._expect("id")
|
| 496 |
-
# For bundle types, dtype.type is "id" — use dtype.value to get the actual type name
|
| 497 |
type_name = dtype.value if dtype.type == "id" else dtype.type
|
| 498 |
-
# Check for array parameter: seed arr[]
|
| 499 |
is_array = False
|
| 500 |
if self._peek().type == "[":
|
| 501 |
-
self._advance()
|
| 502 |
self._expect("]")
|
| 503 |
is_array = True
|
| 504 |
return (type_name, id_tok.value, is_array)
|
| 505 |
|
| 506 |
-
# ======================================================================
|
| 507 |
-
# STATEMENTS
|
| 508 |
-
# ======================================================================
|
| 509 |
|
| 510 |
def _statement(self, allow_reclaim: bool = False):
|
| 511 |
-
"""Consume executable statements; a function's final reclaim is consumed separately."""
|
| 512 |
stopping_tokens = {"}", "EOF", "variety", "soil", "prune"}
|
| 513 |
while self._peek().type not in stopping_tokens:
|
| 514 |
tok = self._peek()
|
|
@@ -517,7 +442,6 @@ class ICGenerator:
|
|
| 517 |
return
|
| 518 |
self._return_stmt()
|
| 519 |
continue
|
| 520 |
-
# Nested blocks expose their valid declaration prefix here.
|
| 521 |
if self._is_data_type(tok) or tok.type == "bundle" or tok.type == "fertile":
|
| 522 |
if tok.type == "fertile":
|
| 523 |
self._const_dec()
|
|
@@ -537,7 +461,6 @@ class ICGenerator:
|
|
| 537 |
self._expect(";")
|
| 538 |
|
| 539 |
def _simple_stmt(self):
|
| 540 |
-
"""Dispatch on the first token."""
|
| 541 |
tok = self._peek()
|
| 542 |
|
| 543 |
if tok.type == "id":
|
|
@@ -553,20 +476,16 @@ class ICGenerator:
|
|
| 553 |
elif tok.type in ("prune", "skip"):
|
| 554 |
self._control_stmt()
|
| 555 |
else:
|
| 556 |
-
# Skip unknown token to avoid infinite loop
|
| 557 |
self.errors.append(f"ICG Line {tok.line}: unexpected token '{tok.type}'")
|
| 558 |
self._advance()
|
| 559 |
|
| 560 |
-
# -- id statement -------------------------------------------------------
|
| 561 |
|
| 562 |
def _id_stmt(self):
|
| 563 |
-
|
| 564 |
-
id_tok = self._advance() # id
|
| 565 |
tok = self._peek()
|
| 566 |
|
| 567 |
-
# Function call
|
| 568 |
if tok.type == "(":
|
| 569 |
-
self._advance()
|
| 570 |
args = self._arguments()
|
| 571 |
self._expect(")")
|
| 572 |
self._expect(";")
|
|
@@ -576,18 +495,15 @@ class ICGenerator:
|
|
| 576 |
self._emit("CALL", id_tok.value, str(len(args)), tmp)
|
| 577 |
return
|
| 578 |
|
| 579 |
-
# Increment / decrement
|
| 580 |
if tok.type in ("++", "--"):
|
| 581 |
op_tok = self._advance()
|
| 582 |
self._expect(";")
|
| 583 |
self._emit("INC" if op_tok.type == "++" else "DEC", id_tok.value)
|
| 584 |
return
|
| 585 |
|
| 586 |
-
# Assignment (possibly with array / struct access on LHS)
|
| 587 |
lhs = id_tok.value
|
| 588 |
lhs = self._resolve_lhs(lhs)
|
| 589 |
|
| 590 |
-
# Assign op
|
| 591 |
if self._peek().type in ASSIGN_OPS:
|
| 592 |
op_tok = self._advance()
|
| 593 |
rhs = self._expression()
|
|
@@ -596,27 +512,22 @@ class ICGenerator:
|
|
| 596 |
if op_tok.type == "=":
|
| 597 |
self._emit("=", rhs, None, lhs)
|
| 598 |
else:
|
| 599 |
-
|
| 600 |
-
base_op = op_tok.type[0] # +, -, *, /, %
|
| 601 |
tmp = self._new_temp()
|
| 602 |
self._emit(base_op, lhs, rhs, tmp)
|
| 603 |
self._emit("=", tmp, None, lhs)
|
| 604 |
else:
|
| 605 |
-
# If it doesn't look like anything valid, skip to ;
|
| 606 |
self.errors.append(f"ICG Line {tok.line}: unexpected token after id '{id_tok.value}'")
|
| 607 |
while self._peek().type not in (";", "}", "EOF"):
|
| 608 |
self._advance()
|
| 609 |
self._match(";")
|
| 610 |
|
| 611 |
def _resolve_lhs(self, base: str) -> str:
|
| 612 |
-
"""Handle array access [expr] or struct access .id after an identifier for LHS."""
|
| 613 |
tok = self._peek()
|
| 614 |
if tok.type == "[":
|
| 615 |
-
# array access
|
| 616 |
self._advance()
|
| 617 |
idx = self._expression()
|
| 618 |
self._expect("]")
|
| 619 |
-
# possibly multi-dimensional
|
| 620 |
while self._peek().type == "[":
|
| 621 |
self._advance()
|
| 622 |
idx2 = self._expression()
|
|
@@ -628,7 +539,6 @@ class ICGenerator:
|
|
| 628 |
elif tok.type == ".":
|
| 629 |
self._advance()
|
| 630 |
member = self._expect("id")
|
| 631 |
-
# Possibly nested
|
| 632 |
chain = f"{base}.{member.value}"
|
| 633 |
while self._peek().type == ".":
|
| 634 |
self._advance()
|
|
@@ -637,11 +547,9 @@ class ICGenerator:
|
|
| 637 |
return chain
|
| 638 |
return base
|
| 639 |
|
| 640 |
-
# -- I/O ---------------------------------------------------------------
|
| 641 |
|
| 642 |
def _io_stmt(self):
|
| 643 |
-
|
| 644 |
-
io_tok = self._advance() # water / plant
|
| 645 |
self._expect("(")
|
| 646 |
args = self._arguments()
|
| 647 |
self._expect(")")
|
|
@@ -650,14 +558,12 @@ class ICGenerator:
|
|
| 650 |
if io_tok.type == "plant":
|
| 651 |
for a in args:
|
| 652 |
self._emit("PRINT", a)
|
| 653 |
-
else:
|
| 654 |
for a in args:
|
| 655 |
self._emit("READ", None, None, a)
|
| 656 |
|
| 657 |
-
# -- conditional --------------------------------------------------------
|
| 658 |
|
| 659 |
def _conditional_stmt(self):
|
| 660 |
-
"""spring ( <expr> ) { <stmt> } <elseif_chain> <else_opt>"""
|
| 661 |
self._expect("spring")
|
| 662 |
self._expect("(")
|
| 663 |
cond = self._expression()
|
|
@@ -675,10 +581,8 @@ class ICGenerator:
|
|
| 675 |
self._emit("GOTO", None, None, end_label)
|
| 676 |
self._emit("LABEL", None, None, false_label)
|
| 677 |
|
| 678 |
-
# elseif chain
|
| 679 |
self._elseif_chain(end_label)
|
| 680 |
|
| 681 |
-
# else opt
|
| 682 |
if self._peek().type == "wither":
|
| 683 |
self._advance()
|
| 684 |
self._expect("{")
|
|
@@ -688,9 +592,8 @@ class ICGenerator:
|
|
| 688 |
self._emit("LABEL", None, None, end_label)
|
| 689 |
|
| 690 |
def _elseif_chain(self, end_label: str):
|
| 691 |
-
"""bud ( <expr> ) { <stmt> } <elseif_chain>"""
|
| 692 |
while self._peek().type == "bud":
|
| 693 |
-
self._advance()
|
| 694 |
self._expect("(")
|
| 695 |
cond = self._expression()
|
| 696 |
self._expect(")")
|
|
@@ -705,7 +608,6 @@ class ICGenerator:
|
|
| 705 |
self._emit("GOTO", None, None, end_label)
|
| 706 |
self._emit("LABEL", None, None, next_label)
|
| 707 |
|
| 708 |
-
# -- loops --------------------------------------------------------------
|
| 709 |
|
| 710 |
def _loop_stmt(self):
|
| 711 |
tok = self._peek()
|
|
@@ -717,7 +619,6 @@ class ICGenerator:
|
|
| 717 |
self._do_while_loop()
|
| 718 |
|
| 719 |
def _while_loop(self):
|
| 720 |
-
"""grow ( <expr> ) { <stmt> }"""
|
| 721 |
self._expect("grow")
|
| 722 |
self._expect("(")
|
| 723 |
|
|
@@ -738,11 +639,9 @@ class ICGenerator:
|
|
| 738 |
self._emit("LABEL", None, None, end_label)
|
| 739 |
|
| 740 |
def _for_loop(self):
|
| 741 |
-
"""cultivate ( <for_init> ; <expr> ; <for_update> ) { <stmt> }"""
|
| 742 |
self._expect("cultivate")
|
| 743 |
self._expect("(")
|
| 744 |
|
| 745 |
-
# init
|
| 746 |
self._for_init()
|
| 747 |
self._expect(";")
|
| 748 |
|
|
@@ -752,13 +651,11 @@ class ICGenerator:
|
|
| 752 |
|
| 753 |
self._emit("LABEL", None, None, start_label)
|
| 754 |
|
| 755 |
-
# condition
|
| 756 |
cond = self._expression()
|
| 757 |
self._emit("IFFALSE", cond, None, end_label)
|
| 758 |
|
| 759 |
self._expect(";")
|
| 760 |
|
| 761 |
-
# save update position — we need to emit update *after* body
|
| 762 |
update_instrs: List[TACInstruction] = []
|
| 763 |
saved_code = self.code
|
| 764 |
self.code = update_instrs
|
|
@@ -770,14 +667,12 @@ class ICGenerator:
|
|
| 770 |
self._statement(allow_reclaim=True)
|
| 771 |
self._expect("}")
|
| 772 |
|
| 773 |
-
# emit update
|
| 774 |
self._emit("LABEL", None, None, update_label)
|
| 775 |
self.code.extend(update_instrs)
|
| 776 |
self._emit("GOTO", None, None, start_label)
|
| 777 |
self._emit("LABEL", None, None, end_label)
|
| 778 |
|
| 779 |
def _for_init(self):
|
| 780 |
-
"""<for_init> → <data_type> id <array_dec> <var_value> | id <id_next> <assign_op> <expression> | λ"""
|
| 781 |
tok = self._peek()
|
| 782 |
if self._is_data_type(tok):
|
| 783 |
self._var_dec()
|
|
@@ -794,10 +689,8 @@ class ICGenerator:
|
|
| 794 |
tmp = self._new_temp()
|
| 795 |
self._emit(base_op, lhs, rhs, tmp)
|
| 796 |
self._emit("=", tmp, None, lhs)
|
| 797 |
-
# else: λ
|
| 798 |
|
| 799 |
def _for_update(self):
|
| 800 |
-
"""<for_update> → id <for_update_type> | λ"""
|
| 801 |
if self._peek().type == "id":
|
| 802 |
id_tok = self._advance()
|
| 803 |
tok = self._peek()
|
|
@@ -818,7 +711,6 @@ class ICGenerator:
|
|
| 818 |
self._emit("=", tmp, None, lhs)
|
| 819 |
|
| 820 |
def _do_while_loop(self):
|
| 821 |
-
"""tend { <stmt> } grow ( <expr> ) ;"""
|
| 822 |
self._expect("tend")
|
| 823 |
self._expect("{")
|
| 824 |
|
|
@@ -836,10 +728,8 @@ class ICGenerator:
|
|
| 836 |
|
| 837 |
self._emit("IF", cond, None, start_label)
|
| 838 |
|
| 839 |
-
# -- switch -------------------------------------------------------------
|
| 840 |
|
| 841 |
def _switch_stmt(self):
|
| 842 |
-
"""harvest ( <expr> ) { <case_list> <default_opt> }"""
|
| 843 |
self._expect("harvest")
|
| 844 |
self._expect("(")
|
| 845 |
expr = self._expression()
|
|
@@ -854,9 +744,8 @@ class ICGenerator:
|
|
| 854 |
self._emit("LABEL", None, None, end_label)
|
| 855 |
|
| 856 |
def _case_list(self, switch_expr: str, end_label: str):
|
| 857 |
-
"""variety <expr> : <case_statements> prune ; <case_list> | λ"""
|
| 858 |
while self._peek().type == "variety":
|
| 859 |
-
self._advance()
|
| 860 |
case_val = self._expression()
|
| 861 |
self._expect(":")
|
| 862 |
|
|
@@ -868,11 +757,9 @@ class ICGenerator:
|
|
| 868 |
self._emit("IFFALSE", cmp_tmp, None, next_label)
|
| 869 |
self._emit("LABEL", None, None, body_label)
|
| 870 |
|
| 871 |
-
# case-local declarations, followed by executable case statements
|
| 872 |
self._declaration()
|
| 873 |
self._case_statements()
|
| 874 |
|
| 875 |
-
# prune ;
|
| 876 |
if self._peek().type == "prune":
|
| 877 |
self._advance()
|
| 878 |
self._expect(";")
|
|
@@ -881,7 +768,6 @@ class ICGenerator:
|
|
| 881 |
self._emit("LABEL", None, None, next_label)
|
| 882 |
|
| 883 |
def _case_statements(self):
|
| 884 |
-
"""<case_statements> → <case_statement> <case_statements> | λ"""
|
| 885 |
while self._peek().type not in ("variety", "soil", "}", "prune", "EOF"):
|
| 886 |
tok = self._peek()
|
| 887 |
if tok.type == "id":
|
|
@@ -893,36 +779,29 @@ class ICGenerator:
|
|
| 893 |
elif tok.type == "skip":
|
| 894 |
self._advance()
|
| 895 |
self._expect(";")
|
| 896 |
-
# skip = continue, emit as nop in switch context
|
| 897 |
elif tok.type == "reclaim":
|
| 898 |
self._return_stmt()
|
| 899 |
else:
|
| 900 |
break
|
| 901 |
|
| 902 |
def _default_opt(self, end_label: str):
|
| 903 |
-
"""soil : <local_declaration> <case_statements> | epsilon"""
|
| 904 |
if self._peek().type == "soil":
|
| 905 |
self._advance()
|
| 906 |
self._expect(":")
|
| 907 |
self._declaration()
|
| 908 |
self._case_statements()
|
| 909 |
|
| 910 |
-
# -- control (break, continue) ------------------------------------------
|
| 911 |
|
| 912 |
def _control_stmt(self):
|
| 913 |
-
tok = self._advance()
|
| 914 |
self._expect(";")
|
| 915 |
if tok.type == "prune":
|
| 916 |
-
self._emit("GOTO", None, None, "BREAK")
|
| 917 |
else:
|
| 918 |
self._emit("GOTO", None, None, "CONTINUE")
|
| 919 |
|
| 920 |
-
# ======================================================================
|
| 921 |
-
# EXPRESSIONS (precedence climbing, matching the CFG)
|
| 922 |
-
# ======================================================================
|
| 923 |
|
| 924 |
def _expression(self) -> str:
|
| 925 |
-
"""Parse assignment as the lowest-precedence, right-associative expression."""
|
| 926 |
left = self._logic_or()
|
| 927 |
if self._peek().type not in ASSIGN_OPS:
|
| 928 |
return left
|
|
@@ -938,7 +817,6 @@ class ICGenerator:
|
|
| 938 |
return left
|
| 939 |
|
| 940 |
def _logic_or(self) -> str:
|
| 941 |
-
"""<logic_or> → <logic_and> <logic_or_next>"""
|
| 942 |
left = self._logic_and()
|
| 943 |
while self._peek().type == "||":
|
| 944 |
self._advance()
|
|
@@ -949,7 +827,6 @@ class ICGenerator:
|
|
| 949 |
return left
|
| 950 |
|
| 951 |
def _logic_and(self) -> str:
|
| 952 |
-
"""<logic_and> → <relational> <logic_and_next>"""
|
| 953 |
left = self._relational()
|
| 954 |
while self._peek().type == "&&":
|
| 955 |
self._advance()
|
|
@@ -960,7 +837,6 @@ class ICGenerator:
|
|
| 960 |
return left
|
| 961 |
|
| 962 |
def _relational(self) -> str:
|
| 963 |
-
"""<relational> → <arithmetic> <relational_next>"""
|
| 964 |
left = self._arithmetic()
|
| 965 |
if self._peek().type in (">", "<", ">=", "<=", "==", "!="):
|
| 966 |
op = self._advance().type
|
|
@@ -971,7 +847,6 @@ class ICGenerator:
|
|
| 971 |
return left
|
| 972 |
|
| 973 |
def _arithmetic(self) -> str:
|
| 974 |
-
"""<arithmetic> → <term> <arithmetic_next>"""
|
| 975 |
left = self._term()
|
| 976 |
while self._peek().type in ("+", "-"):
|
| 977 |
op = self._advance().type
|
|
@@ -982,7 +857,6 @@ class ICGenerator:
|
|
| 982 |
return left
|
| 983 |
|
| 984 |
def _term(self) -> str:
|
| 985 |
-
"""<term> → <factor> <term_next>"""
|
| 986 |
left = self._power()
|
| 987 |
while self._peek().type in ("*", "/", "%"):
|
| 988 |
op = self._advance().type
|
|
@@ -1003,17 +877,14 @@ class ICGenerator:
|
|
| 1003 |
return left
|
| 1004 |
|
| 1005 |
def _factor(self) -> str:
|
| 1006 |
-
"""<factor> → ( <expr> ) | <unary_op> <factor> | id ... | literal"""
|
| 1007 |
tok = self._peek()
|
| 1008 |
|
| 1009 |
-
# ( expression )
|
| 1010 |
if tok.type == "(":
|
| 1011 |
self._advance()
|
| 1012 |
val = self._expression()
|
| 1013 |
self._expect(")")
|
| 1014 |
return val
|
| 1015 |
|
| 1016 |
-
# Unary: ~ or !
|
| 1017 |
if tok.type in ("~", "!"):
|
| 1018 |
op = self._advance()
|
| 1019 |
inner = self._factor()
|
|
@@ -1024,12 +895,10 @@ class ICGenerator:
|
|
| 1024 |
self._emit("NOT", inner, None, tmp)
|
| 1025 |
return tmp
|
| 1026 |
|
| 1027 |
-
# Identifier (variable, array, struct, or function call)
|
| 1028 |
if tok.type == "id":
|
| 1029 |
id_tok = self._advance()
|
| 1030 |
nxt = self._peek()
|
| 1031 |
|
| 1032 |
-
# Function call in expression
|
| 1033 |
if nxt.type == "(":
|
| 1034 |
self._advance()
|
| 1035 |
args = self._arguments()
|
|
@@ -1040,12 +909,10 @@ class ICGenerator:
|
|
| 1040 |
self._emit("CALL", id_tok.value, str(len(args)), tmp)
|
| 1041 |
return tmp
|
| 1042 |
|
| 1043 |
-
# Array access
|
| 1044 |
if nxt.type == "[":
|
| 1045 |
self._advance()
|
| 1046 |
idx = self._expression()
|
| 1047 |
self._expect("]")
|
| 1048 |
-
# multi-dim
|
| 1049 |
while self._peek().type == "[":
|
| 1050 |
self._advance()
|
| 1051 |
idx2 = self._expression()
|
|
@@ -1060,7 +927,6 @@ class ICGenerator:
|
|
| 1060 |
self._emit("ARRAY_LOAD", id_tok.value, idx, tmp)
|
| 1061 |
return tmp
|
| 1062 |
|
| 1063 |
-
# Struct access
|
| 1064 |
if nxt.type == ".":
|
| 1065 |
self._advance()
|
| 1066 |
member = self._expect("id")
|
|
@@ -1076,10 +942,8 @@ class ICGenerator:
|
|
| 1076 |
self._emit("STRUCT_LOAD", id_tok.value, chain, tmp)
|
| 1077 |
return tmp
|
| 1078 |
|
| 1079 |
-
# Simple variable
|
| 1080 |
return id_tok.value
|
| 1081 |
|
| 1082 |
-
# Literals
|
| 1083 |
if tok.type == "intlit":
|
| 1084 |
return self._advance().value
|
| 1085 |
if tok.type == "dbllit":
|
|
@@ -1095,15 +959,12 @@ class ICGenerator:
|
|
| 1095 |
self._advance()
|
| 1096 |
return "false"
|
| 1097 |
|
| 1098 |
-
# Fallback
|
| 1099 |
self.errors.append(f"ICG Line {tok.line}: unexpected token in expression: '{tok.type}'")
|
| 1100 |
self._advance()
|
| 1101 |
return "???"
|
| 1102 |
|
| 1103 |
-
# -- arguments ----------------------------------------------------------
|
| 1104 |
|
| 1105 |
def _arguments(self) -> List[str]:
|
| 1106 |
-
"""<arguments> → <expression> <arg_next> | λ"""
|
| 1107 |
args: List[str] = []
|
| 1108 |
if self._peek().type in (")", "EOF"):
|
| 1109 |
return args
|
|
@@ -1113,19 +974,7 @@ class ICGenerator:
|
|
| 1113 |
return args
|
| 1114 |
|
| 1115 |
|
| 1116 |
-
# ===========================================================================
|
| 1117 |
-
# Public API
|
| 1118 |
-
# ===========================================================================
|
| 1119 |
-
|
| 1120 |
def generate_icg(tokens: List[Any]) -> Dict[str, Any]:
|
| 1121 |
-
"""
|
| 1122 |
-
Main entry point for intermediate code generation.
|
| 1123 |
-
Returns a dict with:
|
| 1124 |
-
- 'success': bool
|
| 1125 |
-
- 'tac': list of TAC instruction dicts
|
| 1126 |
-
- 'tac_text': human-readable TAC string
|
| 1127 |
-
- 'errors': list of error strings
|
| 1128 |
-
"""
|
| 1129 |
gen = ICGenerator(tokens)
|
| 1130 |
code, errors = gen.generate()
|
| 1131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
from dataclasses import dataclass
|
| 3 |
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
def _as_tok(raw: Any) -> _Tok:
|
|
|
|
| 15 |
if isinstance(raw, dict):
|
| 16 |
return _Tok(
|
| 17 |
type=str(raw.get("type", "")),
|
|
|
|
| 27 |
)
|
| 28 |
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
@dataclass
|
| 31 |
class TACInstruction:
|
|
|
|
| 32 |
op: str
|
| 33 |
arg1: Optional[str] = None
|
| 34 |
arg2: Optional[str] = None
|
|
|
|
| 75 |
return f"{self.arg1} = {self.arg1} + 1"
|
| 76 |
if self.op == "DEC":
|
| 77 |
return f"{self.arg1} = {self.arg1} - 1"
|
|
|
|
| 78 |
if self.arg2 is not None:
|
| 79 |
return f"{self.result} = {self.arg1} {self.op} {self.arg2}"
|
| 80 |
if self.op == "=":
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
|
|
|
|
|
|
|
|
|
|
| 98 |
GAL_TYPE_MAP = {
|
| 99 |
"seed": "int",
|
| 100 |
"tree": "float",
|
|
|
|
| 109 |
ASSIGN_OPS = {"=", "+=", "-=", "*=", "/=", "%="}
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
class ICGenerator:
|
|
|
|
| 113 |
|
| 114 |
def __init__(self, tokens: List[Any]):
|
| 115 |
self.tokens: List[_Tok] = self._prepare(tokens)
|
| 116 |
+
self.pos: int = 0
|
| 117 |
+
self.code: List[TACInstruction] = []
|
| 118 |
self.errors: List[str] = []
|
| 119 |
self._temp_counter: int = 0
|
| 120 |
self._label_counter: int = 0
|
| 121 |
|
|
|
|
| 122 |
|
| 123 |
def _prepare(self, raw_tokens: List[Any]) -> List[_Tok]:
|
|
|
|
| 124 |
toks: List[_Tok] = []
|
| 125 |
for t in raw_tokens:
|
| 126 |
tv = _as_tok(t)
|
| 127 |
if tv.type == "\n":
|
| 128 |
continue
|
| 129 |
toks.append(tv)
|
|
|
|
| 130 |
if not toks or toks[-1].type != "EOF":
|
| 131 |
last_line = toks[-1].line if toks else 1
|
| 132 |
toks.append(_Tok("EOF", "EOF", last_line))
|
|
|
|
| 149 |
self.errors.append(
|
| 150 |
f"ICG Line {tok.line}: expected '{token_type}', got '{tok.type}'"
|
| 151 |
)
|
|
|
|
| 152 |
return tok
|
| 153 |
return self._advance()
|
| 154 |
|
| 155 |
def _match(self, token_type: str) -> bool:
|
|
|
|
| 156 |
if self._peek().type == token_type:
|
| 157 |
self._advance()
|
| 158 |
return True
|
|
|
|
| 175 |
def _is_data_type(self, tok: _Tok) -> bool:
|
| 176 |
return tok.type in DATA_TYPE_TOKENS
|
| 177 |
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
def generate(self) -> Tuple[List[TACInstruction], List[str]]:
|
|
|
|
| 180 |
try:
|
| 181 |
self._program()
|
| 182 |
except Exception as exc:
|
|
|
|
| 184 |
return self.code, self.errors
|
| 185 |
|
| 186 |
def _program(self):
|
|
|
|
| 187 |
self._global_declaration()
|
| 188 |
self._function_definition()
|
| 189 |
|
|
|
|
| 190 |
self._expect("root")
|
| 191 |
self._expect("(")
|
| 192 |
self._expect(")")
|
|
|
|
| 203 |
self._expect("}")
|
| 204 |
self._emit("ENDFUNC")
|
| 205 |
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
def _global_declaration(self):
|
|
|
|
| 208 |
while True:
|
| 209 |
tok = self._peek()
|
| 210 |
if tok.type == "bundle":
|
| 211 |
+
self._advance()
|
| 212 |
name_tok = self._expect("id")
|
| 213 |
nxt = self._peek()
|
| 214 |
if nxt.type == "{":
|
| 215 |
+
self._advance()
|
|
|
|
| 216 |
self._bundle_members()
|
| 217 |
self._expect("}")
|
| 218 |
self._expect(";")
|
| 219 |
else:
|
|
|
|
| 220 |
self._bundle_mem_dec()
|
| 221 |
self._expect(";")
|
| 222 |
self._global_declaration()
|
| 223 |
return
|
| 224 |
|
| 225 |
elif self._is_data_type(tok):
|
| 226 |
+
dtype = self._advance()
|
| 227 |
id_tok = self._expect("id")
|
| 228 |
arr_dims = self._array_dec()
|
| 229 |
if arr_dims:
|
|
|
|
| 245 |
return
|
| 246 |
|
| 247 |
else:
|
|
|
|
| 248 |
return
|
| 249 |
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
def _declaration(self):
|
|
|
|
| 252 |
while True:
|
| 253 |
tok = self._peek()
|
| 254 |
if self._is_data_type(tok) or tok.type == "bundle":
|
|
|
|
| 261 |
break
|
| 262 |
|
| 263 |
def _var_dec(self):
|
|
|
|
| 264 |
tok = self._peek()
|
| 265 |
if tok.type == "bundle":
|
| 266 |
self._advance()
|
|
|
|
| 268 |
self._bundle_mem_dec()
|
| 269 |
return
|
| 270 |
|
| 271 |
+
dtype = self._advance()
|
| 272 |
id_tok = self._expect("id")
|
| 273 |
arr_dims = self._array_dec()
|
| 274 |
if arr_dims:
|
|
|
|
| 280 |
self._var_value(id_tok.value)
|
| 281 |
|
| 282 |
def _const_dec(self):
|
|
|
|
| 283 |
self._expect("fertile")
|
| 284 |
+
dtype = self._advance()
|
| 285 |
id_tok = self._expect("id")
|
| 286 |
self._expect("=")
|
| 287 |
val = self._init_val()
|
|
|
|
| 289 |
self._const_next(dtype)
|
| 290 |
|
| 291 |
def _const_next(self, dtype_tok: _Tok):
|
|
|
|
| 292 |
while self._match(","):
|
| 293 |
id_tok = self._expect("id")
|
| 294 |
self._expect("=")
|
|
|
|
| 296 |
self._emit("CONST", GAL_TYPE_MAP.get(dtype_tok.type, dtype_tok.type), val, id_tok.value)
|
| 297 |
|
| 298 |
def _var_value(self, var_name: str):
|
|
|
|
| 299 |
if self._peek().type == "=":
|
| 300 |
+
self._advance()
|
| 301 |
val = self._init_val()
|
| 302 |
self._emit("=", val, None, var_name)
|
| 303 |
self._var_value_next()
|
|
|
|
| 305 |
self._var_value_next()
|
| 306 |
|
| 307 |
def _var_value_next(self):
|
|
|
|
| 308 |
if self._match(","):
|
| 309 |
id_tok = self._expect("id")
|
| 310 |
arr_dims = self._array_dec()
|
|
|
|
| 316 |
self._var_value(id_tok.value)
|
| 317 |
|
| 318 |
def _init_val(self) -> str:
|
|
|
|
| 319 |
if self._peek().type == "{":
|
| 320 |
return self._array_init()
|
| 321 |
return self._expression()
|
| 322 |
|
| 323 |
def _array_init(self) -> str:
|
|
|
|
| 324 |
self._expect("{")
|
| 325 |
tmp = self._new_temp()
|
| 326 |
idx = 0
|
|
|
|
| 340 |
return self._array_init()
|
| 341 |
return self._expression()
|
| 342 |
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
def _array_dec(self) -> List[str]:
|
|
|
|
| 345 |
dims: List[str] = []
|
| 346 |
while self._peek().type == "[":
|
| 347 |
+
self._advance()
|
| 348 |
if self._peek().type == "intlit":
|
| 349 |
dims.append(self._advance().value)
|
| 350 |
else:
|
| 351 |
+
dims.append("0")
|
| 352 |
self._expect("]")
|
| 353 |
return dims
|
| 354 |
|
| 355 |
def _bundle_members(self):
|
|
|
|
| 356 |
while self._is_data_type(self._peek()):
|
| 357 |
dtype = self._advance()
|
| 358 |
id_tok = self._expect("id")
|
| 359 |
self._expect(";")
|
| 360 |
|
| 361 |
def _bundle_mem_dec(self):
|
|
|
|
| 362 |
id_tok = self._expect("id")
|
| 363 |
arr_dims = self._array_dec()
|
| 364 |
if arr_dims:
|
|
|
|
| 367 |
else:
|
| 368 |
self._emit("DECLARE", "bundle", None, id_tok.value)
|
| 369 |
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
def _function_definition(self):
|
|
|
|
| 372 |
while self._peek().type == "pollinate":
|
| 373 |
+
self._advance()
|
| 374 |
|
|
|
|
| 375 |
if self._peek().type == "empty":
|
| 376 |
ret_type = self._advance().type
|
| 377 |
elif self._is_data_type(self._peek()):
|
| 378 |
ret_type = self._advance().type
|
| 379 |
elif self._peek().type == "id":
|
|
|
|
| 380 |
ret_type = self._advance().value
|
| 381 |
else:
|
| 382 |
ret_type = "void"
|
|
|
|
| 384 |
func_name = self._expect("id")
|
| 385 |
self._expect("(")
|
| 386 |
|
|
|
|
| 387 |
params = self._parameters()
|
| 388 |
|
| 389 |
self._expect(")")
|
|
|
|
| 412 |
self._emit("ENDFUNC")
|
| 413 |
|
| 414 |
def _parameters(self):
|
|
|
|
| 415 |
params = []
|
| 416 |
if self._is_data_type(self._peek()) or self._peek().type == "id":
|
| 417 |
p = self._param()
|
|
|
|
| 424 |
def _param(self) -> Tuple[str, str]:
|
| 425 |
dtype = self._advance()
|
| 426 |
id_tok = self._expect("id")
|
|
|
|
| 427 |
type_name = dtype.value if dtype.type == "id" else dtype.type
|
|
|
|
| 428 |
is_array = False
|
| 429 |
if self._peek().type == "[":
|
| 430 |
+
self._advance()
|
| 431 |
self._expect("]")
|
| 432 |
is_array = True
|
| 433 |
return (type_name, id_tok.value, is_array)
|
| 434 |
|
|
|
|
|
|
|
|
|
|
| 435 |
|
| 436 |
def _statement(self, allow_reclaim: bool = False):
|
|
|
|
| 437 |
stopping_tokens = {"}", "EOF", "variety", "soil", "prune"}
|
| 438 |
while self._peek().type not in stopping_tokens:
|
| 439 |
tok = self._peek()
|
|
|
|
| 442 |
return
|
| 443 |
self._return_stmt()
|
| 444 |
continue
|
|
|
|
| 445 |
if self._is_data_type(tok) or tok.type == "bundle" or tok.type == "fertile":
|
| 446 |
if tok.type == "fertile":
|
| 447 |
self._const_dec()
|
|
|
|
| 461 |
self._expect(";")
|
| 462 |
|
| 463 |
def _simple_stmt(self):
|
|
|
|
| 464 |
tok = self._peek()
|
| 465 |
|
| 466 |
if tok.type == "id":
|
|
|
|
| 476 |
elif tok.type in ("prune", "skip"):
|
| 477 |
self._control_stmt()
|
| 478 |
else:
|
|
|
|
| 479 |
self.errors.append(f"ICG Line {tok.line}: unexpected token '{tok.type}'")
|
| 480 |
self._advance()
|
| 481 |
|
|
|
|
| 482 |
|
| 483 |
def _id_stmt(self):
|
| 484 |
+
id_tok = self._advance()
|
|
|
|
| 485 |
tok = self._peek()
|
| 486 |
|
|
|
|
| 487 |
if tok.type == "(":
|
| 488 |
+
self._advance()
|
| 489 |
args = self._arguments()
|
| 490 |
self._expect(")")
|
| 491 |
self._expect(";")
|
|
|
|
| 495 |
self._emit("CALL", id_tok.value, str(len(args)), tmp)
|
| 496 |
return
|
| 497 |
|
|
|
|
| 498 |
if tok.type in ("++", "--"):
|
| 499 |
op_tok = self._advance()
|
| 500 |
self._expect(";")
|
| 501 |
self._emit("INC" if op_tok.type == "++" else "DEC", id_tok.value)
|
| 502 |
return
|
| 503 |
|
|
|
|
| 504 |
lhs = id_tok.value
|
| 505 |
lhs = self._resolve_lhs(lhs)
|
| 506 |
|
|
|
|
| 507 |
if self._peek().type in ASSIGN_OPS:
|
| 508 |
op_tok = self._advance()
|
| 509 |
rhs = self._expression()
|
|
|
|
| 512 |
if op_tok.type == "=":
|
| 513 |
self._emit("=", rhs, None, lhs)
|
| 514 |
else:
|
| 515 |
+
base_op = op_tok.type[0]
|
|
|
|
| 516 |
tmp = self._new_temp()
|
| 517 |
self._emit(base_op, lhs, rhs, tmp)
|
| 518 |
self._emit("=", tmp, None, lhs)
|
| 519 |
else:
|
|
|
|
| 520 |
self.errors.append(f"ICG Line {tok.line}: unexpected token after id '{id_tok.value}'")
|
| 521 |
while self._peek().type not in (";", "}", "EOF"):
|
| 522 |
self._advance()
|
| 523 |
self._match(";")
|
| 524 |
|
| 525 |
def _resolve_lhs(self, base: str) -> str:
|
|
|
|
| 526 |
tok = self._peek()
|
| 527 |
if tok.type == "[":
|
|
|
|
| 528 |
self._advance()
|
| 529 |
idx = self._expression()
|
| 530 |
self._expect("]")
|
|
|
|
| 531 |
while self._peek().type == "[":
|
| 532 |
self._advance()
|
| 533 |
idx2 = self._expression()
|
|
|
|
| 539 |
elif tok.type == ".":
|
| 540 |
self._advance()
|
| 541 |
member = self._expect("id")
|
|
|
|
| 542 |
chain = f"{base}.{member.value}"
|
| 543 |
while self._peek().type == ".":
|
| 544 |
self._advance()
|
|
|
|
| 547 |
return chain
|
| 548 |
return base
|
| 549 |
|
|
|
|
| 550 |
|
| 551 |
def _io_stmt(self):
|
| 552 |
+
io_tok = self._advance()
|
|
|
|
| 553 |
self._expect("(")
|
| 554 |
args = self._arguments()
|
| 555 |
self._expect(")")
|
|
|
|
| 558 |
if io_tok.type == "plant":
|
| 559 |
for a in args:
|
| 560 |
self._emit("PRINT", a)
|
| 561 |
+
else:
|
| 562 |
for a in args:
|
| 563 |
self._emit("READ", None, None, a)
|
| 564 |
|
|
|
|
| 565 |
|
| 566 |
def _conditional_stmt(self):
|
|
|
|
| 567 |
self._expect("spring")
|
| 568 |
self._expect("(")
|
| 569 |
cond = self._expression()
|
|
|
|
| 581 |
self._emit("GOTO", None, None, end_label)
|
| 582 |
self._emit("LABEL", None, None, false_label)
|
| 583 |
|
|
|
|
| 584 |
self._elseif_chain(end_label)
|
| 585 |
|
|
|
|
| 586 |
if self._peek().type == "wither":
|
| 587 |
self._advance()
|
| 588 |
self._expect("{")
|
|
|
|
| 592 |
self._emit("LABEL", None, None, end_label)
|
| 593 |
|
| 594 |
def _elseif_chain(self, end_label: str):
|
|
|
|
| 595 |
while self._peek().type == "bud":
|
| 596 |
+
self._advance()
|
| 597 |
self._expect("(")
|
| 598 |
cond = self._expression()
|
| 599 |
self._expect(")")
|
|
|
|
| 608 |
self._emit("GOTO", None, None, end_label)
|
| 609 |
self._emit("LABEL", None, None, next_label)
|
| 610 |
|
|
|
|
| 611 |
|
| 612 |
def _loop_stmt(self):
|
| 613 |
tok = self._peek()
|
|
|
|
| 619 |
self._do_while_loop()
|
| 620 |
|
| 621 |
def _while_loop(self):
|
|
|
|
| 622 |
self._expect("grow")
|
| 623 |
self._expect("(")
|
| 624 |
|
|
|
|
| 639 |
self._emit("LABEL", None, None, end_label)
|
| 640 |
|
| 641 |
def _for_loop(self):
|
|
|
|
| 642 |
self._expect("cultivate")
|
| 643 |
self._expect("(")
|
| 644 |
|
|
|
|
| 645 |
self._for_init()
|
| 646 |
self._expect(";")
|
| 647 |
|
|
|
|
| 651 |
|
| 652 |
self._emit("LABEL", None, None, start_label)
|
| 653 |
|
|
|
|
| 654 |
cond = self._expression()
|
| 655 |
self._emit("IFFALSE", cond, None, end_label)
|
| 656 |
|
| 657 |
self._expect(";")
|
| 658 |
|
|
|
|
| 659 |
update_instrs: List[TACInstruction] = []
|
| 660 |
saved_code = self.code
|
| 661 |
self.code = update_instrs
|
|
|
|
| 667 |
self._statement(allow_reclaim=True)
|
| 668 |
self._expect("}")
|
| 669 |
|
|
|
|
| 670 |
self._emit("LABEL", None, None, update_label)
|
| 671 |
self.code.extend(update_instrs)
|
| 672 |
self._emit("GOTO", None, None, start_label)
|
| 673 |
self._emit("LABEL", None, None, end_label)
|
| 674 |
|
| 675 |
def _for_init(self):
|
|
|
|
| 676 |
tok = self._peek()
|
| 677 |
if self._is_data_type(tok):
|
| 678 |
self._var_dec()
|
|
|
|
| 689 |
tmp = self._new_temp()
|
| 690 |
self._emit(base_op, lhs, rhs, tmp)
|
| 691 |
self._emit("=", tmp, None, lhs)
|
|
|
|
| 692 |
|
| 693 |
def _for_update(self):
|
|
|
|
| 694 |
if self._peek().type == "id":
|
| 695 |
id_tok = self._advance()
|
| 696 |
tok = self._peek()
|
|
|
|
| 711 |
self._emit("=", tmp, None, lhs)
|
| 712 |
|
| 713 |
def _do_while_loop(self):
|
|
|
|
| 714 |
self._expect("tend")
|
| 715 |
self._expect("{")
|
| 716 |
|
|
|
|
| 728 |
|
| 729 |
self._emit("IF", cond, None, start_label)
|
| 730 |
|
|
|
|
| 731 |
|
| 732 |
def _switch_stmt(self):
|
|
|
|
| 733 |
self._expect("harvest")
|
| 734 |
self._expect("(")
|
| 735 |
expr = self._expression()
|
|
|
|
| 744 |
self._emit("LABEL", None, None, end_label)
|
| 745 |
|
| 746 |
def _case_list(self, switch_expr: str, end_label: str):
|
|
|
|
| 747 |
while self._peek().type == "variety":
|
| 748 |
+
self._advance()
|
| 749 |
case_val = self._expression()
|
| 750 |
self._expect(":")
|
| 751 |
|
|
|
|
| 757 |
self._emit("IFFALSE", cmp_tmp, None, next_label)
|
| 758 |
self._emit("LABEL", None, None, body_label)
|
| 759 |
|
|
|
|
| 760 |
self._declaration()
|
| 761 |
self._case_statements()
|
| 762 |
|
|
|
|
| 763 |
if self._peek().type == "prune":
|
| 764 |
self._advance()
|
| 765 |
self._expect(";")
|
|
|
|
| 768 |
self._emit("LABEL", None, None, next_label)
|
| 769 |
|
| 770 |
def _case_statements(self):
|
|
|
|
| 771 |
while self._peek().type not in ("variety", "soil", "}", "prune", "EOF"):
|
| 772 |
tok = self._peek()
|
| 773 |
if tok.type == "id":
|
|
|
|
| 779 |
elif tok.type == "skip":
|
| 780 |
self._advance()
|
| 781 |
self._expect(";")
|
|
|
|
| 782 |
elif tok.type == "reclaim":
|
| 783 |
self._return_stmt()
|
| 784 |
else:
|
| 785 |
break
|
| 786 |
|
| 787 |
def _default_opt(self, end_label: str):
|
|
|
|
| 788 |
if self._peek().type == "soil":
|
| 789 |
self._advance()
|
| 790 |
self._expect(":")
|
| 791 |
self._declaration()
|
| 792 |
self._case_statements()
|
| 793 |
|
|
|
|
| 794 |
|
| 795 |
def _control_stmt(self):
|
| 796 |
+
tok = self._advance()
|
| 797 |
self._expect(";")
|
| 798 |
if tok.type == "prune":
|
| 799 |
+
self._emit("GOTO", None, None, "BREAK")
|
| 800 |
else:
|
| 801 |
self._emit("GOTO", None, None, "CONTINUE")
|
| 802 |
|
|
|
|
|
|
|
|
|
|
| 803 |
|
| 804 |
def _expression(self) -> str:
|
|
|
|
| 805 |
left = self._logic_or()
|
| 806 |
if self._peek().type not in ASSIGN_OPS:
|
| 807 |
return left
|
|
|
|
| 817 |
return left
|
| 818 |
|
| 819 |
def _logic_or(self) -> str:
|
|
|
|
| 820 |
left = self._logic_and()
|
| 821 |
while self._peek().type == "||":
|
| 822 |
self._advance()
|
|
|
|
| 827 |
return left
|
| 828 |
|
| 829 |
def _logic_and(self) -> str:
|
|
|
|
| 830 |
left = self._relational()
|
| 831 |
while self._peek().type == "&&":
|
| 832 |
self._advance()
|
|
|
|
| 837 |
return left
|
| 838 |
|
| 839 |
def _relational(self) -> str:
|
|
|
|
| 840 |
left = self._arithmetic()
|
| 841 |
if self._peek().type in (">", "<", ">=", "<=", "==", "!="):
|
| 842 |
op = self._advance().type
|
|
|
|
| 847 |
return left
|
| 848 |
|
| 849 |
def _arithmetic(self) -> str:
|
|
|
|
| 850 |
left = self._term()
|
| 851 |
while self._peek().type in ("+", "-"):
|
| 852 |
op = self._advance().type
|
|
|
|
| 857 |
return left
|
| 858 |
|
| 859 |
def _term(self) -> str:
|
|
|
|
| 860 |
left = self._power()
|
| 861 |
while self._peek().type in ("*", "/", "%"):
|
| 862 |
op = self._advance().type
|
|
|
|
| 877 |
return left
|
| 878 |
|
| 879 |
def _factor(self) -> str:
|
|
|
|
| 880 |
tok = self._peek()
|
| 881 |
|
|
|
|
| 882 |
if tok.type == "(":
|
| 883 |
self._advance()
|
| 884 |
val = self._expression()
|
| 885 |
self._expect(")")
|
| 886 |
return val
|
| 887 |
|
|
|
|
| 888 |
if tok.type in ("~", "!"):
|
| 889 |
op = self._advance()
|
| 890 |
inner = self._factor()
|
|
|
|
| 895 |
self._emit("NOT", inner, None, tmp)
|
| 896 |
return tmp
|
| 897 |
|
|
|
|
| 898 |
if tok.type == "id":
|
| 899 |
id_tok = self._advance()
|
| 900 |
nxt = self._peek()
|
| 901 |
|
|
|
|
| 902 |
if nxt.type == "(":
|
| 903 |
self._advance()
|
| 904 |
args = self._arguments()
|
|
|
|
| 909 |
self._emit("CALL", id_tok.value, str(len(args)), tmp)
|
| 910 |
return tmp
|
| 911 |
|
|
|
|
| 912 |
if nxt.type == "[":
|
| 913 |
self._advance()
|
| 914 |
idx = self._expression()
|
| 915 |
self._expect("]")
|
|
|
|
| 916 |
while self._peek().type == "[":
|
| 917 |
self._advance()
|
| 918 |
idx2 = self._expression()
|
|
|
|
| 927 |
self._emit("ARRAY_LOAD", id_tok.value, idx, tmp)
|
| 928 |
return tmp
|
| 929 |
|
|
|
|
| 930 |
if nxt.type == ".":
|
| 931 |
self._advance()
|
| 932 |
member = self._expect("id")
|
|
|
|
| 942 |
self._emit("STRUCT_LOAD", id_tok.value, chain, tmp)
|
| 943 |
return tmp
|
| 944 |
|
|
|
|
| 945 |
return id_tok.value
|
| 946 |
|
|
|
|
| 947 |
if tok.type == "intlit":
|
| 948 |
return self._advance().value
|
| 949 |
if tok.type == "dbllit":
|
|
|
|
| 959 |
self._advance()
|
| 960 |
return "false"
|
| 961 |
|
|
|
|
| 962 |
self.errors.append(f"ICG Line {tok.line}: unexpected token in expression: '{tok.type}'")
|
| 963 |
self._advance()
|
| 964 |
return "???"
|
| 965 |
|
|
|
|
| 966 |
|
| 967 |
def _arguments(self) -> List[str]:
|
|
|
|
| 968 |
args: List[str] = []
|
| 969 |
if self._peek().type in (")", "EOF"):
|
| 970 |
return args
|
|
|
|
| 974 |
return args
|
| 975 |
|
| 976 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 977 |
def generate_icg(tokens: List[Any]) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
gen = ICGenerator(tokens)
|
| 979 |
code, errors = gen.generate()
|
| 980 |
|
Backend/interpreter/__init__.py
CHANGED
|
@@ -1,11 +1,3 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# INTERPRETER PACKAGE - Public API for the runtime execution phase
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Re-exports the Interpreter class and the runtime exception types so
|
| 5 |
-
# external callers (server.py) can keep using:
|
| 6 |
-
# from interpreter import Interpreter, InterpreterError, _CancelledError
|
| 7 |
-
# unchanged after the restructure.
|
| 8 |
-
# ============================================================================
|
| 9 |
|
| 10 |
from .interpreter import Interpreter # noqa: F401
|
| 11 |
from .errors import ( # noqa: F401 - convenience re-exports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
from .interpreter import Interpreter # noqa: F401
|
| 3 |
from .errors import ( # noqa: F401 - convenience re-exports
|
Backend/interpreter/errors.py
CHANGED
|
@@ -1,28 +1,16 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# RUNTIME / INTERPRETER ERRORS - Raised by the tree-walking interpreter
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Lives with the interpreter because these are pure runtime concerns:
|
| 5 |
-
# - ReturnValue : control-flow unwind for `reclaim <expr>;`
|
| 6 |
-
# - _CancelledError : raised when the user starts a new run mid-execution
|
| 7 |
-
# - InterpreterError : standard runtime error (division by zero, etc.)
|
| 8 |
-
# - InterpreterInputRequest : raised when water() needs input from the client
|
| 9 |
-
# ============================================================================
|
| 10 |
|
| 11 |
|
| 12 |
class ReturnValue(Exception):
|
| 13 |
-
"""Raised by 'reclaim <expr>;' to unwind out of a function with a value."""
|
| 14 |
|
| 15 |
def __init__(self, value):
|
| 16 |
self.value = value
|
| 17 |
|
| 18 |
|
| 19 |
class _CancelledError(Exception):
|
| 20 |
-
"""Raised when an interpreter is cancelled mid-execution."""
|
| 21 |
pass
|
| 22 |
|
| 23 |
|
| 24 |
class InterpreterError(Exception):
|
| 25 |
-
"""All runtime errors raised by the interpreter inherit from this."""
|
| 26 |
|
| 27 |
def __init__(self, message, line):
|
| 28 |
super().__init__(message)
|
|
@@ -36,7 +24,6 @@ class InterpreterError(Exception):
|
|
| 36 |
|
| 37 |
|
| 38 |
class InterpreterInputRequest(Exception):
|
| 39 |
-
"""Carries a prompt up the call stack when water() needs input."""
|
| 40 |
|
| 41 |
def __init__(self, prompt, line):
|
| 42 |
self.prompt = prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
|
| 3 |
class ReturnValue(Exception):
|
|
|
|
| 4 |
|
| 5 |
def __init__(self, value):
|
| 6 |
self.value = value
|
| 7 |
|
| 8 |
|
| 9 |
class _CancelledError(Exception):
|
|
|
|
| 10 |
pass
|
| 11 |
|
| 12 |
|
| 13 |
class InterpreterError(Exception):
|
|
|
|
| 14 |
|
| 15 |
def __init__(self, message, line):
|
| 16 |
super().__init__(message)
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
class InterpreterInputRequest(Exception):
|
|
|
|
| 27 |
|
| 28 |
def __init__(self, prompt, line):
|
| 29 |
self.prompt = prompt
|
Backend/interpreter/interpreter.py
CHANGED
|
@@ -1,44 +1,12 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
# Extracted from Backend/GALinterpreter.py during the modular restructure.
|
| 5 |
-
# Pipeline position:
|
| 6 |
-
# lex -> parse -> AST -> semantic -> ICG (display) -> THIS FILE (execute)
|
| 7 |
-
# ============================================================================
|
| 8 |
-
|
| 9 |
-
# ============================================================================
|
| 10 |
-
# GAL INTERPRETER - Tree-walking executor for the semantic AST
|
| 11 |
-
# ============================================================================
|
| 12 |
-
# This is the runtime engine of the GAL compiler. It walks the AST produced
|
| 13 |
-
# by GALsemantic and executes each node directly. The interpreter does NOT
|
| 14 |
-
# consume the ICG output — TAC is a parallel display layer; execution happens
|
| 15 |
-
# here on the AST.
|
| 16 |
-
#
|
| 17 |
-
# Pipeline position:
|
| 18 |
-
# lex -> parse -> AST -> semantic -> ICG (display) -> THIS FILE (execute)
|
| 19 |
-
#
|
| 20 |
-
# Output is sent via self.socketio.emit('output', ...) which can be either
|
| 21 |
-
# a real Socket.IO instance (live mode) or an OutputCollector (synchronous).
|
| 22 |
-
# Input is collected via self.socketio.emit('input_required', ...) which
|
| 23 |
-
# parks the interpreter until provide_input() is called.
|
| 24 |
-
# ============================================================================
|
| 25 |
-
|
| 26 |
-
# ============================================================================
|
| 27 |
-
# IMPORTS - AST node classes from the semantic analyzer + concurrency
|
| 28 |
-
# primitives for cooperative input-waiting.
|
| 29 |
-
# ============================================================================
|
| 30 |
-
from shared.ast_nodes import * # AST node classes
|
| 31 |
|
| 32 |
import threading
|
| 33 |
import sys
|
| 34 |
|
| 35 |
-
# Allow deeply recursive GAL programs (e.g. recursive pollinate functions)
|
| 36 |
-
# without hitting Python's default recursion limit (1000)
|
| 37 |
sys.setrecursionlimit(10000)
|
| 38 |
|
| 39 |
-
# Prefer eventlet's cooperative Event so wait_for_input yields to the
|
| 40 |
-
# eventlet hub instead of blocking the entire event loop.
|
| 41 |
-
# Falls back to threading.Event if eventlet isn't installed (e.g. in tests).
|
| 42 |
try:
|
| 43 |
import eventlet.event as _ev
|
| 44 |
_USE_EVENTLET = True
|
|
@@ -46,11 +14,6 @@ except ImportError:
|
|
| 46 |
_USE_EVENTLET = False
|
| 47 |
|
| 48 |
|
| 49 |
-
# ============================================================================
|
| 50 |
-
# EXCEPTION CLASSES - All extracted to errors.py during the restructure.
|
| 51 |
-
# Re-exported here so existing `from GALinterpreter import InterpreterError, ...`
|
| 52 |
-
# imports keep working unchanged.
|
| 53 |
-
# ============================================================================
|
| 54 |
from semantic.errors import SemanticError # noqa: F401 - some runtime checks raise it
|
| 55 |
from interpreter.errors import ( # noqa: F401 - runtime-specific error classes
|
| 56 |
ReturnValue,
|
|
@@ -59,57 +22,31 @@ from interpreter.errors import ( # noqa: F401 - runtime-specific error classes
|
|
| 59 |
InterpreterInputRequest,
|
| 60 |
)
|
| 61 |
|
| 62 |
-
# ============================================================================
|
| 63 |
-
# INTERPRETER CLASS - Tree-walks the AST and executes each node
|
| 64 |
-
# ============================================================================
|
| 65 |
-
# State organization:
|
| 66 |
-
# - self.scopes : stack of scope dicts (innermost on top); enter_scope
|
| 67 |
-
# pushes a fresh dict, exit_scope pops it
|
| 68 |
-
# - self.variables : globals (visible from every scope)
|
| 69 |
-
# - self.functions : pollinate definitions, keyed by function name
|
| 70 |
-
# - self.bundle_types: bundle (struct) layouts
|
| 71 |
-
# - self.loop_stack : current loop nesting, used by prune/skip
|
| 72 |
-
# - self.break_flag : 'prune' was hit, propagate up to nearest loop/switch
|
| 73 |
-
# - self.continue_flag: 'skip' was hit, propagate up to nearest loop
|
| 74 |
-
# - self.input_* : channels for water() input arriving from the client
|
| 75 |
-
# - self.socketio : adapter for output/input I/O (SessionEmitter or
|
| 76 |
-
# OutputCollector — both expose .emit())
|
| 77 |
-
# ============================================================================
|
| 78 |
class Interpreter:
|
| 79 |
def __init__(self, socketio=None):
|
| 80 |
-
|
| 81 |
-
self.
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
self.
|
| 86 |
-
|
| 87 |
-
self.
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
self.
|
| 92 |
-
self.input_values = {} # var_name -> string typed by client
|
| 93 |
-
|
| 94 |
-
# ---- Diagnostics ----
|
| 95 |
-
self.current_node = None # node being interpreted (for error msgs)
|
| 96 |
self.current_parent = None
|
| 97 |
|
| 98 |
-
|
| 99 |
-
self.
|
| 100 |
-
self.
|
| 101 |
-
self.
|
| 102 |
-
self.
|
| 103 |
-
self.
|
| 104 |
-
self.
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
# ========================================================================
|
| 109 |
-
# VARIABLE MANAGEMENT - declare / lookup / set
|
| 110 |
-
# Lookup walks the scope stack from innermost outward, then falls back to
|
| 111 |
-
# globals. This implements lexical scoping for the AST walker.
|
| 112 |
-
# ========================================================================
|
| 113 |
def declare_variable(self, name, type_, value=None, is_list=False, is_fertile=False):
|
| 114 |
scope = self.scopes[-1]
|
| 115 |
current_func = self.current_func_name
|
|
@@ -135,7 +72,6 @@ class Interpreter:
|
|
| 135 |
self.global_variables[name] = self.variables[name]
|
| 136 |
|
| 137 |
|
| 138 |
-
|
| 139 |
def lookup_variable(self, name):
|
| 140 |
for i, scope in enumerate(reversed(self.scopes)):
|
| 141 |
if name in scope:
|
|
@@ -156,14 +92,8 @@ class Interpreter:
|
|
| 156 |
return f"Semantic Error: Variable '{name}' not declared in any scope."
|
| 157 |
|
| 158 |
|
| 159 |
-
# ========================================================================
|
| 160 |
-
# FUNCTION MANAGEMENT - declare / lookup
|
| 161 |
-
# GAL functions are first registered (when the parser walks pollinate
|
| 162 |
-
# declarations) and then invoked by name from eval_function_call.
|
| 163 |
-
# ========================================================================
|
| 164 |
def declare_function(self, name, return_type, params, node=None):
|
| 165 |
if name in self.functions:
|
| 166 |
-
# Returns an error string — caller decides whether to raise
|
| 167 |
return f"Semantic Error: Function '{name}' already declared."
|
| 168 |
self.functions[name] = {"return_type": return_type, "params": params, "node": node}
|
| 169 |
|
|
@@ -173,22 +103,14 @@ class Interpreter:
|
|
| 173 |
return f"Semantic Error: Function '{name}' is not defined."
|
| 174 |
|
| 175 |
|
| 176 |
-
# ========================================================================
|
| 177 |
-
# SCOPE MANAGEMENT - enter / exit
|
| 178 |
-
# Each function call, block, and loop body pushes a fresh scope onto
|
| 179 |
-
# self.scopes. Variables declared inside go in the top dict and vanish
|
| 180 |
-
# when the scope is popped.
|
| 181 |
-
# ========================================================================
|
| 182 |
def enter_scope(self):
|
| 183 |
self.scopes.append({})
|
| 184 |
|
| 185 |
|
| 186 |
def exit_scope(self):
|
| 187 |
-
# Never pop the global scope (scopes[0])
|
| 188 |
if len(self.scopes) > 1:
|
| 189 |
self.scopes.pop()
|
| 190 |
|
| 191 |
-
# Clear function-local cache if we're leaving a function call
|
| 192 |
if self.current_func_name:
|
| 193 |
current_func = self.current_func_name
|
| 194 |
|
|
@@ -196,11 +118,6 @@ class Interpreter:
|
|
| 196 |
self.function_variables[current_func].clear()
|
| 197 |
|
| 198 |
|
| 199 |
-
# ========================================================================
|
| 200 |
-
# MAIN AST DISPATCH - The 'switch' that maps each AST-node class to its
|
| 201 |
-
# eval_* handler. This is the entry point of execution; server.py calls
|
| 202 |
-
# interp.interpret(ast) where ast is a ProgramNode.
|
| 203 |
-
# ========================================================================
|
| 204 |
def interpret(self, node):
|
| 205 |
if isinstance(node, ProgramNode):
|
| 206 |
return self.eval_program(node)
|
|
@@ -294,31 +211,20 @@ class Interpreter:
|
|
| 294 |
else:
|
| 295 |
raise Exception(f"Unknown AST node type: {node.node_type}")
|
| 296 |
|
| 297 |
-
# ========================================================================
|
| 298 |
-
# PROGRAM ENTRY - Walks every top-level declaration (globals, fertile,
|
| 299 |
-
# bundle, pollinate functions) and then synthesizes a call to root().
|
| 300 |
-
# This implements GAL's rule that root() is always the entry point.
|
| 301 |
-
# ========================================================================
|
| 302 |
def eval_program(self, node):
|
| 303 |
-
# 1. Process every top-level node (registers globals + functions)
|
| 304 |
for child in node.children:
|
| 305 |
self.interpret(child)
|
| 306 |
|
| 307 |
-
# 2. Now invoke root() — the program's entry point
|
| 308 |
main_call = FunctionCallNode("root", [], node.line)
|
| 309 |
return self.interpret(main_call)
|
| 310 |
|
| 311 |
|
| 312 |
-
# ========================================================================
|
| 313 |
-
# DECLARATIONS - variables, bundles, members, fertile (constants)
|
| 314 |
-
# ========================================================================
|
| 315 |
def eval_variable_declaration(self, node):
|
| 316 |
var_type = node.children[0].value
|
| 317 |
var_name = node.children[1].value
|
| 318 |
value_node = node.children[2] if len(node.children) > 2 else None
|
| 319 |
is_list = False
|
| 320 |
|
| 321 |
-
# Default values for uninitialized variables
|
| 322 |
default_values = {
|
| 323 |
"seed": 0,
|
| 324 |
"tree": 0.0,
|
|
@@ -331,7 +237,6 @@ class Interpreter:
|
|
| 331 |
if value_node.node_type == "List":
|
| 332 |
value = []
|
| 333 |
if var_type in self.bundle_types:
|
| 334 |
-
# Bundle array: initialize list of dicts with default member values (recursive for nested)
|
| 335 |
for _ in value_node.children:
|
| 336 |
value.append(self._build_bundle_defaults(var_type))
|
| 337 |
else:
|
|
@@ -380,22 +285,17 @@ class Interpreter:
|
|
| 380 |
else:
|
| 381 |
value = True
|
| 382 |
else:
|
| 383 |
-
# Uninitialized variable — use default value for the type
|
| 384 |
if var_type in self.bundle_types:
|
| 385 |
-
# Bundle variable: initialize with dict of default member values (recursive for nested)
|
| 386 |
value = self._build_bundle_defaults(var_type)
|
| 387 |
else:
|
| 388 |
value = default_values.get(var_type, None)
|
| 389 |
|
| 390 |
-
#print(f"\nDeclaring variable '{var_name}' of type '{var_type}' with initial value: {value}")
|
| 391 |
self.declare_variable(var_name, var_type, value, is_list=is_list)
|
| 392 |
|
| 393 |
def eval_bundle_definition(self, node):
|
| 394 |
-
"""Store bundle type definition for later use."""
|
| 395 |
self.bundle_types[node.bundle_name] = node.members
|
| 396 |
|
| 397 |
def _build_bundle_defaults(self, bundle_type_name):
|
| 398 |
-
"""Recursively build default values for a bundle type, including nested bundles."""
|
| 399 |
_member_defaults = {"seed": 0, "tree": 0.0, "leaf": '', "vine": "", "branch": False}
|
| 400 |
members = self.bundle_types[bundle_type_name]
|
| 401 |
result = {}
|
|
@@ -407,18 +307,14 @@ class Interpreter:
|
|
| 407 |
return result
|
| 408 |
|
| 409 |
def eval_member_access(self, node):
|
| 410 |
-
"""Read a bundle member value: p.age or p.addr.zip (nested)"""
|
| 411 |
obj_child = node.children[0]
|
| 412 |
member_name = node.children[1].value
|
| 413 |
|
| 414 |
if obj_child.node_type == "MemberAccess":
|
| 415 |
-
# Nested: evaluate inner member access first
|
| 416 |
bundle_value = self.eval_member_access(obj_child)
|
| 417 |
elif obj_child.node_type == "ArrayMemberAccess":
|
| 418 |
-
# Chained after array member: p[0].addr.zip
|
| 419 |
bundle_value = self.eval_array_member_access(obj_child)
|
| 420 |
else:
|
| 421 |
-
# Simple: obj_child is ASTNode("Object", var_name)
|
| 422 |
obj_name = obj_child.value
|
| 423 |
var_info = self.lookup_variable(obj_name)
|
| 424 |
if isinstance(var_info, str):
|
|
@@ -432,7 +328,6 @@ class Interpreter:
|
|
| 432 |
return bundle_value[member_name]
|
| 433 |
|
| 434 |
def eval_array_member_access(self, node):
|
| 435 |
-
"""Read a bundle array element member value: p[0].x"""
|
| 436 |
list_access_node = node.children[0]
|
| 437 |
member_name = node.children[1].value
|
| 438 |
bundle_element = self.eval_list_access(list_access_node)
|
|
@@ -450,16 +345,6 @@ class Interpreter:
|
|
| 450 |
self.declare_variable(var_name, var_type, value, is_list=False, is_fertile=True)
|
| 451 |
|
| 452 |
|
| 453 |
-
# ========================================================================
|
| 454 |
-
# ASSIGNMENT - The biggest dispatch in this file. Handles every shape:
|
| 455 |
-
# x = expr; (simple variable)
|
| 456 |
-
# arr[i] = expr; (single-dim list element)
|
| 457 |
-
# arr[i][j] = expr; (multi-dim list element)
|
| 458 |
-
# bundle.member = expr;(struct member)
|
| 459 |
-
# x += expr; etc. (compound assignment operators)
|
| 460 |
-
# Type-checking against the variable's declared type happens here too.
|
| 461 |
-
# Returning the stored value lets the same node serve as an expression.
|
| 462 |
-
# ========================================================================
|
| 463 |
def eval_assignment(self, node):
|
| 464 |
target_node = node.children[0]
|
| 465 |
value_node = node.children[1]
|
|
@@ -475,7 +360,6 @@ class Interpreter:
|
|
| 475 |
return
|
| 476 |
|
| 477 |
if target_node.node_type == "ListAccess":
|
| 478 |
-
# Collect all index nodes from outermost to innermost
|
| 479 |
indices = []
|
| 480 |
current = target_node
|
| 481 |
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
|
|
@@ -483,10 +367,8 @@ class Interpreter:
|
|
| 483 |
if not isinstance(idx, int):
|
| 484 |
raise InterpreterError(f"Runtime Error: List index must be an integer. Got '{idx}'", node.line)
|
| 485 |
indices.append(idx)
|
| 486 |
-
# children[0] is ASTNode("ListName", name_or_node)
|
| 487 |
current = current.children[0].value
|
| 488 |
|
| 489 |
-
# current is now the base variable name (string)
|
| 490 |
list_name = current
|
| 491 |
list_entry = self.lookup_variable(list_name)
|
| 492 |
if isinstance(list_entry, str):
|
|
@@ -496,7 +378,6 @@ class Interpreter:
|
|
| 496 |
if not isinstance(list_value, (list, str)):
|
| 497 |
raise InterpreterError(f"Runtime Error: Variable '{list_name}' is not a list.", node.line)
|
| 498 |
|
| 499 |
-
# Handle string element assignment (vine)
|
| 500 |
if isinstance(list_value, str):
|
| 501 |
if len(indices) != 1:
|
| 502 |
raise InterpreterError(f"Runtime Error: Multi-dimensional indexing not supported for strings.", node.line)
|
|
@@ -508,10 +389,8 @@ class Interpreter:
|
|
| 508 |
list_value = list_value[:final_idx] + value + list_value[final_idx + 1:]
|
| 509 |
list_entry["value"] = list_value
|
| 510 |
else:
|
| 511 |
-
# indices were collected outermost-first, reverse to get innermost-first
|
| 512 |
indices.reverse()
|
| 513 |
|
| 514 |
-
# Navigate to the correct nested list
|
| 515 |
target = list_value
|
| 516 |
for i, idx in enumerate(indices[:-1]):
|
| 517 |
if idx < 0 or idx >= len(target):
|
|
@@ -527,19 +406,15 @@ class Interpreter:
|
|
| 527 |
target[final_idx] = value
|
| 528 |
|
| 529 |
elif target_node.node_type == "MemberAccess":
|
| 530 |
-
# Bundle member assignment: p.age = 19 or p.addr.zip = 1000
|
| 531 |
-
# Collect the chain of member names from outermost to innermost
|
| 532 |
chain = []
|
| 533 |
current = target_node
|
| 534 |
while hasattr(current, 'node_type') and current.node_type == "MemberAccess":
|
| 535 |
-
chain.append(current.children[1].value)
|
| 536 |
-
current = current.children[0]
|
| 537 |
|
| 538 |
-
chain.reverse()
|
| 539 |
|
| 540 |
-
# current is now ASTNode("Object", var_name) or an ArrayMemberAccessNode
|
| 541 |
if hasattr(current, 'node_type') and current.node_type == "ArrayMemberAccess":
|
| 542 |
-
# Chained off array member: p[0].addr.zip = 1000
|
| 543 |
bundle_value = self.interpret(current)
|
| 544 |
if not isinstance(bundle_value, dict):
|
| 545 |
raise InterpreterError(f"Runtime Error: Value is not a bundle.", node.line)
|
|
@@ -552,7 +427,6 @@ class Interpreter:
|
|
| 552 |
if not isinstance(bundle_value, dict):
|
| 553 |
raise InterpreterError(f"Runtime Error: Variable '{obj_name}' is not a bundle.", node.line)
|
| 554 |
|
| 555 |
-
# Navigate to the parent dict (all but last member)
|
| 556 |
for member in chain[:-1]:
|
| 557 |
if member not in bundle_value:
|
| 558 |
raise InterpreterError(f"Runtime Error: Bundle has no member '{member}'.", node.line)
|
|
@@ -564,10 +438,8 @@ class Interpreter:
|
|
| 564 |
if final_member not in bundle_value:
|
| 565 |
raise InterpreterError(f"Runtime Error: Bundle has no member '{final_member}'.", node.line)
|
| 566 |
|
| 567 |
-
# Type coercion: find the final member's declared type
|
| 568 |
type_chain_current = current
|
| 569 |
if hasattr(type_chain_current, 'node_type') and type_chain_current.node_type == "ArrayMemberAccess":
|
| 570 |
-
# For array member chains, walk up to find the variable's bundle type
|
| 571 |
la_node = type_chain_current.children[0]
|
| 572 |
while hasattr(la_node, 'node_type') and la_node.node_type == "ListAccess":
|
| 573 |
la_node = la_node.children[0].value
|
|
@@ -578,7 +450,6 @@ class Interpreter:
|
|
| 578 |
var_type = var_info["type"] if not isinstance(var_info, str) else None
|
| 579 |
|
| 580 |
if var_type and var_type in self.bundle_types:
|
| 581 |
-
# Walk through the type definitions to find the final member type
|
| 582 |
cur_type = var_type
|
| 583 |
for member in chain:
|
| 584 |
if cur_type in self.bundle_types:
|
|
@@ -593,7 +464,6 @@ class Interpreter:
|
|
| 593 |
bundle_value[final_member] = value
|
| 594 |
|
| 595 |
elif target_node.node_type == "ArrayMemberAccess":
|
| 596 |
-
# Bundle array member assignment: p[0].x = 1
|
| 597 |
list_access_node = target_node.children[0]
|
| 598 |
member_name = target_node.children[1].value
|
| 599 |
bundle_element = self.eval_list_access(list_access_node)
|
|
@@ -602,7 +472,6 @@ class Interpreter:
|
|
| 602 |
if member_name not in bundle_element:
|
| 603 |
raise InterpreterError(f"Runtime Error: Bundle has no member '{member_name}'.", node.line)
|
| 604 |
|
| 605 |
-
# Type coercion for the member
|
| 606 |
current = list_access_node
|
| 607 |
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
|
| 608 |
current = current.children[0].value
|
|
@@ -636,26 +505,15 @@ class Interpreter:
|
|
| 636 |
value = True if value != 0 else False
|
| 637 |
|
| 638 |
self.set_variable(var_name, value)
|
| 639 |
-
#print(f"\nUpdating variable '{var_name}' of type '{var_type}' with value: {value}")
|
| 640 |
|
| 641 |
return value
|
| 642 |
|
| 643 |
|
| 644 |
-
# ========================================================================
|
| 645 |
-
# BINARY OPERATIONS - + - * / % ** == != < <= > >= && || `
|
| 646 |
-
# The biggest expression evaluator. Each operator branch coerces non-
|
| 647 |
-
# numeric operands (bool, str) into numeric form before computing, so
|
| 648 |
-
# mixed-type expressions don't crash. The `(backtick) operator handles
|
| 649 |
-
# vine concatenation specially (BEFORE _parse_literal) to preserve
|
| 650 |
-
# whitespace-only strings like " ".
|
| 651 |
-
# ========================================================================
|
| 652 |
def eval_binary_op(self, node):
|
| 653 |
left = self.interpret(node.children[0])
|
| 654 |
right = self.interpret(node.children[1])
|
| 655 |
operator = node.value
|
| 656 |
|
| 657 |
-
# Handle string concatenation with ` before _parse_literal
|
| 658 |
-
# to preserve whitespace-only vine values like " "
|
| 659 |
if operator == '`':
|
| 660 |
result = str(left) + str(right)
|
| 661 |
return result
|
|
@@ -819,12 +677,6 @@ class Interpreter:
|
|
| 819 |
except ZeroDivisionError:
|
| 820 |
raise InterpreterError("Runtime Error: Division by zero", "")
|
| 821 |
|
| 822 |
-
# ========================================================================
|
| 823 |
-
# _PARSE_LITERAL - Converts a token's string value into the right Python
|
| 824 |
-
# type. Handles GAL's negative-number prefix '~' (e.g. '~5' -> -5), the
|
| 825 |
-
# boolean keywords sunshine/frost, and ints / floats / strings / chars.
|
| 826 |
-
# Called from inside eval_binary_op before doing arithmetic.
|
| 827 |
-
# ========================================================================
|
| 828 |
def _parse_literal(self, value):
|
| 829 |
|
| 830 |
if isinstance(value, str):
|
|
@@ -851,7 +703,6 @@ class Interpreter:
|
|
| 851 |
if value in ('false', 'frost'):
|
| 852 |
return False
|
| 853 |
|
| 854 |
-
# Handle GAL negative literals: ~20 → -20, ~3.14 → -3.14
|
| 855 |
parse_value = value
|
| 856 |
if parse_value.startswith('~'):
|
| 857 |
parse_value = '-' + parse_value[1:]
|
|
@@ -864,11 +715,6 @@ class Interpreter:
|
|
| 864 |
return value
|
| 865 |
|
| 866 |
|
| 867 |
-
# ========================================================================
|
| 868 |
-
# FUNCTION DECLARATION - Registers a 'pollinate' function so it can be
|
| 869 |
-
# called later. This walks the parameter list and saves the function
|
| 870 |
-
# body node for the actual call to interpret.
|
| 871 |
-
# ========================================================================
|
| 872 |
def eval_function_declaration(self, node):
|
| 873 |
return_type = node.children[0].value
|
| 874 |
parameters_node = node.children[1]
|
|
@@ -888,41 +734,23 @@ class Interpreter:
|
|
| 888 |
|
| 889 |
return None
|
| 890 |
|
| 891 |
-
# ========================================================================
|
| 892 |
-
# BLOCK EXECUTION - Walks the children of a block { ... } in order,
|
| 893 |
-
# short-circuiting if a break (prune) or continue (skip) was triggered.
|
| 894 |
-
# ========================================================================
|
| 895 |
def eval_block(self, block_node):
|
| 896 |
for statement in block_node.children:
|
| 897 |
self.interpret(statement)
|
| 898 |
-
# 'prune' was hit somewhere inside — stop executing this block
|
| 899 |
if self.break_triggered():
|
| 900 |
return
|
| 901 |
-
# 'skip' was hit — also stop; the surrounding loop handles re-entry
|
| 902 |
if self.continue_flag:
|
| 903 |
return
|
| 904 |
|
| 905 |
|
| 906 |
-
# ========================================================================
|
| 907 |
-
# OUTPUT - 'plant' primitive. Routes to socketio.emit so output is
|
| 908 |
-
# delivered to the right client (live mode) or collected (sync mode).
|
| 909 |
-
# ========================================================================
|
| 910 |
def plant(self, value):
|
| 911 |
-
"""GAL output primitive."""
|
| 912 |
self.socketio.emit('output', {'output': str(value)})
|
| 913 |
|
| 914 |
-
# Backward-compatible alias kept for older internal callers
|
| 915 |
def plant_out(self, num):
|
| 916 |
self.socketio.emit('output', {'output': str(num)})
|
| 917 |
self.output.append(str(num))
|
| 918 |
|
| 919 |
|
| 920 |
-
# ========================================================================
|
| 921 |
-
# PRINT STATEMENT - Implements plant("template", arg1, arg2, ...).
|
| 922 |
-
# If the first argument is a string with {} placeholders, it formats
|
| 923 |
-
# the rest into the template (Python str.format style). Otherwise
|
| 924 |
-
# arguments are joined with spaces (C printf style).
|
| 925 |
-
# ========================================================================
|
| 926 |
def eval_print(self, node):
|
| 927 |
if not node.children:
|
| 928 |
return
|
|
@@ -957,7 +785,6 @@ class Interpreter:
|
|
| 957 |
self.plant(output_str)
|
| 958 |
return
|
| 959 |
|
| 960 |
-
# No {} placeholders — print all arguments separated by spaces (C-style)
|
| 961 |
if len(node.children) > 1:
|
| 962 |
parts = [str(evaluated_first)]
|
| 963 |
for arg in node.children[1:]:
|
|
@@ -972,17 +799,11 @@ class Interpreter:
|
|
| 972 |
|
| 973 |
self.plant(str(evaluated_first))
|
| 974 |
|
| 975 |
-
# ========================================================================
|
| 976 |
-
# FORMATTED STRING - Strips outer quotes and converts escape sequences
|
| 977 |
-
# (\n, \t, \", \{, \}, \/, \\) into their real characters before the
|
| 978 |
-
# string is used as a plant() template.
|
| 979 |
-
# ========================================================================
|
| 980 |
def eval_formatted_string(self, node):
|
| 981 |
value = node.value
|
| 982 |
if value.startswith('"') and value.endswith('"'):
|
| 983 |
value = value[1:-1]
|
| 984 |
|
| 985 |
-
# Escape sequences
|
| 986 |
value = value.replace(r'\\', '\\')
|
| 987 |
value = value.replace(r'\n', '\n')
|
| 988 |
value = value.replace(r'\t', '\t')
|
|
@@ -993,13 +814,7 @@ class Interpreter:
|
|
| 993 |
return value
|
| 994 |
|
| 995 |
|
| 996 |
-
# ========================================================================
|
| 997 |
-
# LIST ACCESS - arr[index] reads. Validates bounds and that the index
|
| 998 |
-
# is an integer. NOTE: indexing is 0-based in the implementation, even
|
| 999 |
-
# though the GAL spec describes 1-based arrays.
|
| 1000 |
-
# ========================================================================
|
| 1001 |
def eval_list_access(self, node):
|
| 1002 |
-
# children[0] is ASTNode("ListName", list_name) where list_name is a string or ListAccessNode
|
| 1003 |
name_or_node = node.children[0].value
|
| 1004 |
if hasattr(name_or_node, 'node_type') and name_or_node.node_type == "ListAccess":
|
| 1005 |
list_value = self.eval_list_access(name_or_node)
|
|
@@ -1025,22 +840,11 @@ class Interpreter:
|
|
| 1025 |
return list_value[index]
|
| 1026 |
|
| 1027 |
|
| 1028 |
-
# ========================================================================
|
| 1029 |
-
# RETURN ('reclaim') - Uses exception unwinding so the return value
|
| 1030 |
-
# bubbles up cleanly through any number of nested blocks/loops/ifs.
|
| 1031 |
-
# eval_function_call catches ReturnValue and uses its .value as the
|
| 1032 |
-
# call's result.
|
| 1033 |
-
# ========================================================================
|
| 1034 |
def eval_return(self, node):
|
| 1035 |
value = self.interpret(node.children[0]) if node.children else None
|
| 1036 |
raise ReturnValue(value)
|
| 1037 |
|
| 1038 |
|
| 1039 |
-
# ========================================================================
|
| 1040 |
-
# FUNCTION CALL - Looks up the function, validates arg count, binds
|
| 1041 |
-
# parameters into a fresh scope, and runs the body. ReturnValue is
|
| 1042 |
-
# caught to extract 'reclaim'-returned values.
|
| 1043 |
-
# ========================================================================
|
| 1044 |
def eval_function_call(self, node):
|
| 1045 |
function_name = node.value
|
| 1046 |
args = [self.interpret(arg.children[0]) for arg in node.children]
|
|
@@ -1066,7 +870,6 @@ class Interpreter:
|
|
| 1066 |
param_type = param["type"]
|
| 1067 |
arg_value = args[i]
|
| 1068 |
is_list = param.get("is_list", False)
|
| 1069 |
-
#print(f"\n[CALL] In function: {function_name} — Argument '{param_name}' of type '{param_type}' with value: {arg_value}")
|
| 1070 |
self.declare_variable(param_name, param_type, arg_value, is_list=is_list)
|
| 1071 |
|
| 1072 |
try:
|
|
@@ -1082,11 +885,6 @@ class Interpreter:
|
|
| 1082 |
self.current_func_name = None
|
| 1083 |
|
| 1084 |
|
| 1085 |
-
# ========================================================================
|
| 1086 |
-
# LIST OPERATIONS - append, insert, remove
|
| 1087 |
-
# These mutate the underlying Python list stored in the variable's value.
|
| 1088 |
-
# Index validation matches eval_list_access (0-based bounds).
|
| 1089 |
-
# ========================================================================
|
| 1090 |
def eval_append(self, node):
|
| 1091 |
list_name = node.parent.children[0].value
|
| 1092 |
list_info = self.lookup_variable(list_name)
|
|
@@ -1094,7 +892,6 @@ class Interpreter:
|
|
| 1094 |
for child in node.children:
|
| 1095 |
value = self.interpret(child)
|
| 1096 |
list_info["value"].append(value)
|
| 1097 |
-
#print(f"\nAppending value '{value}' to list '{list_name}'")
|
| 1098 |
|
| 1099 |
|
| 1100 |
def eval_insert(self, node):
|
|
@@ -1113,7 +910,6 @@ class Interpreter:
|
|
| 1113 |
value = self.interpret(child)
|
| 1114 |
list_info["value"].insert(index, value)
|
| 1115 |
index += 1
|
| 1116 |
-
#print(f"Inserted {value} at index {index} in list '{list_name}': {list_info['value']}")
|
| 1117 |
|
| 1118 |
|
| 1119 |
def eval_remove(self, node):
|
|
@@ -1133,26 +929,16 @@ class Interpreter:
|
|
| 1133 |
raise InterpreterError(f"Runtime Error: Index {index} out of bounds for remove", node.line)
|
| 1134 |
|
| 1135 |
removed = list_info["value"].pop(index)
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
# ========================================================================
|
| 1139 |
-
# UNARY OPERATIONS - x++, x--, ~x (negate), !x (logical not)
|
| 1140 |
-
# Increment/decrement mutate the variable in place; ~ and ! return a
|
| 1141 |
-
# new value without mutation. Works on both simple variables and
|
| 1142 |
-
# list elements (arr[i]++).
|
| 1143 |
-
# ========================================================================
|
| 1144 |
def eval_unaryop(self, node):
|
| 1145 |
-
# ++ / -- on a bundle member: ++p.age; p.addr.zip--;
|
| 1146 |
if isinstance(node.children[0], MemberAccessNode) and node.value in {"++", "--"}:
|
| 1147 |
target = node.children[0]
|
| 1148 |
-
# Walk the member chain to find the leaf bundle dict + final member name
|
| 1149 |
chain = []
|
| 1150 |
current = target
|
| 1151 |
while isinstance(current, MemberAccessNode):
|
| 1152 |
chain.append(current.children[1].value)
|
| 1153 |
current = current.children[0]
|
| 1154 |
chain.reverse()
|
| 1155 |
-
# current is now an Object/Identifier node carrying the root variable name
|
| 1156 |
obj_name = current.value
|
| 1157 |
var_info = self.lookup_variable(obj_name)
|
| 1158 |
if isinstance(var_info, str):
|
|
@@ -1160,7 +946,6 @@ class Interpreter:
|
|
| 1160 |
bundle_value = var_info["value"]
|
| 1161 |
if not isinstance(bundle_value, dict):
|
| 1162 |
raise InterpreterError(f"Runtime Error: Variable '{obj_name}' is not a bundle.", node.line)
|
| 1163 |
-
# Navigate down to the parent dict (all but last member)
|
| 1164 |
for member in chain[:-1]:
|
| 1165 |
if member not in bundle_value:
|
| 1166 |
raise InterpreterError(f"Runtime Error: Bundle has no member '{member}'.", node.line)
|
|
@@ -1186,7 +971,7 @@ class Interpreter:
|
|
| 1186 |
if node.position == "pre":
|
| 1187 |
var_info["value"] += 1
|
| 1188 |
return var_info["value"]
|
| 1189 |
-
else:
|
| 1190 |
original = var_info["value"]
|
| 1191 |
var_info["value"] += 1
|
| 1192 |
return original
|
|
@@ -1197,7 +982,7 @@ class Interpreter:
|
|
| 1197 |
if node.position == "pre":
|
| 1198 |
var_info["value"] -= 1
|
| 1199 |
return var_info["value"]
|
| 1200 |
-
else:
|
| 1201 |
original = var_info["value"]
|
| 1202 |
var_info["value"] -= 1
|
| 1203 |
return original
|
|
@@ -1246,14 +1031,8 @@ class Interpreter:
|
|
| 1246 |
return original if node.position == "post" else list_value[index]
|
| 1247 |
|
| 1248 |
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
raise InterpreterError(f"Unknown unary operator {node.value}", node.line)
|
| 1252 |
|
| 1253 |
-
# ========================================================================
|
| 1254 |
-
# TYPE CAST - Converts a value to the named GAL type. Supports the five
|
| 1255 |
-
# data types: seed, tree, leaf, branch, vine.
|
| 1256 |
-
# ========================================================================
|
| 1257 |
def eval_cast(self, node):
|
| 1258 |
value = self.interpret(node.children[1])
|
| 1259 |
cast_type = node.children[0].value
|
|
@@ -1264,7 +1043,7 @@ class Interpreter:
|
|
| 1264 |
return float(value)
|
| 1265 |
elif cast_type == "leaf":
|
| 1266 |
if isinstance(value, int):
|
| 1267 |
-
return chr(value)
|
| 1268 |
return str(value)[0] if value else '\0'
|
| 1269 |
elif cast_type == "branch":
|
| 1270 |
return bool(value)
|
|
@@ -1274,21 +1053,12 @@ class Interpreter:
|
|
| 1274 |
raise InterpreterError(f"Unknown cast type: {cast_type}", node.line)
|
| 1275 |
|
| 1276 |
|
| 1277 |
-
# ========================================================================
|
| 1278 |
-
# STRING / LIST UTILITY HELPERS - taper, ts, soil, bloom
|
| 1279 |
-
# taper : split a vine into a list of leaves (characters)
|
| 1280 |
-
# ts : length of a list, vine, or leaf-array
|
| 1281 |
-
# soil : lowercase a string
|
| 1282 |
-
# bloom : uppercase a string
|
| 1283 |
-
# These are GAL's built-in string/collection primitives.
|
| 1284 |
-
# ========================================================================
|
| 1285 |
def eval_taper(self, node):
|
| 1286 |
var_name = node.children[0].value
|
| 1287 |
var_info = self.lookup_variable(var_name)
|
| 1288 |
|
| 1289 |
if var_info["type"] == "leaf":
|
| 1290 |
value = list(var_info["value"])
|
| 1291 |
-
#print(f"Tapered string '{var_name}' into list: {var_info['value']}")
|
| 1292 |
|
| 1293 |
return value
|
| 1294 |
|
|
@@ -1314,18 +1084,11 @@ class Interpreter:
|
|
| 1314 |
var_info = self.lookup_variable(var_name)
|
| 1315 |
return var_info["value"].upper()
|
| 1316 |
|
| 1317 |
-
# ========================================================================
|
| 1318 |
-
# CONDITIONAL: spring / bud / wither (if / else-if / else)
|
| 1319 |
-
# GAL chains: spring (cond) {} bud (cond) {} bud (cond) {} wither {}
|
| 1320 |
-
# We evaluate the spring condition, then bud chain, then wither default.
|
| 1321 |
-
# The condition must be a boolean (branch type).
|
| 1322 |
-
# ========================================================================
|
| 1323 |
def eval_if_statement(self, node):
|
| 1324 |
condition_result = self.interpret(node.children[0].children[0])
|
| 1325 |
self.enter_scope()
|
| 1326 |
|
| 1327 |
|
| 1328 |
-
|
| 1329 |
try:
|
| 1330 |
if condition_result:
|
| 1331 |
self.eval_block(node.children[1])
|
|
@@ -1345,14 +1108,12 @@ class Interpreter:
|
|
| 1345 |
if elif_condition_result:
|
| 1346 |
try:
|
| 1347 |
self.enter_scope()
|
| 1348 |
-
#print(f"Executing ElseIf block: {elif_node.line}")
|
| 1349 |
self.eval_block(elif_node.children[1])
|
| 1350 |
finally:
|
| 1351 |
self.exit_scope()
|
| 1352 |
return
|
| 1353 |
|
| 1354 |
elif elif_node.node_type == "ElseStatement":
|
| 1355 |
-
#print(f"Executing Else block: {elif_node.line}")
|
| 1356 |
try:
|
| 1357 |
self.enter_scope()
|
| 1358 |
self.eval_block(elif_node.children[0])
|
|
@@ -1366,16 +1127,10 @@ class Interpreter:
|
|
| 1366 |
|
| 1367 |
return None
|
| 1368 |
|
| 1369 |
-
# ========================================================================
|
| 1370 |
-
# LOOPS: cultivate (for), grow (while), tend (do-while)
|
| 1371 |
-
# All three use the same MAX_LOOP_ITERATIONS guard (10000) to catch
|
| 1372 |
-
# runaway loops during a demo. Each tracks break/continue flags so
|
| 1373 |
-
# 'prune' exits the loop and 'skip' jumps to the next iteration.
|
| 1374 |
-
# ========================================================================
|
| 1375 |
def eval_for_loop(self, node):
|
| 1376 |
self.enter_loop('for')
|
| 1377 |
self.enter_scope()
|
| 1378 |
-
MAX_LOOP_ITERATIONS = 10000
|
| 1379 |
LOOP_COUNTER = 0
|
| 1380 |
|
| 1381 |
try:
|
|
@@ -1487,12 +1242,6 @@ class Interpreter:
|
|
| 1487 |
self.exit_loop()
|
| 1488 |
|
| 1489 |
|
| 1490 |
-
# ========================================================================
|
| 1491 |
-
# BREAK / CONTINUE - 'prune' and 'skip'
|
| 1492 |
-
# These set flags that the surrounding block / loop checks each
|
| 1493 |
-
# iteration. The loop_stack ensures break/continue can only be used
|
| 1494 |
-
# inside a loop or switch.
|
| 1495 |
-
# ========================================================================
|
| 1496 |
def eval_break(self, node):
|
| 1497 |
if self.loop_stack:
|
| 1498 |
self.trigger_break()
|
|
@@ -1506,7 +1255,6 @@ class Interpreter:
|
|
| 1506 |
return self.break_flag
|
| 1507 |
|
| 1508 |
def enter_loop(self, loop_type):
|
| 1509 |
-
# Push a new loop frame; reset break/continue so they don't carry over
|
| 1510 |
self.loop_stack.append(loop_type)
|
| 1511 |
self.break_flag = False
|
| 1512 |
self.continue_flag = False
|
|
@@ -1530,11 +1278,6 @@ class Interpreter:
|
|
| 1530 |
self.continue_flag = True
|
| 1531 |
|
| 1532 |
|
| 1533 |
-
# ========================================================================
|
| 1534 |
-
# SWITCH: harvest / variety / soil (switch / case / default)
|
| 1535 |
-
# Cases match the switch value. 'prune' inside a case exits the switch.
|
| 1536 |
-
# 'soil' is the default case (executes if no variety matched).
|
| 1537 |
-
# ========================================================================
|
| 1538 |
def eval_switch(self, node):
|
| 1539 |
self.enter_loop('switch')
|
| 1540 |
self.enter_scope()
|
|
@@ -1579,44 +1322,28 @@ class Interpreter:
|
|
| 1579 |
self.exit_scope()
|
| 1580 |
|
| 1581 |
|
| 1582 |
-
# ========================================================================
|
| 1583 |
-
# INPUT SYSTEM ('water')
|
| 1584 |
-
# When water() runs, the interpreter:
|
| 1585 |
-
# 1. emit_input_request -> tells the client to show an input box
|
| 1586 |
-
# 2. wait_for_input -> parks on an Event (eventlet or threading)
|
| 1587 |
-
# 3. (client emits 'capture_input' -> server calls provide_input)
|
| 1588 |
-
# 4. provide_input -> .send() / .set() unblocks the waiter
|
| 1589 |
-
# 5. wait_for_input returns the typed string
|
| 1590 |
-
# 6. eval_input parses and type-checks it (e.g. ~5 -> -5 for seed)
|
| 1591 |
-
# ========================================================================
|
| 1592 |
def emit_input_request(self, var_name, prompt):
|
| 1593 |
-
# Tell the client a water() prompt is needed
|
| 1594 |
self.socketio.emit('input_required', {'prompt': prompt, 'variable': var_name})
|
| 1595 |
|
| 1596 |
-
# Method to capture input from the client
|
| 1597 |
def provide_input(self, var_name, input_value):
|
| 1598 |
evt = self.input_events.get(var_name)
|
| 1599 |
if evt is None:
|
| 1600 |
-
# No waiter yet — stash the value for later
|
| 1601 |
self.input_values[var_name] = input_value
|
| 1602 |
return
|
| 1603 |
if _USE_EVENTLET:
|
| 1604 |
-
# eventlet.event.Event.send() unblocks the waiting greenlet
|
| 1605 |
evt.send(input_value)
|
| 1606 |
else:
|
| 1607 |
self.input_values[var_name] = input_value
|
| 1608 |
evt.set()
|
| 1609 |
|
| 1610 |
-
# Method to wait for input asynchronously
|
| 1611 |
def wait_for_input(self, var_name):
|
| 1612 |
-
# Check if input was already stashed (provide_input called first)
|
| 1613 |
if var_name in self.input_values:
|
| 1614 |
return self.input_values.pop(var_name)
|
| 1615 |
|
| 1616 |
if _USE_EVENTLET:
|
| 1617 |
evt = _ev.Event()
|
| 1618 |
self.input_events[var_name] = evt
|
| 1619 |
-
value = evt.wait()
|
| 1620 |
self.input_events.pop(var_name, None)
|
| 1621 |
if getattr(self, '_cancelled', False):
|
| 1622 |
raise _CancelledError()
|
|
@@ -1631,14 +1358,6 @@ class Interpreter:
|
|
| 1631 |
self.input_events.pop(var_name, None)
|
| 1632 |
return value
|
| 1633 |
|
| 1634 |
-
# ========================================================================
|
| 1635 |
-
# EVAL_INPUT - Implements 'water(...)'. Determines what type of value
|
| 1636 |
-
# the receiving variable expects, prompts the user, then validates and
|
| 1637 |
-
# converts the typed input to that type. Enforces strict GAL syntax:
|
| 1638 |
-
# - Negative numbers must use '~' (not '-')
|
| 1639 |
-
# - Booleans must be 'sunshine' or 'frost' (not 'true'/'false')
|
| 1640 |
-
# Errors include a "did you mean" suggestion to guide the user.
|
| 1641 |
-
# ========================================================================
|
| 1642 |
def eval_input(self, node):
|
| 1643 |
parent_node = node.parent
|
| 1644 |
if isinstance(parent_node, VariableDeclarationNode):
|
|
@@ -1648,8 +1367,6 @@ class Interpreter:
|
|
| 1648 |
elif isinstance(parent_node, AssignmentNode):
|
| 1649 |
target = parent_node.children[0]
|
| 1650 |
if isinstance(target, ListAccessNode):
|
| 1651 |
-
# water(arr[i]) — target is a list access node
|
| 1652 |
-
# Walk down to find the base list name
|
| 1653 |
current = target
|
| 1654 |
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
|
| 1655 |
current = current.children[0].value
|
|
@@ -1660,7 +1377,6 @@ class Interpreter:
|
|
| 1660 |
var_type = self.lookup_variable(var_name)["type"]
|
| 1661 |
|
| 1662 |
else:
|
| 1663 |
-
# Standalone water() / water(type) — extract type from node value
|
| 1664 |
var_name = "_input"
|
| 1665 |
if node.value and "(" in node.value:
|
| 1666 |
inner = node.value.split("(")[1].rstrip(")")
|
|
@@ -1668,21 +1384,18 @@ class Interpreter:
|
|
| 1668 |
else:
|
| 1669 |
var_type = "vine"
|
| 1670 |
|
| 1671 |
-
prompt = f"Input for {var_name}: "
|
| 1672 |
self.input_required = True
|
| 1673 |
|
| 1674 |
|
| 1675 |
-
# Emit the input request to the client
|
| 1676 |
self.emit_input_request(var_name, prompt)
|
| 1677 |
|
| 1678 |
-
# Wait for the input asynchronously
|
| 1679 |
input_value = self.wait_for_input(var_name)
|
| 1680 |
|
| 1681 |
|
| 1682 |
-
self.input_required = False
|
| 1683 |
|
| 1684 |
if var_type == "seed":
|
| 1685 |
-
# GAL uses ~ for negative numbers; '-' is NOT accepted at input
|
| 1686 |
original_input = input_value
|
| 1687 |
if isinstance(input_value, str) and input_value.startswith('-'):
|
| 1688 |
raise InterpreterError(f"Runtime Error: GAL uses '~' for negative numbers, not '-'. Got '{original_input}'; did you mean '~{original_input[1:]}'?", node.line)
|
|
@@ -1696,7 +1409,6 @@ class Interpreter:
|
|
| 1696 |
raise InterpreterError(f"Runtime Error: Expected integer value, got '{original_input}'", node.line)
|
| 1697 |
|
| 1698 |
elif var_type == "tree":
|
| 1699 |
-
# GAL uses ~ for negative numbers; '-' is NOT accepted at input
|
| 1700 |
original_input = input_value
|
| 1701 |
if isinstance(input_value, str) and input_value.startswith('-'):
|
| 1702 |
raise InterpreterError(f"Runtime Error: GAL uses '~' for negative numbers, not '-'. Got '{original_input}'; did you mean '~{original_input[1:]}'?", node.line)
|
|
@@ -1721,7 +1433,6 @@ class Interpreter:
|
|
| 1721 |
raise InterpreterError(f"Runtime Error: Expected float value, got '{original_input}'", node.line)
|
| 1722 |
|
| 1723 |
elif var_type == "branch":
|
| 1724 |
-
# GAL booleans are 'sunshine' (true) and 'frost' (false); 'true'/'false' are NOT accepted
|
| 1725 |
if input_value == "true" or input_value == "false":
|
| 1726 |
suggestion = "sunshine" if input_value == "true" else "frost"
|
| 1727 |
raise InterpreterError(f"Runtime Error: GAL uses 'sunshine' and 'frost' for booleans, not 'true'/'false'. Got '{input_value}'; did you mean '{suggestion}'?", node.line)
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
from shared.ast_nodes import *
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
import threading
|
| 6 |
import sys
|
| 7 |
|
|
|
|
|
|
|
| 8 |
sys.setrecursionlimit(10000)
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
try:
|
| 11 |
import eventlet.event as _ev
|
| 12 |
_USE_EVENTLET = True
|
|
|
|
| 14 |
_USE_EVENTLET = False
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from semantic.errors import SemanticError # noqa: F401 - some runtime checks raise it
|
| 18 |
from interpreter.errors import ( # noqa: F401 - runtime-specific error classes
|
| 19 |
ReturnValue,
|
|
|
|
| 22 |
InterpreterInputRequest,
|
| 23 |
)
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
class Interpreter:
|
| 26 |
def __init__(self, socketio=None):
|
| 27 |
+
self.output = []
|
| 28 |
+
self.socketio = socketio
|
| 29 |
+
|
| 30 |
+
self.loop_stack = []
|
| 31 |
+
self.break_flag = False
|
| 32 |
+
self.continue_flag = False
|
| 33 |
+
|
| 34 |
+
self.input_required = False
|
| 35 |
+
self.input_events = {}
|
| 36 |
+
self.input_values = {}
|
| 37 |
+
|
| 38 |
+
self.current_node = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
self.current_parent = None
|
| 40 |
|
| 41 |
+
self.variables = {}
|
| 42 |
+
self.global_variables = {}
|
| 43 |
+
self.functions = {}
|
| 44 |
+
self.scopes = [{}]
|
| 45 |
+
self.current_func_name = None
|
| 46 |
+
self.function_variables = {}
|
| 47 |
+
self.bundle_types = {}
|
| 48 |
+
|
| 49 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
def declare_variable(self, name, type_, value=None, is_list=False, is_fertile=False):
|
| 51 |
scope = self.scopes[-1]
|
| 52 |
current_func = self.current_func_name
|
|
|
|
| 72 |
self.global_variables[name] = self.variables[name]
|
| 73 |
|
| 74 |
|
|
|
|
| 75 |
def lookup_variable(self, name):
|
| 76 |
for i, scope in enumerate(reversed(self.scopes)):
|
| 77 |
if name in scope:
|
|
|
|
| 92 |
return f"Semantic Error: Variable '{name}' not declared in any scope."
|
| 93 |
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
def declare_function(self, name, return_type, params, node=None):
|
| 96 |
if name in self.functions:
|
|
|
|
| 97 |
return f"Semantic Error: Function '{name}' already declared."
|
| 98 |
self.functions[name] = {"return_type": return_type, "params": params, "node": node}
|
| 99 |
|
|
|
|
| 103 |
return f"Semantic Error: Function '{name}' is not defined."
|
| 104 |
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
def enter_scope(self):
|
| 107 |
self.scopes.append({})
|
| 108 |
|
| 109 |
|
| 110 |
def exit_scope(self):
|
|
|
|
| 111 |
if len(self.scopes) > 1:
|
| 112 |
self.scopes.pop()
|
| 113 |
|
|
|
|
| 114 |
if self.current_func_name:
|
| 115 |
current_func = self.current_func_name
|
| 116 |
|
|
|
|
| 118 |
self.function_variables[current_func].clear()
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
def interpret(self, node):
|
| 122 |
if isinstance(node, ProgramNode):
|
| 123 |
return self.eval_program(node)
|
|
|
|
| 211 |
else:
|
| 212 |
raise Exception(f"Unknown AST node type: {node.node_type}")
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
def eval_program(self, node):
|
|
|
|
| 215 |
for child in node.children:
|
| 216 |
self.interpret(child)
|
| 217 |
|
|
|
|
| 218 |
main_call = FunctionCallNode("root", [], node.line)
|
| 219 |
return self.interpret(main_call)
|
| 220 |
|
| 221 |
|
|
|
|
|
|
|
|
|
|
| 222 |
def eval_variable_declaration(self, node):
|
| 223 |
var_type = node.children[0].value
|
| 224 |
var_name = node.children[1].value
|
| 225 |
value_node = node.children[2] if len(node.children) > 2 else None
|
| 226 |
is_list = False
|
| 227 |
|
|
|
|
| 228 |
default_values = {
|
| 229 |
"seed": 0,
|
| 230 |
"tree": 0.0,
|
|
|
|
| 237 |
if value_node.node_type == "List":
|
| 238 |
value = []
|
| 239 |
if var_type in self.bundle_types:
|
|
|
|
| 240 |
for _ in value_node.children:
|
| 241 |
value.append(self._build_bundle_defaults(var_type))
|
| 242 |
else:
|
|
|
|
| 285 |
else:
|
| 286 |
value = True
|
| 287 |
else:
|
|
|
|
| 288 |
if var_type in self.bundle_types:
|
|
|
|
| 289 |
value = self._build_bundle_defaults(var_type)
|
| 290 |
else:
|
| 291 |
value = default_values.get(var_type, None)
|
| 292 |
|
|
|
|
| 293 |
self.declare_variable(var_name, var_type, value, is_list=is_list)
|
| 294 |
|
| 295 |
def eval_bundle_definition(self, node):
|
|
|
|
| 296 |
self.bundle_types[node.bundle_name] = node.members
|
| 297 |
|
| 298 |
def _build_bundle_defaults(self, bundle_type_name):
|
|
|
|
| 299 |
_member_defaults = {"seed": 0, "tree": 0.0, "leaf": '', "vine": "", "branch": False}
|
| 300 |
members = self.bundle_types[bundle_type_name]
|
| 301 |
result = {}
|
|
|
|
| 307 |
return result
|
| 308 |
|
| 309 |
def eval_member_access(self, node):
|
|
|
|
| 310 |
obj_child = node.children[0]
|
| 311 |
member_name = node.children[1].value
|
| 312 |
|
| 313 |
if obj_child.node_type == "MemberAccess":
|
|
|
|
| 314 |
bundle_value = self.eval_member_access(obj_child)
|
| 315 |
elif obj_child.node_type == "ArrayMemberAccess":
|
|
|
|
| 316 |
bundle_value = self.eval_array_member_access(obj_child)
|
| 317 |
else:
|
|
|
|
| 318 |
obj_name = obj_child.value
|
| 319 |
var_info = self.lookup_variable(obj_name)
|
| 320 |
if isinstance(var_info, str):
|
|
|
|
| 328 |
return bundle_value[member_name]
|
| 329 |
|
| 330 |
def eval_array_member_access(self, node):
|
|
|
|
| 331 |
list_access_node = node.children[0]
|
| 332 |
member_name = node.children[1].value
|
| 333 |
bundle_element = self.eval_list_access(list_access_node)
|
|
|
|
| 345 |
self.declare_variable(var_name, var_type, value, is_list=False, is_fertile=True)
|
| 346 |
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
def eval_assignment(self, node):
|
| 349 |
target_node = node.children[0]
|
| 350 |
value_node = node.children[1]
|
|
|
|
| 360 |
return
|
| 361 |
|
| 362 |
if target_node.node_type == "ListAccess":
|
|
|
|
| 363 |
indices = []
|
| 364 |
current = target_node
|
| 365 |
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
|
|
|
|
| 367 |
if not isinstance(idx, int):
|
| 368 |
raise InterpreterError(f"Runtime Error: List index must be an integer. Got '{idx}'", node.line)
|
| 369 |
indices.append(idx)
|
|
|
|
| 370 |
current = current.children[0].value
|
| 371 |
|
|
|
|
| 372 |
list_name = current
|
| 373 |
list_entry = self.lookup_variable(list_name)
|
| 374 |
if isinstance(list_entry, str):
|
|
|
|
| 378 |
if not isinstance(list_value, (list, str)):
|
| 379 |
raise InterpreterError(f"Runtime Error: Variable '{list_name}' is not a list.", node.line)
|
| 380 |
|
|
|
|
| 381 |
if isinstance(list_value, str):
|
| 382 |
if len(indices) != 1:
|
| 383 |
raise InterpreterError(f"Runtime Error: Multi-dimensional indexing not supported for strings.", node.line)
|
|
|
|
| 389 |
list_value = list_value[:final_idx] + value + list_value[final_idx + 1:]
|
| 390 |
list_entry["value"] = list_value
|
| 391 |
else:
|
|
|
|
| 392 |
indices.reverse()
|
| 393 |
|
|
|
|
| 394 |
target = list_value
|
| 395 |
for i, idx in enumerate(indices[:-1]):
|
| 396 |
if idx < 0 or idx >= len(target):
|
|
|
|
| 406 |
target[final_idx] = value
|
| 407 |
|
| 408 |
elif target_node.node_type == "MemberAccess":
|
|
|
|
|
|
|
| 409 |
chain = []
|
| 410 |
current = target_node
|
| 411 |
while hasattr(current, 'node_type') and current.node_type == "MemberAccess":
|
| 412 |
+
chain.append(current.children[1].value)
|
| 413 |
+
current = current.children[0]
|
| 414 |
|
| 415 |
+
chain.reverse()
|
| 416 |
|
|
|
|
| 417 |
if hasattr(current, 'node_type') and current.node_type == "ArrayMemberAccess":
|
|
|
|
| 418 |
bundle_value = self.interpret(current)
|
| 419 |
if not isinstance(bundle_value, dict):
|
| 420 |
raise InterpreterError(f"Runtime Error: Value is not a bundle.", node.line)
|
|
|
|
| 427 |
if not isinstance(bundle_value, dict):
|
| 428 |
raise InterpreterError(f"Runtime Error: Variable '{obj_name}' is not a bundle.", node.line)
|
| 429 |
|
|
|
|
| 430 |
for member in chain[:-1]:
|
| 431 |
if member not in bundle_value:
|
| 432 |
raise InterpreterError(f"Runtime Error: Bundle has no member '{member}'.", node.line)
|
|
|
|
| 438 |
if final_member not in bundle_value:
|
| 439 |
raise InterpreterError(f"Runtime Error: Bundle has no member '{final_member}'.", node.line)
|
| 440 |
|
|
|
|
| 441 |
type_chain_current = current
|
| 442 |
if hasattr(type_chain_current, 'node_type') and type_chain_current.node_type == "ArrayMemberAccess":
|
|
|
|
| 443 |
la_node = type_chain_current.children[0]
|
| 444 |
while hasattr(la_node, 'node_type') and la_node.node_type == "ListAccess":
|
| 445 |
la_node = la_node.children[0].value
|
|
|
|
| 450 |
var_type = var_info["type"] if not isinstance(var_info, str) else None
|
| 451 |
|
| 452 |
if var_type and var_type in self.bundle_types:
|
|
|
|
| 453 |
cur_type = var_type
|
| 454 |
for member in chain:
|
| 455 |
if cur_type in self.bundle_types:
|
|
|
|
| 464 |
bundle_value[final_member] = value
|
| 465 |
|
| 466 |
elif target_node.node_type == "ArrayMemberAccess":
|
|
|
|
| 467 |
list_access_node = target_node.children[0]
|
| 468 |
member_name = target_node.children[1].value
|
| 469 |
bundle_element = self.eval_list_access(list_access_node)
|
|
|
|
| 472 |
if member_name not in bundle_element:
|
| 473 |
raise InterpreterError(f"Runtime Error: Bundle has no member '{member_name}'.", node.line)
|
| 474 |
|
|
|
|
| 475 |
current = list_access_node
|
| 476 |
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
|
| 477 |
current = current.children[0].value
|
|
|
|
| 505 |
value = True if value != 0 else False
|
| 506 |
|
| 507 |
self.set_variable(var_name, value)
|
|
|
|
| 508 |
|
| 509 |
return value
|
| 510 |
|
| 511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
def eval_binary_op(self, node):
|
| 513 |
left = self.interpret(node.children[0])
|
| 514 |
right = self.interpret(node.children[1])
|
| 515 |
operator = node.value
|
| 516 |
|
|
|
|
|
|
|
| 517 |
if operator == '`':
|
| 518 |
result = str(left) + str(right)
|
| 519 |
return result
|
|
|
|
| 677 |
except ZeroDivisionError:
|
| 678 |
raise InterpreterError("Runtime Error: Division by zero", "")
|
| 679 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
def _parse_literal(self, value):
|
| 681 |
|
| 682 |
if isinstance(value, str):
|
|
|
|
| 703 |
if value in ('false', 'frost'):
|
| 704 |
return False
|
| 705 |
|
|
|
|
| 706 |
parse_value = value
|
| 707 |
if parse_value.startswith('~'):
|
| 708 |
parse_value = '-' + parse_value[1:]
|
|
|
|
| 715 |
return value
|
| 716 |
|
| 717 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
def eval_function_declaration(self, node):
|
| 719 |
return_type = node.children[0].value
|
| 720 |
parameters_node = node.children[1]
|
|
|
|
| 734 |
|
| 735 |
return None
|
| 736 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
def eval_block(self, block_node):
|
| 738 |
for statement in block_node.children:
|
| 739 |
self.interpret(statement)
|
|
|
|
| 740 |
if self.break_triggered():
|
| 741 |
return
|
|
|
|
| 742 |
if self.continue_flag:
|
| 743 |
return
|
| 744 |
|
| 745 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
def plant(self, value):
|
|
|
|
| 747 |
self.socketio.emit('output', {'output': str(value)})
|
| 748 |
|
|
|
|
| 749 |
def plant_out(self, num):
|
| 750 |
self.socketio.emit('output', {'output': str(num)})
|
| 751 |
self.output.append(str(num))
|
| 752 |
|
| 753 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
def eval_print(self, node):
|
| 755 |
if not node.children:
|
| 756 |
return
|
|
|
|
| 785 |
self.plant(output_str)
|
| 786 |
return
|
| 787 |
|
|
|
|
| 788 |
if len(node.children) > 1:
|
| 789 |
parts = [str(evaluated_first)]
|
| 790 |
for arg in node.children[1:]:
|
|
|
|
| 799 |
|
| 800 |
self.plant(str(evaluated_first))
|
| 801 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
def eval_formatted_string(self, node):
|
| 803 |
value = node.value
|
| 804 |
if value.startswith('"') and value.endswith('"'):
|
| 805 |
value = value[1:-1]
|
| 806 |
|
|
|
|
| 807 |
value = value.replace(r'\\', '\\')
|
| 808 |
value = value.replace(r'\n', '\n')
|
| 809 |
value = value.replace(r'\t', '\t')
|
|
|
|
| 814 |
return value
|
| 815 |
|
| 816 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
def eval_list_access(self, node):
|
|
|
|
| 818 |
name_or_node = node.children[0].value
|
| 819 |
if hasattr(name_or_node, 'node_type') and name_or_node.node_type == "ListAccess":
|
| 820 |
list_value = self.eval_list_access(name_or_node)
|
|
|
|
| 840 |
return list_value[index]
|
| 841 |
|
| 842 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
def eval_return(self, node):
|
| 844 |
value = self.interpret(node.children[0]) if node.children else None
|
| 845 |
raise ReturnValue(value)
|
| 846 |
|
| 847 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 848 |
def eval_function_call(self, node):
|
| 849 |
function_name = node.value
|
| 850 |
args = [self.interpret(arg.children[0]) for arg in node.children]
|
|
|
|
| 870 |
param_type = param["type"]
|
| 871 |
arg_value = args[i]
|
| 872 |
is_list = param.get("is_list", False)
|
|
|
|
| 873 |
self.declare_variable(param_name, param_type, arg_value, is_list=is_list)
|
| 874 |
|
| 875 |
try:
|
|
|
|
| 885 |
self.current_func_name = None
|
| 886 |
|
| 887 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
def eval_append(self, node):
|
| 889 |
list_name = node.parent.children[0].value
|
| 890 |
list_info = self.lookup_variable(list_name)
|
|
|
|
| 892 |
for child in node.children:
|
| 893 |
value = self.interpret(child)
|
| 894 |
list_info["value"].append(value)
|
|
|
|
| 895 |
|
| 896 |
|
| 897 |
def eval_insert(self, node):
|
|
|
|
| 910 |
value = self.interpret(child)
|
| 911 |
list_info["value"].insert(index, value)
|
| 912 |
index += 1
|
|
|
|
| 913 |
|
| 914 |
|
| 915 |
def eval_remove(self, node):
|
|
|
|
| 929 |
raise InterpreterError(f"Runtime Error: Index {index} out of bounds for remove", node.line)
|
| 930 |
|
| 931 |
removed = list_info["value"].pop(index)
|
| 932 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
def eval_unaryop(self, node):
|
|
|
|
| 934 |
if isinstance(node.children[0], MemberAccessNode) and node.value in {"++", "--"}:
|
| 935 |
target = node.children[0]
|
|
|
|
| 936 |
chain = []
|
| 937 |
current = target
|
| 938 |
while isinstance(current, MemberAccessNode):
|
| 939 |
chain.append(current.children[1].value)
|
| 940 |
current = current.children[0]
|
| 941 |
chain.reverse()
|
|
|
|
| 942 |
obj_name = current.value
|
| 943 |
var_info = self.lookup_variable(obj_name)
|
| 944 |
if isinstance(var_info, str):
|
|
|
|
| 946 |
bundle_value = var_info["value"]
|
| 947 |
if not isinstance(bundle_value, dict):
|
| 948 |
raise InterpreterError(f"Runtime Error: Variable '{obj_name}' is not a bundle.", node.line)
|
|
|
|
| 949 |
for member in chain[:-1]:
|
| 950 |
if member not in bundle_value:
|
| 951 |
raise InterpreterError(f"Runtime Error: Bundle has no member '{member}'.", node.line)
|
|
|
|
| 971 |
if node.position == "pre":
|
| 972 |
var_info["value"] += 1
|
| 973 |
return var_info["value"]
|
| 974 |
+
else:
|
| 975 |
original = var_info["value"]
|
| 976 |
var_info["value"] += 1
|
| 977 |
return original
|
|
|
|
| 982 |
if node.position == "pre":
|
| 983 |
var_info["value"] -= 1
|
| 984 |
return var_info["value"]
|
| 985 |
+
else:
|
| 986 |
original = var_info["value"]
|
| 987 |
var_info["value"] -= 1
|
| 988 |
return original
|
|
|
|
| 1031 |
return original if node.position == "post" else list_value[index]
|
| 1032 |
|
| 1033 |
|
|
|
|
|
|
|
| 1034 |
raise InterpreterError(f"Unknown unary operator {node.value}", node.line)
|
| 1035 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
def eval_cast(self, node):
|
| 1037 |
value = self.interpret(node.children[1])
|
| 1038 |
cast_type = node.children[0].value
|
|
|
|
| 1043 |
return float(value)
|
| 1044 |
elif cast_type == "leaf":
|
| 1045 |
if isinstance(value, int):
|
| 1046 |
+
return chr(value)
|
| 1047 |
return str(value)[0] if value else '\0'
|
| 1048 |
elif cast_type == "branch":
|
| 1049 |
return bool(value)
|
|
|
|
| 1053 |
raise InterpreterError(f"Unknown cast type: {cast_type}", node.line)
|
| 1054 |
|
| 1055 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1056 |
def eval_taper(self, node):
|
| 1057 |
var_name = node.children[0].value
|
| 1058 |
var_info = self.lookup_variable(var_name)
|
| 1059 |
|
| 1060 |
if var_info["type"] == "leaf":
|
| 1061 |
value = list(var_info["value"])
|
|
|
|
| 1062 |
|
| 1063 |
return value
|
| 1064 |
|
|
|
|
| 1084 |
var_info = self.lookup_variable(var_name)
|
| 1085 |
return var_info["value"].upper()
|
| 1086 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
def eval_if_statement(self, node):
|
| 1088 |
condition_result = self.interpret(node.children[0].children[0])
|
| 1089 |
self.enter_scope()
|
| 1090 |
|
| 1091 |
|
|
|
|
| 1092 |
try:
|
| 1093 |
if condition_result:
|
| 1094 |
self.eval_block(node.children[1])
|
|
|
|
| 1108 |
if elif_condition_result:
|
| 1109 |
try:
|
| 1110 |
self.enter_scope()
|
|
|
|
| 1111 |
self.eval_block(elif_node.children[1])
|
| 1112 |
finally:
|
| 1113 |
self.exit_scope()
|
| 1114 |
return
|
| 1115 |
|
| 1116 |
elif elif_node.node_type == "ElseStatement":
|
|
|
|
| 1117 |
try:
|
| 1118 |
self.enter_scope()
|
| 1119 |
self.eval_block(elif_node.children[0])
|
|
|
|
| 1127 |
|
| 1128 |
return None
|
| 1129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1130 |
def eval_for_loop(self, node):
|
| 1131 |
self.enter_loop('for')
|
| 1132 |
self.enter_scope()
|
| 1133 |
+
MAX_LOOP_ITERATIONS = 10000
|
| 1134 |
LOOP_COUNTER = 0
|
| 1135 |
|
| 1136 |
try:
|
|
|
|
| 1242 |
self.exit_loop()
|
| 1243 |
|
| 1244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1245 |
def eval_break(self, node):
|
| 1246 |
if self.loop_stack:
|
| 1247 |
self.trigger_break()
|
|
|
|
| 1255 |
return self.break_flag
|
| 1256 |
|
| 1257 |
def enter_loop(self, loop_type):
|
|
|
|
| 1258 |
self.loop_stack.append(loop_type)
|
| 1259 |
self.break_flag = False
|
| 1260 |
self.continue_flag = False
|
|
|
|
| 1278 |
self.continue_flag = True
|
| 1279 |
|
| 1280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1281 |
def eval_switch(self, node):
|
| 1282 |
self.enter_loop('switch')
|
| 1283 |
self.enter_scope()
|
|
|
|
| 1322 |
self.exit_scope()
|
| 1323 |
|
| 1324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1325 |
def emit_input_request(self, var_name, prompt):
|
|
|
|
| 1326 |
self.socketio.emit('input_required', {'prompt': prompt, 'variable': var_name})
|
| 1327 |
|
|
|
|
| 1328 |
def provide_input(self, var_name, input_value):
|
| 1329 |
evt = self.input_events.get(var_name)
|
| 1330 |
if evt is None:
|
|
|
|
| 1331 |
self.input_values[var_name] = input_value
|
| 1332 |
return
|
| 1333 |
if _USE_EVENTLET:
|
|
|
|
| 1334 |
evt.send(input_value)
|
| 1335 |
else:
|
| 1336 |
self.input_values[var_name] = input_value
|
| 1337 |
evt.set()
|
| 1338 |
|
|
|
|
| 1339 |
def wait_for_input(self, var_name):
|
|
|
|
| 1340 |
if var_name in self.input_values:
|
| 1341 |
return self.input_values.pop(var_name)
|
| 1342 |
|
| 1343 |
if _USE_EVENTLET:
|
| 1344 |
evt = _ev.Event()
|
| 1345 |
self.input_events[var_name] = evt
|
| 1346 |
+
value = evt.wait()
|
| 1347 |
self.input_events.pop(var_name, None)
|
| 1348 |
if getattr(self, '_cancelled', False):
|
| 1349 |
raise _CancelledError()
|
|
|
|
| 1358 |
self.input_events.pop(var_name, None)
|
| 1359 |
return value
|
| 1360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1361 |
def eval_input(self, node):
|
| 1362 |
parent_node = node.parent
|
| 1363 |
if isinstance(parent_node, VariableDeclarationNode):
|
|
|
|
| 1367 |
elif isinstance(parent_node, AssignmentNode):
|
| 1368 |
target = parent_node.children[0]
|
| 1369 |
if isinstance(target, ListAccessNode):
|
|
|
|
|
|
|
| 1370 |
current = target
|
| 1371 |
while hasattr(current, 'node_type') and current.node_type == "ListAccess":
|
| 1372 |
current = current.children[0].value
|
|
|
|
| 1377 |
var_type = self.lookup_variable(var_name)["type"]
|
| 1378 |
|
| 1379 |
else:
|
|
|
|
| 1380 |
var_name = "_input"
|
| 1381 |
if node.value and "(" in node.value:
|
| 1382 |
inner = node.value.split("(")[1].rstrip(")")
|
|
|
|
| 1384 |
else:
|
| 1385 |
var_type = "vine"
|
| 1386 |
|
| 1387 |
+
prompt = f"Input for {var_name}: "
|
| 1388 |
self.input_required = True
|
| 1389 |
|
| 1390 |
|
|
|
|
| 1391 |
self.emit_input_request(var_name, prompt)
|
| 1392 |
|
|
|
|
| 1393 |
input_value = self.wait_for_input(var_name)
|
| 1394 |
|
| 1395 |
|
| 1396 |
+
self.input_required = False
|
| 1397 |
|
| 1398 |
if var_type == "seed":
|
|
|
|
| 1399 |
original_input = input_value
|
| 1400 |
if isinstance(input_value, str) and input_value.startswith('-'):
|
| 1401 |
raise InterpreterError(f"Runtime Error: GAL uses '~' for negative numbers, not '-'. Got '{original_input}'; did you mean '~{original_input[1:]}'?", node.line)
|
|
|
|
| 1409 |
raise InterpreterError(f"Runtime Error: Expected integer value, got '{original_input}'", node.line)
|
| 1410 |
|
| 1411 |
elif var_type == "tree":
|
|
|
|
| 1412 |
original_input = input_value
|
| 1413 |
if isinstance(input_value, str) and input_value.startswith('-'):
|
| 1414 |
raise InterpreterError(f"Runtime Error: GAL uses '~' for negative numbers, not '-'. Got '{original_input}'; did you mean '~{original_input[1:]}'?", node.line)
|
|
|
|
| 1433 |
raise InterpreterError(f"Runtime Error: Expected float value, got '{original_input}'", node.line)
|
| 1434 |
|
| 1435 |
elif var_type == "branch":
|
|
|
|
| 1436 |
if input_value == "true" or input_value == "false":
|
| 1437 |
suggestion = "sunshine" if input_value == "true" else "frost"
|
| 1438 |
raise InterpreterError(f"Runtime Error: GAL uses 'sunshine' and 'frost' for booleans, not 'true'/'false'. Got '{input_value}'; did you mean '{suggestion}'?", node.line)
|
Backend/lexer/__init__.py
CHANGED
|
@@ -1,16 +1,6 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# LEXER PACKAGE - Public API for the lexical-analysis phase
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Re-exports everything needed by external callers (server.py, parser, tests).
|
| 5 |
-
# After the modular restructure, `from lexer import lex, Token, TT_RW_ROOT, ...`
|
| 6 |
-
# still works exactly as before — the Lexer class itself lives in
|
| 7 |
-
# lexer/scanner.py, while shared types live in tokens.py / positions.py /
|
| 8 |
-
# errors.py at the Backend/ root.
|
| 9 |
-
# ============================================================================
|
| 10 |
|
| 11 |
-
from .scanner import Lexer, lex
|
| 12 |
|
| 13 |
-
# Re-export shared types so old `from lexer import X` callers keep working.
|
| 14 |
from shared.tokens import * # noqa: F401,F403 - TT_* constants
|
| 15 |
from shared.tokens import Token, get_token_description # noqa: F401 - explicit
|
| 16 |
from lexer.positions import Position # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
+
from .scanner import Lexer, lex
|
| 3 |
|
|
|
|
| 4 |
from shared.tokens import * # noqa: F401,F403 - TT_* constants
|
| 5 |
from shared.tokens import Token, get_token_description # noqa: F401 - explicit
|
| 6 |
from lexer.positions import Position # noqa: F401
|
Backend/lexer/delimiters.py
CHANGED
|
@@ -1,54 +1,43 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# DELIMITER SETS - Character sets used by the lexer's FSM
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Each delim* set names the characters that may legally appear AFTER a
|
| 5 |
-
# specific token type. The lexer consults these to enforce GAL's strict
|
| 6 |
-
# delimiter rules and reject ambiguous trailing characters.
|
| 7 |
-
# ============================================================================
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
ZERODIGIT = ZERO + DIGIT # All digits (0-9)
|
| 13 |
|
| 14 |
-
LOW_ALPHA = 'abcdefghijklmnopqrstuvwxyz'
|
| 15 |
-
UPPER_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
| 16 |
-
ALPHA = LOW_ALPHA + UPPER_ALPHA
|
| 17 |
-
ALPHANUM = ALPHA + ZERODIGIT + '_'
|
| 18 |
|
| 19 |
-
# --- DELIMITER SETS ---
|
| 20 |
-
# Delimiters are characters that can legally appear AFTER certain tokens.
|
| 21 |
-
# Different token types have different valid delimiters to ensure proper syntax.
|
| 22 |
space_delim = {' ', '\t', '\n'}
|
| 23 |
-
delim2 = {':'}
|
| 24 |
-
delim3 = {'{'}
|
| 25 |
-
delim4 = {':', '('}
|
| 26 |
-
delim5 = {'('}
|
| 27 |
-
delim6 = {';', ',', '=', '>', '<', '!', '}', ')', '('}
|
| 28 |
-
delim7 = {'('}
|
| 29 |
-
delim8 = {';'}
|
| 30 |
-
delim9 = set(ALPHA + '(' + ',' + ';' + ')')
|
| 31 |
-
delim10 = {';', ')'}
|
| 32 |
-
delim11 = set([LOW_ALPHA, ZERODIGIT, ']', '~'])
|
| 33 |
-
delim12 = set(ALPHA + ZERODIGIT + ']' + '~')
|
| 34 |
-
delim13 = {';', ')', '['}
|
| 35 |
-
delim14 = set(ALPHA + ZERODIGIT + '"' + "'" + '{')
|
| 36 |
-
delim15 = {'\n', ';', '}', ','}
|
| 37 |
-
delim16 = set(ALPHANUM + ')' + '"' + '!' + '(' + '[' + '\'')
|
| 38 |
-
delim17 = {'}', ';', ',', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|'}
|
| 39 |
-
delim18 = {';', '{', ')', '&', '|', '+', '-', '*', '/', '%'}
|
| 40 |
-
delim19 = {';', ',', '}', ')', '=', '>', '<', '!'}
|
| 41 |
-
delim20 = set(ALPHA + ZERODIGIT + '"' + "'" + '{')
|
| 42 |
-
delim21 = set(DIGIT)
|
| 43 |
-
delim22 = {',', ';', '(', ')', '{', '[', ']'}
|
| 44 |
-
delim23 = {';', ',', '}', ']', ')', ':', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|'}
|
| 45 |
-
delim24 = set(ZERODIGIT + ALPHA + '~!(' + "\"'" + ' \t\n')
|
| 46 |
-
delim25 = set(ALPHANUM + ';}) \t\n')
|
| 47 |
-
delim26 = {'(', '['} | set(ALPHA) | {' ', '\t', '\n'}
|
| 48 |
idf_delim = {' ', ',', ';', '(', ')', '{', '}', '[', ']', ':', '+', '-', '*', '/', '%',
|
| 49 |
-
'>', '<', '=', '\t', '\n', '.', '"', "'"}
|
| 50 |
whlnum_delim = {';', ' ', ',', '}', ']', ')', ':', '+', '-', '*', '/', '%', '=', '>', '<',
|
| 51 |
-
'!', '&', '|', '\t', '\n'}
|
| 52 |
decim_delim = {'}', ';', ',', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|', ' ',
|
| 53 |
-
'\t', '\n', ')', ']'}
|
| 54 |
-
comment_delim = set(ALPHANUM + ';+-*/%}{()' + '\n')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
+
ZERO = '0'
|
| 3 |
+
DIGIT = '123456789'
|
| 4 |
+
ZERODIGIT = ZERO + DIGIT
|
|
|
|
| 5 |
|
| 6 |
+
LOW_ALPHA = 'abcdefghijklmnopqrstuvwxyz'
|
| 7 |
+
UPPER_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
| 8 |
+
ALPHA = LOW_ALPHA + UPPER_ALPHA
|
| 9 |
+
ALPHANUM = ALPHA + ZERODIGIT + '_'
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
space_delim = {' ', '\t', '\n'}
|
| 12 |
+
delim2 = {':'}
|
| 13 |
+
delim3 = {'{'}
|
| 14 |
+
delim4 = {':', '('}
|
| 15 |
+
delim5 = {'('}
|
| 16 |
+
delim6 = {';', ',', '=', '>', '<', '!', '}', ')', '('}
|
| 17 |
+
delim7 = {'('}
|
| 18 |
+
delim8 = {';'}
|
| 19 |
+
delim9 = set(ALPHA + '(' + ',' + ';' + ')')
|
| 20 |
+
delim10 = {';', ')'}
|
| 21 |
+
delim11 = set([LOW_ALPHA, ZERODIGIT, ']', '~'])
|
| 22 |
+
delim12 = set(ALPHA + ZERODIGIT + ']' + '~')
|
| 23 |
+
delim13 = {';', ')', '['}
|
| 24 |
+
delim14 = set(ALPHA + ZERODIGIT + '"' + "'" + '{')
|
| 25 |
+
delim15 = {'\n', ';', '}', ','}
|
| 26 |
+
delim16 = set(ALPHANUM + ')' + '"' + '!' + '(' + '[' + '\'')
|
| 27 |
+
delim17 = {'}', ';', ',', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|'}
|
| 28 |
+
delim18 = {';', '{', ')', '&', '|', '+', '-', '*', '/', '%'}
|
| 29 |
+
delim19 = {';', ',', '}', ')', '=', '>', '<', '!'}
|
| 30 |
+
delim20 = set(ALPHA + ZERODIGIT + '"' + "'" + '{')
|
| 31 |
+
delim21 = set(DIGIT)
|
| 32 |
+
delim22 = {',', ';', '(', ')', '{', '[', ']'}
|
| 33 |
+
delim23 = {';', ',', '}', ']', ')', ':', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|'}
|
| 34 |
+
delim24 = set(ZERODIGIT + ALPHA + '~!(' + "\"'" + ' \t\n')
|
| 35 |
+
delim25 = set(ALPHANUM + ';}) \t\n')
|
| 36 |
+
delim26 = {'(', '['} | set(ALPHA) | {' ', '\t', '\n'}
|
| 37 |
idf_delim = {' ', ',', ';', '(', ')', '{', '}', '[', ']', ':', '+', '-', '*', '/', '%',
|
| 38 |
+
'>', '<', '=', '\t', '\n', '.', '"', "'"}
|
| 39 |
whlnum_delim = {';', ' ', ',', '}', ']', ')', ':', '+', '-', '*', '/', '%', '=', '>', '<',
|
| 40 |
+
'!', '&', '|', '\t', '\n'}
|
| 41 |
decim_delim = {'}', ';', ',', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|', ' ',
|
| 42 |
+
'\t', '\n', ')', ']'}
|
| 43 |
+
comment_delim = set(ALPHANUM + ';+-*/%}{()' + '\n')
|
Backend/lexer/errors.py
CHANGED
|
@@ -1,19 +1,11 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# LEXICAL ERROR - Raised by the lexer FSM on illegal characters / delimiters
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Stores a Position so the IDE can highlight the offending column.
|
| 5 |
-
# Lives next to the scanner because lexical errors are a pure lexer concern.
|
| 6 |
-
# ============================================================================
|
| 7 |
|
| 8 |
|
| 9 |
class LexicalError:
|
| 10 |
-
"""Stores information about a lexical error (position and description)"""
|
| 11 |
|
| 12 |
def __init__(self, pos, details):
|
| 13 |
-
self.pos = pos
|
| 14 |
-
self.details = details
|
| 15 |
|
| 16 |
def as_string(self):
|
| 17 |
-
|
| 18 |
-
self.details = self.details.replace('\n', '\\n') # Escape newlines in error message
|
| 19 |
return f"LEXICAL error line {self.pos.ln} col {self.pos.col} {self.details}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
|
| 3 |
class LexicalError:
|
|
|
|
| 4 |
|
| 5 |
def __init__(self, pos, details):
|
| 6 |
+
self.pos = pos
|
| 7 |
+
self.details = details
|
| 8 |
|
| 9 |
def as_string(self):
|
| 10 |
+
self.details = self.details.replace('\n', '\\n')
|
|
|
|
| 11 |
return f"LEXICAL error line {self.pos.ln} col {self.pos.col} {self.details}"
|
Backend/lexer/positions.py
CHANGED
|
@@ -1,31 +1,21 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# POSITION CLASS - Tracks current location in source code
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from lexer.py during the modular restructure.
|
| 5 |
-
# Shared by every compiler phase that needs to report source locations
|
| 6 |
-
# (lexer, parser, semantic analyzer, ICG, interpreter).
|
| 7 |
-
# ============================================================================
|
| 8 |
|
| 9 |
|
| 10 |
class Position:
|
| 11 |
-
"""Tracks the position (line, column, index) in the source code for error reporting"""
|
| 12 |
|
| 13 |
def __init__(self, index, ln, col=0):
|
| 14 |
-
self.index = index
|
| 15 |
-
self.ln = ln
|
| 16 |
-
self.col = col
|
| 17 |
|
| 18 |
def advance(self, current_char):
|
| 19 |
-
"""Advance to the next character, updating line/column numbers"""
|
| 20 |
self.index += 1
|
| 21 |
self.col += 1
|
| 22 |
|
| 23 |
-
if current_char == '\n':
|
| 24 |
self.ln += 1
|
| 25 |
-
self.col = 0
|
| 26 |
|
| 27 |
return self
|
| 28 |
|
| 29 |
def copy(self):
|
| 30 |
-
"""Returns a copy of the current position (for error tracking)"""
|
| 31 |
return Position(self.index, self.ln, self.col)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
|
| 3 |
class Position:
|
|
|
|
| 4 |
|
| 5 |
def __init__(self, index, ln, col=0):
|
| 6 |
+
self.index = index
|
| 7 |
+
self.ln = ln
|
| 8 |
+
self.col = col
|
| 9 |
|
| 10 |
def advance(self, current_char):
|
|
|
|
| 11 |
self.index += 1
|
| 12 |
self.col += 1
|
| 13 |
|
| 14 |
+
if current_char == '\n':
|
| 15 |
self.ln += 1
|
| 16 |
+
self.col = 0
|
| 17 |
|
| 18 |
return self
|
| 19 |
|
| 20 |
def copy(self):
|
|
|
|
| 21 |
return Position(self.index, self.ln, self.col)
|
Backend/lexer/scanner.py
CHANGED
|
@@ -1,12 +1,3 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# LEXER SCANNER - The Lexer FSM and the lex() entry point
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from the monolithic Backend/lexer.py during the modular restructure.
|
| 5 |
-
# This file contains the Lexer class (character-by-character FSM) plus the
|
| 6 |
-
# lex() helper that wraps it. Shared types (Token, Position, LexicalError,
|
| 7 |
-
# TT_* constants) live in tokens.py / positions.py / errors.py and are
|
| 8 |
-
# imported below. Character / delimiter sets live in delimiters.py.
|
| 9 |
-
# ============================================================================
|
| 10 |
from shared.tokens import * # noqa: F401,F403 - TT_* constants used heavily by the FSM
|
| 11 |
from shared.tokens import Token, get_token_description # noqa: F401 - explicit re-export
|
| 12 |
from lexer.positions import Position
|
|
@@ -22,177 +13,147 @@ from lexer.delimiters import (
|
|
| 22 |
|
| 23 |
|
| 24 |
class Lexer:
|
| 25 |
-
"""
|
| 26 |
-
The Lexer class performs lexical analysis on GAL source code.
|
| 27 |
-
It scans character by character and groups them into tokens.
|
| 28 |
-
"""
|
| 29 |
def __init__(self, source_code):
|
| 30 |
-
self.source_code = source_code.replace('\r', '')
|
| 31 |
-
self.pos = Position(-1, 1, -1)
|
| 32 |
-
self.current_char = None
|
| 33 |
-
self.advance()
|
| 34 |
|
| 35 |
def advance(self):
|
| 36 |
-
"""Advance to the next character in the source code"""
|
| 37 |
self.pos.advance(self.current_char)
|
| 38 |
-
# Get character at current index, or None if at end
|
| 39 |
self.current_char = self.source_code[self.pos.index] if self.pos.index<len(self.source_code) else None
|
| 40 |
|
| 41 |
def make_tokens(self):
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
- errors: list of LexicalError objects
|
| 47 |
-
"""
|
| 48 |
-
tokens = [] # accumulator: every recognized Token is appended here
|
| 49 |
-
line = 1 # running line number (1-indexed for user-facing errors)
|
| 50 |
-
errors = [] # accumulator: every LexicalError encountered is appended here
|
| 51 |
-
pos = self.pos.copy() # snapshot of current pos; reused as fallback for EOF token at end
|
| 52 |
|
| 53 |
-
|
| 54 |
-
while self.current_char != None: # None sentinel = end of source_code reached
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
# If no keyword path completes, the trailing "else" falls through to
|
| 60 |
-
# generic identifier scanning at the bottom of this if-block.
|
| 61 |
-
# =====================================================================
|
| 62 |
-
if self.current_char in ALPHA: # only letters can start a keyword/identifier (digits cannot)
|
| 63 |
-
ident_str = '' # buffer that will hold the lexeme as we walk it character-by-character
|
| 64 |
-
pos = self.pos.copy() # capture starting position so error/Token carries the *first* char's line/col
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
self.
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
self.
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
self.
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
self.
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
self.
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
self.advance() # consume 'h'; current_char is now whatever comes AFTER "branch"
|
| 85 |
-
# Maximal-munch check: only emit BRANCH token if the next char is a legal delimiter.
|
| 86 |
-
# Otherwise "branchx" must become an identifier, not keyword+identifier.
|
| 87 |
if self.current_char is None or self.current_char in space_delim or self.current_char == ';':
|
| 88 |
-
tokens.append(Token(TT_RW_BRANCH, ident_str, line, pos.col))
|
| 89 |
-
continue
|
| 90 |
-
elif self.current_char not in ALPHANUM:
|
| 91 |
-
tokens.append(Token(TT_RW_BRANCH, ident_str, line, pos.col))
|
| 92 |
-
continue
|
| 93 |
-
elif self.current_char == "u":
|
| 94 |
-
ident_str += self.current_char
|
| 95 |
-
self.advance()
|
| 96 |
-
if self.current_char == "d":
|
| 97 |
-
ident_str += self.current_char
|
| 98 |
-
self.advance()
|
| 99 |
-
# Maximal-munch: "bud" needs whitespace or delim4 ({':', '('}) to terminate.
|
| 100 |
if self.current_char is None or (self.current_char is not None and self.current_char.isspace()) or self.current_char in delim4:
|
| 101 |
-
tokens.append(Token(TT_RW_BUD, ident_str, line, pos.col))
|
| 102 |
-
continue
|
| 103 |
elif self.current_char is not None and not self.current_char.isspace() and self.current_char not in delim4 and self.current_char not in ALPHANUM:
|
| 104 |
-
# Strict rule: an illegal delimiter after the keyword is a lexical error (not a fallback to identifier).
|
| 105 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 106 |
-
self.advance()
|
| 107 |
-
continue
|
| 108 |
-
elif self.current_char == "n":
|
| 109 |
-
ident_str += self.current_char
|
| 110 |
-
self.advance()
|
| 111 |
-
if self.current_char == "d":
|
| 112 |
-
ident_str += self.current_char
|
| 113 |
-
self.advance()
|
| 114 |
-
if self.current_char == "l":
|
| 115 |
-
ident_str += self.current_char
|
| 116 |
-
self.advance()
|
| 117 |
-
if self.current_char == "e":
|
| 118 |
-
ident_str += self.current_char
|
| 119 |
-
self.advance()
|
| 120 |
-
if self.current_char is not None and self.current_char in space_delim:
|
| 121 |
-
tokens.append(Token(TT_RW_BUNDLE, ident_str, line, pos.col))
|
| 122 |
-
continue
|
| 123 |
elif self.current_char is not None and self.current_char not in space_delim and self.current_char not in ALPHANUM:
|
| 124 |
-
# Anything else (and not a letter that would extend it to an identifier) → illegal delimiter error.
|
| 125 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 126 |
-
self.advance()
|
| 127 |
-
continue
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
self.
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
self.
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
self.
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
self.
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
self.
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
self.
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
self.
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
self.
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
self.advance() # consume 'e'
|
| 157 |
-
# cultivate (for-loop) requires whitespace or delim4 ({':', '('}) next.
|
| 158 |
if self.current_char is None or (self.current_char is not None and self.current_char.isspace()) or self.current_char in delim4:
|
| 159 |
-
tokens.append(Token(TT_RW_CULTIVATE, ident_str, line, pos.col))
|
| 160 |
-
continue
|
| 161 |
elif self.current_char is not None and not self.current_char.isspace() and self.current_char not in delim4 and self.current_char not in ALPHANUM:
|
| 162 |
-
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 163 |
-
self.advance()
|
| 164 |
-
continue
|
| 165 |
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
self.
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
self.
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
self.
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
self.
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
self.advance() # consume 'y'
|
| 182 |
-
# empty is the void return type; whitespace is a valid delimiter.
|
| 183 |
if self.current_char is None or (self.current_char is not None and self.current_char.isspace()) or self.current_char in space_delim:
|
| 184 |
-
tokens.append(Token(TT_RW_EMPTY, ident_str, line, pos.col))
|
| 185 |
-
continue
|
| 186 |
elif self.current_char is not None and not self.current_char.isspace() and self.current_char not in space_delim and self.current_char not in ALPHANUM:
|
| 187 |
-
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 188 |
-
self.advance()
|
| 189 |
-
continue
|
| 190 |
|
| 191 |
-
# Letter F
|
| 192 |
elif self.current_char == "f":
|
| 193 |
ident_str += self.current_char
|
| 194 |
self.advance()
|
| 195 |
-
if self.current_char == "r":
|
| 196 |
ident_str += self.current_char
|
| 197 |
self.advance()
|
| 198 |
if self.current_char == "o":
|
|
@@ -205,10 +166,9 @@ class Lexer:
|
|
| 205 |
ident_str += self.current_char
|
| 206 |
self.advance()
|
| 207 |
if self.current_char is None or (self.current_char not in ALPHANUM and self.current_char != '_'):
|
| 208 |
-
tokens.append(Token(TT_BOOLLIT_FALSE, ident_str, line, pos.col))
|
| 209 |
continue
|
| 210 |
-
|
| 211 |
-
elif self.current_char == "e": # fertile
|
| 212 |
ident_str += self.current_char
|
| 213 |
self.advance()
|
| 214 |
if self.current_char == "r":
|
|
@@ -234,11 +194,10 @@ class Lexer:
|
|
| 234 |
self.advance()
|
| 235 |
continue
|
| 236 |
|
| 237 |
-
# Letter G
|
| 238 |
elif self.current_char == "g":
|
| 239 |
ident_str += self.current_char
|
| 240 |
self.advance()
|
| 241 |
-
if self.current_char == "r":
|
| 242 |
ident_str += self.current_char
|
| 243 |
self.advance()
|
| 244 |
if self.current_char == "o":
|
|
@@ -255,11 +214,10 @@ class Lexer:
|
|
| 255 |
self.advance()
|
| 256 |
continue
|
| 257 |
|
| 258 |
-
# Letter H
|
| 259 |
elif self.current_char == "h":
|
| 260 |
ident_str += self.current_char
|
| 261 |
self.advance()
|
| 262 |
-
if self.current_char == "a":
|
| 263 |
ident_str += self.current_char
|
| 264 |
self.advance()
|
| 265 |
if self.current_char == "r":
|
|
@@ -285,11 +243,10 @@ class Lexer:
|
|
| 285 |
self.advance()
|
| 286 |
continue
|
| 287 |
|
| 288 |
-
# Letter L
|
| 289 |
elif self.current_char == "l":
|
| 290 |
ident_str += self.current_char
|
| 291 |
self.advance()
|
| 292 |
-
if self.current_char == "e":
|
| 293 |
ident_str += self.current_char
|
| 294 |
self.advance()
|
| 295 |
if self.current_char == "a":
|
|
@@ -298,7 +255,6 @@ class Lexer:
|
|
| 298 |
if self.current_char == "f":
|
| 299 |
ident_str += self.current_char
|
| 300 |
self.advance()
|
| 301 |
-
# Leaf keyword recognized - emit token
|
| 302 |
if self.current_char is None or self.current_char in space_delim or self.current_char == ';':
|
| 303 |
tokens.append(Token(TT_RW_LEAF, ident_str, line, pos.col))
|
| 304 |
continue
|
|
@@ -306,11 +262,10 @@ class Lexer:
|
|
| 306 |
tokens.append(Token(TT_RW_LEAF, ident_str, line, pos.col))
|
| 307 |
continue
|
| 308 |
|
| 309 |
-
# Letter P
|
| 310 |
elif self.current_char == "p":
|
| 311 |
ident_str += self.current_char
|
| 312 |
self.advance()
|
| 313 |
-
if self.current_char == "l":
|
| 314 |
ident_str += self.current_char
|
| 315 |
self.advance()
|
| 316 |
if self.current_char == "a":
|
|
@@ -329,7 +284,7 @@ class Lexer:
|
|
| 329 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 330 |
self.advance()
|
| 331 |
continue
|
| 332 |
-
elif self.current_char == "o":
|
| 333 |
ident_str += self.current_char
|
| 334 |
self.advance()
|
| 335 |
if self.current_char == "l":
|
|
@@ -360,7 +315,7 @@ class Lexer:
|
|
| 360 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 361 |
self.advance()
|
| 362 |
continue
|
| 363 |
-
elif self.current_char == "r":
|
| 364 |
ident_str += self.current_char
|
| 365 |
self.advance()
|
| 366 |
if self.current_char == "u":
|
|
@@ -380,11 +335,10 @@ class Lexer:
|
|
| 380 |
self.advance()
|
| 381 |
continue
|
| 382 |
|
| 383 |
-
# Letter R
|
| 384 |
elif self.current_char == "r":
|
| 385 |
ident_str += self.current_char
|
| 386 |
self.advance()
|
| 387 |
-
if self.current_char == "e":
|
| 388 |
ident_str += self.current_char
|
| 389 |
self.advance()
|
| 390 |
if self.current_char == "c":
|
|
@@ -409,7 +363,7 @@ class Lexer:
|
|
| 409 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 410 |
self.advance()
|
| 411 |
continue
|
| 412 |
-
elif self.current_char == "o":
|
| 413 |
ident_str += self.current_char
|
| 414 |
self.advance()
|
| 415 |
if self.current_char == "o":
|
|
@@ -423,14 +377,13 @@ class Lexer:
|
|
| 423 |
continue
|
| 424 |
elif self.current_char is not None and not self.current_char.isspace() and self.current_char not in delim7 and self.current_char not in ALPHANUM:
|
| 425 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 426 |
-
self.advance()
|
| 427 |
continue
|
| 428 |
|
| 429 |
-
# Letter S
|
| 430 |
elif self.current_char == "s":
|
| 431 |
ident_str += self.current_char
|
| 432 |
self.advance()
|
| 433 |
-
if self.current_char == "e":
|
| 434 |
ident_str += self.current_char
|
| 435 |
self.advance()
|
| 436 |
if self.current_char == "e":
|
|
@@ -439,14 +392,13 @@ class Lexer:
|
|
| 439 |
if self.current_char == "d":
|
| 440 |
ident_str += self.current_char
|
| 441 |
self.advance()
|
| 442 |
-
# Seed keyword recognized - emit token
|
| 443 |
if self.current_char is None or self.current_char in space_delim or self.current_char == ';':
|
| 444 |
tokens.append(Token(TT_RW_SEED, ident_str, line, pos.col))
|
| 445 |
continue
|
| 446 |
elif self.current_char not in ALPHANUM:
|
| 447 |
tokens.append(Token(TT_RW_SEED, ident_str, line, pos.col))
|
| 448 |
continue
|
| 449 |
-
elif self.current_char == "k":
|
| 450 |
ident_str += self.current_char
|
| 451 |
self.advance()
|
| 452 |
if self.current_char == "i":
|
|
@@ -462,7 +414,7 @@ class Lexer:
|
|
| 462 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 463 |
self.advance()
|
| 464 |
continue
|
| 465 |
-
elif self.current_char == "o":
|
| 466 |
ident_str += self.current_char
|
| 467 |
self.advance()
|
| 468 |
if self.current_char == "i":
|
|
@@ -478,7 +430,7 @@ class Lexer:
|
|
| 478 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 479 |
self.advance()
|
| 480 |
continue
|
| 481 |
-
elif self.current_char == "p":
|
| 482 |
ident_str += self.current_char
|
| 483 |
self.advance()
|
| 484 |
if self.current_char == "r":
|
|
@@ -500,7 +452,7 @@ class Lexer:
|
|
| 500 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 501 |
self.advance()
|
| 502 |
continue
|
| 503 |
-
elif self.current_char == "u":
|
| 504 |
ident_str += self.current_char
|
| 505 |
self.advance()
|
| 506 |
if self.current_char == "n":
|
|
@@ -522,18 +474,17 @@ class Lexer:
|
|
| 522 |
ident_str += self.current_char
|
| 523 |
self.advance()
|
| 524 |
if self.current_char is None or self.current_char in delim23 or self.current_char in space_delim:
|
| 525 |
-
tokens.append(Token(TT_BOOLLIT_TRUE, ident_str, line, pos.col))
|
| 526 |
continue
|
| 527 |
elif self.current_char not in ALPHANUM:
|
| 528 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 529 |
self.advance()
|
| 530 |
continue
|
| 531 |
|
| 532 |
-
# Letter T
|
| 533 |
elif self.current_char == "t":
|
| 534 |
ident_str += self.current_char
|
| 535 |
self.advance()
|
| 536 |
-
if self.current_char == "e":
|
| 537 |
ident_str += self.current_char
|
| 538 |
self.advance()
|
| 539 |
if self.current_char == "n":
|
|
@@ -549,7 +500,7 @@ class Lexer:
|
|
| 549 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 550 |
self.advance()
|
| 551 |
continue
|
| 552 |
-
elif self.current_char == "r":
|
| 553 |
ident_str += self.current_char
|
| 554 |
self.advance()
|
| 555 |
if self.current_char == "e":
|
|
@@ -558,7 +509,6 @@ class Lexer:
|
|
| 558 |
if self.current_char == "e":
|
| 559 |
ident_str += self.current_char
|
| 560 |
self.advance()
|
| 561 |
-
# Tree keyword recognized - emit token
|
| 562 |
if self.current_char is None or self.current_char in space_delim or self.current_char == ';':
|
| 563 |
tokens.append(Token(TT_RW_TREE, ident_str, line, pos.col))
|
| 564 |
continue
|
|
@@ -566,11 +516,10 @@ class Lexer:
|
|
| 566 |
tokens.append(Token(TT_RW_TREE, ident_str, line, pos.col))
|
| 567 |
continue
|
| 568 |
|
| 569 |
-
# Letter V
|
| 570 |
elif self.current_char == "v":
|
| 571 |
ident_str += self.current_char
|
| 572 |
self.advance()
|
| 573 |
-
if self.current_char == "i":
|
| 574 |
ident_str += self.current_char
|
| 575 |
self.advance()
|
| 576 |
if self.current_char == "n":
|
|
@@ -585,7 +534,7 @@ class Lexer:
|
|
| 585 |
elif self.current_char not in ALPHANUM:
|
| 586 |
tokens.append(Token(TT_RW_VINE, ident_str, line, pos.col))
|
| 587 |
continue
|
| 588 |
-
elif self.current_char == "a":
|
| 589 |
ident_str += self.current_char
|
| 590 |
self.advance()
|
| 591 |
if self.current_char == "r":
|
|
@@ -603,7 +552,6 @@ class Lexer:
|
|
| 603 |
if self.current_char == "y":
|
| 604 |
ident_str += self.current_char
|
| 605 |
self.advance()
|
| 606 |
-
# variety introduces a case literal, written after whitespace.
|
| 607 |
if self.current_char is None or self.current_char in space_delim:
|
| 608 |
tokens.append(Token(TT_RW_VARIETY, ident_str, line, pos.col))
|
| 609 |
continue
|
|
@@ -611,13 +559,11 @@ class Lexer:
|
|
| 611 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 612 |
self.advance()
|
| 613 |
continue
|
| 614 |
-
# If followed by alphanum, continue to identifier parsing.
|
| 615 |
|
| 616 |
-
# Letter W
|
| 617 |
elif self.current_char == "w":
|
| 618 |
ident_str += self.current_char
|
| 619 |
self.advance()
|
| 620 |
-
if self.current_char == "a":
|
| 621 |
ident_str += self.current_char
|
| 622 |
self.advance()
|
| 623 |
if self.current_char == "t":
|
|
@@ -636,7 +582,7 @@ class Lexer:
|
|
| 636 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 637 |
self.advance()
|
| 638 |
continue
|
| 639 |
-
elif self.current_char == "i":
|
| 640 |
ident_str += self.current_char
|
| 641 |
self.advance()
|
| 642 |
if self.current_char == "t":
|
|
@@ -659,26 +605,19 @@ class Lexer:
|
|
| 659 |
self.advance()
|
| 660 |
continue
|
| 661 |
|
| 662 |
-
|
| 663 |
-
maxIdentifierLength = 15 # Changed from 20 to 15
|
| 664 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 665 |
ident_str += self.current_char
|
| 666 |
self.advance()
|
| 667 |
|
| 668 |
-
# Check if identifier exceeds max length
|
| 669 |
if len(ident_str) > maxIdentifierLength:
|
| 670 |
-
# Process in chunks of 15: each 15-char chunk is an error
|
| 671 |
-
# Only the final remaining chunk (≤14 chars) is valid and added to tokens
|
| 672 |
i = 0
|
| 673 |
remaining = None
|
| 674 |
while i < len(ident_str):
|
| 675 |
-
# Check if there are at least 15 characters remaining
|
| 676 |
if i + 15 <= len(ident_str):
|
| 677 |
-
# 15-char chunk: generate error, don't add token
|
| 678 |
errors.append(LexicalError(pos, f"Identifier exceeds maximum length of {maxIdentifierLength} characters"))
|
| 679 |
i += 15
|
| 680 |
else:
|
| 681 |
-
# Final chunk with 14 or fewer characters: valid token
|
| 682 |
remaining = ident_str[i:]
|
| 683 |
if self.current_char is None or self.current_char in idf_delim:
|
| 684 |
tokens.append(Token(TT_IDENTIFIER, remaining, line, pos.col))
|
|
@@ -686,14 +625,11 @@ class Lexer:
|
|
| 686 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after identifier"))
|
| 687 |
break
|
| 688 |
if remaining is None:
|
| 689 |
-
# Length was exact multiple of 15 — all chars consumed as errors
|
| 690 |
-
# Still create a token with the last chunk
|
| 691 |
last_chunk = ident_str[i - 15:] if i >= 15 else ident_str
|
| 692 |
if self.current_char is None or self.current_char in idf_delim:
|
| 693 |
tokens.append(Token(TT_IDENTIFIER, last_chunk, line, pos.col))
|
| 694 |
continue
|
| 695 |
else:
|
| 696 |
-
# Identifier is within max length
|
| 697 |
if self.current_char is None or self.current_char in idf_delim:
|
| 698 |
tokens.append(Token(TT_IDENTIFIER, ident_str, line, pos.col))
|
| 699 |
continue
|
|
@@ -708,7 +644,6 @@ class Lexer:
|
|
| 708 |
pos = self.pos.copy()
|
| 709 |
self.advance()
|
| 710 |
if self.current_char == "-":
|
| 711 |
-
# Tokenize -- as DECREMENT operator
|
| 712 |
ident_str += self.current_char
|
| 713 |
self.advance()
|
| 714 |
if self.current_char is not None and self.current_char not in delim25:
|
|
@@ -722,22 +657,19 @@ class Lexer:
|
|
| 722 |
self.advance()
|
| 723 |
tokens.append(Token(TT_MINUSEQ, ident_str, line, pos.col))
|
| 724 |
continue
|
| 725 |
-
# Check for valid delimiter after -
|
| 726 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ' + '\t' + '\n'):
|
| 727 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 728 |
-
self.advance()
|
| 729 |
-
continue
|
| 730 |
tokens.append(Token(TT_MINUS, ident_str, line, pos.col))
|
| 731 |
continue
|
| 732 |
|
| 733 |
-
elif self.current_char == "~":
|
| 734 |
ident_str = self.current_char
|
| 735 |
pos = self.pos.copy()
|
| 736 |
self.advance()
|
| 737 |
|
| 738 |
-
# If ~ is directly followed by a digit, consume the number as a negative literal
|
| 739 |
if self.current_char is not None and self.current_char in ZERODIGIT:
|
| 740 |
-
# Read integer digits
|
| 741 |
num_str = ""
|
| 742 |
integer_digit_count = 0
|
| 743 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
|
@@ -745,7 +677,6 @@ class Lexer:
|
|
| 745 |
num_str += self.current_char
|
| 746 |
self.advance()
|
| 747 |
|
| 748 |
-
# Check for decimal point (negative double)
|
| 749 |
if self.current_char == ".":
|
| 750 |
num_str += self.current_char
|
| 751 |
self.advance()
|
|
@@ -760,7 +691,6 @@ class Lexer:
|
|
| 760 |
if fractional_digit_count > 8:
|
| 761 |
errors.append(LexicalError(pos, f"Fractional part exceeds maximum of 8 digits"))
|
| 762 |
continue
|
| 763 |
-
# Format the double
|
| 764 |
parts = num_str.split(".")
|
| 765 |
integer_part = parts[0].lstrip("0") or "0"
|
| 766 |
fractional_part = (parts[1] if len(parts) > 1 else "").rstrip("0") or "0"
|
|
@@ -772,7 +702,6 @@ class Lexer:
|
|
| 772 |
tokens.append(Token(TT_DOUBLELIT, ident_str, line, pos.col))
|
| 773 |
continue
|
| 774 |
else:
|
| 775 |
-
# Negative integer
|
| 776 |
if integer_digit_count > 8:
|
| 777 |
errors.append(LexicalError(pos, f"Integer exceeds maximum of 8 digits"))
|
| 778 |
continue
|
|
@@ -781,8 +710,6 @@ class Lexer:
|
|
| 781 |
tokens.append(Token(TT_INTEGERLIT, ident_str, line, pos.col))
|
| 782 |
continue
|
| 783 |
|
| 784 |
-
# ~ not followed by a digit: emit as the arithmetic-negation operator.
|
| 785 |
-
# Permit a directly grouped operand, such as ~(x + 1).
|
| 786 |
elif self.current_char is None or self.current_char in ALPHANUM + '( \t\n':
|
| 787 |
tokens.append(Token(TT_NEGATIVE, ident_str, line, pos.col))
|
| 788 |
continue
|
|
@@ -798,7 +725,6 @@ class Lexer:
|
|
| 798 |
if self.current_char == "=":
|
| 799 |
ident_str += self.current_char
|
| 800 |
self.advance()
|
| 801 |
-
# Check for consecutive comparison operators
|
| 802 |
if len(tokens) > 0 and tokens[-1].type in [TT_GT, TT_LT, TT_EQTO, TT_NOTEQ, TT_GTEQ, TT_LTEQ]:
|
| 803 |
errors.append(LexicalError(pos, f"Consecutive operators '{tokens[-1].value} {ident_str}' are not allowed"))
|
| 804 |
continue
|
|
@@ -817,11 +743,10 @@ class Lexer:
|
|
| 817 |
self.advance()
|
| 818 |
tokens.append(Token(TT_MODEQ, ident_str, line, pos.col))
|
| 819 |
continue
|
| 820 |
-
# Check for valid delimiter after %
|
| 821 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ;' + '\t' + '\n'):
|
| 822 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 823 |
-
self.advance()
|
| 824 |
-
continue
|
| 825 |
tokens.append(Token(TT_MOD, ident_str, line, pos.col))
|
| 826 |
continue
|
| 827 |
|
|
@@ -835,7 +760,6 @@ class Lexer:
|
|
| 835 |
tokens.append(Token(TT_AND, ident_str, line, pos.col))
|
| 836 |
continue
|
| 837 |
else:
|
| 838 |
-
# Emit single & token for parser to detect
|
| 839 |
tokens.append(Token(TT_SINGLE_AND, ident_str, line, pos.col))
|
| 840 |
continue
|
| 841 |
|
|
@@ -860,8 +784,6 @@ class Lexer:
|
|
| 860 |
if self.current_char == "*":
|
| 861 |
ident_str += self.current_char
|
| 862 |
self.advance()
|
| 863 |
-
# Maximal munch: '**=' must be one token (exponent-assign),
|
| 864 |
-
# not two ('**' then '='), or downstream parsing breaks.
|
| 865 |
if self.current_char == "=":
|
| 866 |
ident_str += self.current_char
|
| 867 |
self.advance()
|
|
@@ -874,18 +796,16 @@ class Lexer:
|
|
| 874 |
self.advance()
|
| 875 |
tokens.append(Token(TT_MULTIEQ, ident_str, line, pos.col))
|
| 876 |
continue
|
| 877 |
-
# Check for valid delimiter after *
|
| 878 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ' + '\t' + '\n'):
|
| 879 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 880 |
-
self.advance()
|
| 881 |
-
continue
|
| 882 |
tokens.append(Token(TT_MUL, ident_str, line, pos.col))
|
| 883 |
continue
|
| 884 |
|
| 885 |
elif self.current_char == ",":
|
| 886 |
ident_str = self.current_char
|
| 887 |
pos = self.pos.copy()
|
| 888 |
-
# Check for consecutive commas
|
| 889 |
if len(tokens) > 0 and tokens[-1].type == TT_COMMA:
|
| 890 |
errors.append(LexicalError(pos, f"Invalid delimiters ','"))
|
| 891 |
self.advance()
|
|
@@ -894,11 +814,6 @@ class Lexer:
|
|
| 894 |
tokens.append(Token(TT_COMMA, ident_str, line, pos.col))
|
| 895 |
continue
|
| 896 |
|
| 897 |
-
# This entire block is for a token that isn't really a token.
|
| 898 |
-
# The logic for escape sequences is handled *inside* the string and char
|
| 899 |
-
# literal parsers. This block should be removed.
|
| 900 |
-
# elif self.current_char == "\\":
|
| 901 |
-
# ...
|
| 902 |
|
| 903 |
elif self.current_char == ";":
|
| 904 |
ident_str = self.current_char
|
|
@@ -945,7 +860,6 @@ class Lexer:
|
|
| 945 |
tokens.append(Token(TT_OR, ident_str, line, pos.col))
|
| 946 |
continue
|
| 947 |
else:
|
| 948 |
-
# Emit single | token for parser to detect
|
| 949 |
tokens.append(Token(TT_SINGLE_OR, ident_str, line, pos.col))
|
| 950 |
continue
|
| 951 |
|
|
@@ -954,7 +868,6 @@ class Lexer:
|
|
| 954 |
pos = self.pos.copy()
|
| 955 |
self.advance()
|
| 956 |
if self.current_char == "+":
|
| 957 |
-
# Tokenize ++ as INCREMENT operator
|
| 958 |
ident_str += self.current_char
|
| 959 |
self.advance()
|
| 960 |
if self.current_char is not None and self.current_char not in delim25:
|
|
@@ -972,11 +885,10 @@ class Lexer:
|
|
| 972 |
continue
|
| 973 |
tokens.append(Token(TT_PLUSEQ, ident_str, line, pos.col))
|
| 974 |
continue
|
| 975 |
-
# Check for valid delimiter after +
|
| 976 |
if self.current_char is not None and self.current_char not in delim24:
|
| 977 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 978 |
-
self.advance()
|
| 979 |
-
continue
|
| 980 |
tokens.append(Token(TT_PLUS, ident_str, line, pos.col))
|
| 981 |
continue
|
| 982 |
|
|
@@ -992,31 +904,23 @@ class Lexer:
|
|
| 992 |
tokens.append(Token(TT_LT, ident_str, line, pos.col))
|
| 993 |
continue
|
| 994 |
|
| 995 |
-
# =====================================================================
|
| 996 |
-
# EQUALS OPERATOR: = (assignment) or == (equality comparison)
|
| 997 |
-
# =====================================================================
|
| 998 |
elif self.current_char == "=":
|
| 999 |
ident_str = self.current_char
|
| 1000 |
pos = self.pos.copy()
|
| 1001 |
self.advance()
|
| 1002 |
|
| 1003 |
-
if self.current_char == "=":
|
| 1004 |
ident_str += self.current_char
|
| 1005 |
self.advance()
|
| 1006 |
|
| 1007 |
-
# Don't check for third '=' - let it tokenize as separate '=' token
|
| 1008 |
-
# Parser will detect the pattern '==' followed by '='
|
| 1009 |
|
| 1010 |
-
# Prevent consecutive comparison operators (e.g., "< ==" or "> ==")
|
| 1011 |
if len(tokens) > 0 and tokens[-1].type in [TT_GT, TT_LT, TT_EQTO, TT_NOTEQ, TT_GTEQ, TT_LTEQ]:
|
| 1012 |
errors.append(LexicalError(pos, f"invalid delimiters '{tokens[-1].value} {ident_str}' are not allowed"))
|
| 1013 |
continue
|
| 1014 |
|
| 1015 |
-
# Valid '==' (equality comparison operator)
|
| 1016 |
tokens.append(Token(TT_EQTO, ident_str, line, pos.col))
|
| 1017 |
continue
|
| 1018 |
|
| 1019 |
-
# Single '=' (assignment operator)
|
| 1020 |
tokens.append(Token(TT_EQ, ident_str, line, pos.col))
|
| 1021 |
continue
|
| 1022 |
|
|
@@ -1027,13 +931,11 @@ class Lexer:
|
|
| 1027 |
if self.current_char == "=":
|
| 1028 |
ident_str += self.current_char
|
| 1029 |
self.advance()
|
| 1030 |
-
# Check for consecutive comparison operators
|
| 1031 |
if len(tokens) > 0 and tokens[-1].type in [TT_GT, TT_LT, TT_EQTO, TT_NOTEQ, TT_GTEQ, TT_LTEQ]:
|
| 1032 |
errors.append(LexicalError(pos, f"invalid delimiters '{tokens[-1].value} {ident_str}' are not allowed"))
|
| 1033 |
continue
|
| 1034 |
tokens.append(Token(TT_GTEQ, ident_str, line, pos.col))
|
| 1035 |
continue
|
| 1036 |
-
# Check for consecutive comparison operators
|
| 1037 |
if len(tokens) > 0 and tokens[-1].type in [TT_GT, TT_LT, TT_EQTO, TT_NOTEQ, TT_GTEQ, TT_LTEQ]:
|
| 1038 |
errors.append(LexicalError(pos, f"invalid delimiters '{tokens[-1].value} {ident_str}' are not allowed"))
|
| 1039 |
continue
|
|
@@ -1070,45 +972,36 @@ class Lexer:
|
|
| 1070 |
self.advance()
|
| 1071 |
continue
|
| 1072 |
|
| 1073 |
-
# =====================================================================
|
| 1074 |
-
# FORWARD SLASH: / (division), // (comment), /* */ (multi-line comment), /= (divide-assign)
|
| 1075 |
-
# =====================================================================
|
| 1076 |
elif self.current_char == "/":
|
| 1077 |
ident_str = self.current_char
|
| 1078 |
pos = self.pos.copy()
|
| 1079 |
self.advance()
|
| 1080 |
|
| 1081 |
-
if self.current_char == "/":
|
| 1082 |
ident_str += self.current_char
|
| 1083 |
self.advance()
|
| 1084 |
-
# Consume all characters until end of line or file
|
| 1085 |
while self.current_char is not None and self.current_char != "\n":
|
| 1086 |
ident_str += self.current_char
|
| 1087 |
self.advance()
|
| 1088 |
-
# Comments are NOT added to tokens (they are skipped)
|
| 1089 |
continue
|
| 1090 |
|
| 1091 |
-
elif self.current_char == "*":
|
| 1092 |
ident_str += self.current_char
|
| 1093 |
self.advance()
|
| 1094 |
found_close = False
|
| 1095 |
-
# Consume all characters until closing */
|
| 1096 |
while self.current_char is not None:
|
| 1097 |
-
# Check for closing */ sequence
|
| 1098 |
if self.current_char == "*" and self.pos.index + 1 < len(self.source_code) and self.source_code[self.pos.index + 1] == "/":
|
| 1099 |
ident_str += "*/"
|
| 1100 |
-
self.advance()
|
| 1101 |
-
self.advance()
|
| 1102 |
found_close = True
|
| 1103 |
break
|
| 1104 |
else:
|
| 1105 |
ident_str += self.current_char
|
| 1106 |
-
if self.current_char == "\n":
|
| 1107 |
line += 1
|
| 1108 |
self.advance()
|
| 1109 |
-
# Comments are NOT added to tokens (they are skipped)
|
| 1110 |
|
| 1111 |
-
# Error if comment was never closed
|
| 1112 |
if not found_close:
|
| 1113 |
errors.append(LexicalError(pos, f"Missing closing '*/' after '{ident_str}'"))
|
| 1114 |
continue
|
|
@@ -1119,11 +1012,10 @@ class Lexer:
|
|
| 1119 |
tokens.append(Token(TT_DIVEQ, ident_str, line, pos.col))
|
| 1120 |
continue
|
| 1121 |
else:
|
| 1122 |
-
# Check for valid delimiter after /
|
| 1123 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ' + '\t' + '\n'):
|
| 1124 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 1125 |
-
self.advance()
|
| 1126 |
-
continue
|
| 1127 |
tokens.append(Token(TT_DIV, ident_str, line, pos.col))
|
| 1128 |
continue
|
| 1129 |
|
|
@@ -1142,7 +1034,6 @@ class Lexer:
|
|
| 1142 |
if len(fractional_part + self.current_char) > 8:
|
| 1143 |
errors.append(LexicalError(pos, f"'{ident_str}' exceeds maximum number of decimal places"))
|
| 1144 |
overflow = True
|
| 1145 |
-
# Consume remaining digits to avoid cascading errors
|
| 1146 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
| 1147 |
self.advance()
|
| 1148 |
break
|
|
@@ -1169,142 +1060,104 @@ class Lexer:
|
|
| 1169 |
tokens.append(Token(TT_COLON, ident_str, line, pos.col))
|
| 1170 |
continue
|
| 1171 |
|
| 1172 |
-
# =====================================================================
|
| 1173 |
-
#
|
| 1174 |
-
#
|
| 1175 |
-
# =====================================================================
|
| 1176 |
elif self.current_char in ZERODIGIT:
|
| 1177 |
-
dot_count = 0
|
| 1178 |
-
ident_str = ""
|
| 1179 |
-
pos = self.pos.copy()
|
| 1180 |
-
integer_digit_count = 0
|
| 1181 |
-
fractional_digit_count = 0
|
| 1182 |
-
has_e = False
|
| 1183 |
|
| 1184 |
|
| 1185 |
-
# Read all digits before decimal point
|
| 1186 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
| 1187 |
integer_digit_count += 1
|
| 1188 |
ident_str += self.current_char
|
| 1189 |
self.advance()
|
| 1190 |
|
| 1191 |
-
# Step 2: Check for decimal point (converts to double/float)
|
| 1192 |
if self.current_char == ".":
|
| 1193 |
-
# For decimals, check if integer part exceeds 15 digits
|
| 1194 |
if integer_digit_count > 15:
|
| 1195 |
-
# Process integer part in chunks of 15: each 15-digit chunk is an error
|
| 1196 |
-
# Only the final remaining chunk (≤15 digits) continues to decimal processing
|
| 1197 |
integer_part = ident_str
|
| 1198 |
i = 0
|
| 1199 |
while i < len(integer_part):
|
| 1200 |
if i + 15 < len(integer_part):
|
| 1201 |
-
# More than 15 digits remain: generate error for this chunk
|
| 1202 |
errors.append(LexicalError(pos, f"Integer part of decimal exceeds maximum of 15 digits"))
|
| 1203 |
i += 15
|
| 1204 |
else:
|
| 1205 |
-
# Final chunk with 15 or fewer digits — valid
|
| 1206 |
ident_str = integer_part[i:]
|
| 1207 |
break
|
| 1208 |
else:
|
| 1209 |
-
# All digits consumed as errors, no valid remainder
|
| 1210 |
ident_str = "0"
|
| 1211 |
-
dot_count = 1
|
| 1212 |
ident_str += self.current_char
|
| 1213 |
self.advance()
|
| 1214 |
|
| 1215 |
-
# After decimal point, must have at least one digit (e.g., "3." is invalid)
|
| 1216 |
if self.current_char is None or self.current_char not in ZERODIGIT:
|
| 1217 |
errors.append(LexicalError(pos, f"Invalid number '{ident_str}': decimal point must be followed by digits"))
|
| 1218 |
continue
|
| 1219 |
|
| 1220 |
-
# Read fractional part (digits after decimal point)
|
| 1221 |
fractional_part = ""
|
| 1222 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
| 1223 |
fractional_digit_count += 1
|
| 1224 |
fractional_part += self.current_char
|
| 1225 |
self.advance()
|
| 1226 |
|
| 1227 |
-
# Check if fractional part exceeds 8 digits
|
| 1228 |
if fractional_digit_count > 8:
|
| 1229 |
-
# Process in chunks of 8: each 8-digit chunk is an error
|
| 1230 |
-
# Only the final remaining chunk (≤8 digits) is added to the number
|
| 1231 |
i = 0
|
| 1232 |
final_fractional = ""
|
| 1233 |
while i < len(fractional_part):
|
| 1234 |
if i + 8 < len(fractional_part):
|
| 1235 |
-
# More than 8 digits remain: generate error for this chunk
|
| 1236 |
errors.append(LexicalError(pos, f"Fractional part exceeds maximum of 8 digits"))
|
| 1237 |
i += 8
|
| 1238 |
else:
|
| 1239 |
-
# Final chunk with 8 or fewer digits — valid
|
| 1240 |
final_fractional = fractional_part[i:]
|
| 1241 |
break
|
| 1242 |
ident_str += final_fractional
|
| 1243 |
else:
|
| 1244 |
ident_str += fractional_part
|
| 1245 |
|
| 1246 |
-
# Handle integers longer than 8 digits
|
| 1247 |
-
# Process in chunks of 8: each chunk beyond the valid last ≤8 digits is an error
|
| 1248 |
if dot_count == 0 and integer_digit_count > 8:
|
| 1249 |
i = 0
|
| 1250 |
remaining = None
|
| 1251 |
while i < len(ident_str):
|
| 1252 |
-
# Check if more than 8 characters remain
|
| 1253 |
if i + 8 < len(ident_str):
|
| 1254 |
-
# More than 8 digits remain: generate error for this chunk
|
| 1255 |
errors.append(LexicalError(pos, f"Integer exceeds maximum of 8 digits"))
|
| 1256 |
i += 8
|
| 1257 |
else:
|
| 1258 |
-
# Final chunk with 8 or fewer digits: valid token
|
| 1259 |
remaining = ident_str[i:]
|
| 1260 |
-
# Strip leading zeros for the remaining valid portion
|
| 1261 |
remaining = remaining.lstrip("0") or "0"
|
| 1262 |
tokens.append(Token(TT_INTEGERLIT, remaining, line, pos.col))
|
| 1263 |
break
|
| 1264 |
if remaining is None:
|
| 1265 |
-
# Safety fallback (should not happen with the corrected condition)
|
| 1266 |
tokens.append(Token(TT_INTEGERLIT, "0", line, pos.col))
|
| 1267 |
continue
|
| 1268 |
|
| 1269 |
if fractional_digit_count > 8:
|
| 1270 |
-
# Error already reported and fractional part already truncated above.
|
| 1271 |
-
# Fall through to create a token with the truncated valid portion.
|
| 1272 |
pass
|
| 1273 |
|
| 1274 |
-
# Step 3: Check for scientific notation (e.g., 1.5e10, 2.3e-5)
|
| 1275 |
-
# Only valid for doubles (must have decimal point)
|
| 1276 |
if self.current_char is not None and self.current_char in 'eE' and dot_count == 1:
|
| 1277 |
has_e = True
|
| 1278 |
ident_str += self.current_char
|
| 1279 |
self.advance()
|
| 1280 |
|
| 1281 |
-
# Handle optional sign after 'e' (e.g., 1.2e-5 or 1.2e+5)
|
| 1282 |
if self.current_char in '+-':
|
| 1283 |
ident_str += self.current_char
|
| 1284 |
self.advance()
|
| 1285 |
|
| 1286 |
-
# Must have digits after 'e' (e.g., "1.5e" is invalid)
|
| 1287 |
if self.current_char is None or self.current_char not in ZERODIGIT:
|
| 1288 |
errors.append(LexicalError(pos, f"Invalid scientific notation: 'e' must be followed by digits."))
|
| 1289 |
continue
|
| 1290 |
|
| 1291 |
-
# Read exponent digits
|
| 1292 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
| 1293 |
ident_str += self.current_char
|
| 1294 |
self.advance()
|
| 1295 |
|
| 1296 |
if dot_count == 0 and not has_e:
|
| 1297 |
-
# Integer literal - type checking is done by the parser (SYNTAX error)
|
| 1298 |
|
| 1299 |
-
# Check for valid delimiter after integer
|
| 1300 |
if self.current_char is not None and self.current_char not in whlnum_delim:
|
| 1301 |
-
# Strip leading zeros and tokenize the valid integer first
|
| 1302 |
valid_int = ident_str.lstrip("0") or "0"
|
| 1303 |
tokens.append(Token(TT_INTEGERLIT, valid_int, line, pos.col))
|
| 1304 |
|
| 1305 |
-
# Check if followed by letter (not underscore or digit)
|
| 1306 |
if self.current_char in ALPHA:
|
| 1307 |
-
# Peek ahead to see if it's a reserved word
|
| 1308 |
temp_str = ''
|
| 1309 |
temp_pos = self.pos.copy()
|
| 1310 |
temp_char = self.current_char
|
|
@@ -1313,7 +1166,6 @@ class Lexer:
|
|
| 1313 |
temp_pos.advance(temp_char)
|
| 1314 |
temp_char = self.source_code[temp_pos.index] if temp_pos.index < len(self.source_code) else None
|
| 1315 |
|
| 1316 |
-
# Check if it's a reserved word
|
| 1317 |
reserved_words = {'water', 'plant', 'seed', 'leaf', 'branch', 'tree', 'spring', 'wither', 'bud',
|
| 1318 |
'harvest', 'grow', 'cultivate', 'tend', 'empty', 'prune', 'skip', 'reclaim',
|
| 1319 |
'root', 'pollinate', 'variety', 'fertile', 'soil', 'bundle', 'string', 'vine', 'sunshine', 'frost'}
|
|
@@ -1323,14 +1175,11 @@ class Lexer:
|
|
| 1323 |
else:
|
| 1324 |
errors.append(LexicalError(pos, f"Identifiers cannot start with a number: '{ident_str}{self.current_char}...'"))
|
| 1325 |
|
| 1326 |
-
# Consume the rest of the invalid identifier
|
| 1327 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 1328 |
self.advance()
|
| 1329 |
elif self.current_char == '_':
|
| 1330 |
-
# Check if underscore is followed by letters (reserved word or identifier)
|
| 1331 |
temp_index = self.pos.index + 1
|
| 1332 |
if temp_index < len(self.source_code) and self.source_code[temp_index] in ALPHA:
|
| 1333 |
-
# Peek ahead to see if it's a reserved word
|
| 1334 |
temp_str = ''
|
| 1335 |
while temp_index < len(self.source_code) and self.source_code[temp_index] in ALPHANUM:
|
| 1336 |
temp_str += self.source_code[temp_index]
|
|
@@ -1345,69 +1194,56 @@ class Lexer:
|
|
| 1345 |
else:
|
| 1346 |
errors.append(LexicalError(pos, f"Identifiers cannot start with a number: '{ident_str}_...'"))
|
| 1347 |
|
| 1348 |
-
# Consume underscore and the rest
|
| 1349 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 1350 |
self.advance()
|
| 1351 |
else:
|
| 1352 |
-
# Underscore followed by digits - numeric separator error
|
| 1353 |
errors.append(LexicalError(pos, f"Underscore cannot be used in numeric literals"))
|
| 1354 |
-
# Consume underscore and any following digits/underscores/letters
|
| 1355 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 1356 |
self.advance()
|
| 1357 |
else:
|
| 1358 |
-
# Invalid delimiter error already added when integer was tokenized
|
| 1359 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 1360 |
-
# Don't consume the character - let the main loop handle it
|
| 1361 |
continue
|
| 1362 |
|
| 1363 |
ident_str = ident_str.lstrip("0") or "0"
|
| 1364 |
tokens.append(Token(TT_INTEGERLIT, ident_str, line, pos.col))
|
| 1365 |
continue
|
| 1366 |
|
| 1367 |
-
else:
|
| 1368 |
-
# Check for valid delimiter after double
|
| 1369 |
if self.current_char is not None and self.current_char not in decim_delim:
|
| 1370 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 1371 |
self.advance()
|
| 1372 |
continue
|
| 1373 |
-
if not has_e:
|
| 1374 |
parts = ident_str.split(".")
|
| 1375 |
integer_part = parts[0].lstrip("0") or "0"
|
| 1376 |
fractional_part = (parts[1] if len(parts) > 1 else "").rstrip("0") or "0"
|
| 1377 |
if fractional_part == "0":
|
| 1378 |
-
ident_str = f"{integer_part}.0"
|
| 1379 |
else:
|
| 1380 |
ident_str = f"{integer_part}.{fractional_part}"
|
| 1381 |
|
| 1382 |
tokens.append(Token(TT_DOUBLELIT, ident_str, line, pos.col))
|
| 1383 |
continue
|
| 1384 |
|
| 1385 |
-
# =====================================================================
|
| 1386 |
-
# STRING LITERAL PARSING: "hello world"
|
| 1387 |
-
# Supports escape sequences: \n (newline), \t (tab), \{ \} (literal braces)
|
| 1388 |
-
# =====================================================================
|
| 1389 |
elif self.current_char == '"':
|
| 1390 |
-
string = ''
|
| 1391 |
-
pos = self.pos.copy()
|
| 1392 |
-
escape_character = False
|
| 1393 |
-
string += self.current_char
|
| 1394 |
self.advance()
|
| 1395 |
|
| 1396 |
-
# Supported escape sequences
|
| 1397 |
escape_characters = {
|
| 1398 |
-
'n': '\n',
|
| 1399 |
-
't': '\t',
|
| 1400 |
-
'{': '\\{',
|
| 1401 |
-
'}': '\\}',
|
| 1402 |
-
'"': '"',
|
| 1403 |
-
'\\': '\\',
|
| 1404 |
}
|
| 1405 |
|
| 1406 |
has_string_error = False
|
| 1407 |
-
# Read until closing quote (or end of file)
|
| 1408 |
while self.current_char is not None and (self.current_char != '"' or escape_character):
|
| 1409 |
if escape_character:
|
| 1410 |
-
# Process escape sequence (e.g., \n becomes newline)
|
| 1411 |
if self.current_char in escape_characters:
|
| 1412 |
string += escape_characters[self.current_char]
|
| 1413 |
else:
|
|
@@ -1424,7 +1260,6 @@ class Lexer:
|
|
| 1424 |
self.advance()
|
| 1425 |
|
| 1426 |
if has_string_error:
|
| 1427 |
-
# Consume rest of string to avoid cascading errors
|
| 1428 |
while self.current_char is not None and self.current_char != '"':
|
| 1429 |
self.advance()
|
| 1430 |
if self.current_char == '"':
|
|
@@ -1435,7 +1270,6 @@ class Lexer:
|
|
| 1435 |
string += self.current_char
|
| 1436 |
self.advance()
|
| 1437 |
else:
|
| 1438 |
-
# Missing closing quote - report error and skip (don't create token)
|
| 1439 |
errors.append(LexicalError(pos, f"Missing closing '\"' for string literal"))
|
| 1440 |
continue
|
| 1441 |
|
|
@@ -1444,48 +1278,36 @@ class Lexer:
|
|
| 1444 |
self.advance()
|
| 1445 |
continue
|
| 1446 |
|
| 1447 |
-
# Token stores the REAL characters (like C compilers do).
|
| 1448 |
-
# Display escaping is handled separately in the server.
|
| 1449 |
tokens.append(Token(TT_STRINGLIT, string, line, pos.col))
|
| 1450 |
continue
|
| 1451 |
|
| 1452 |
-
# =====================================================================
|
| 1453 |
-
# CHARACTER LITERAL PARSING: 'a', '\n', '\t'
|
| 1454 |
-
# Single character enclosed in single quotes
|
| 1455 |
-
# =====================================================================
|
| 1456 |
elif self.current_char == "'":
|
| 1457 |
-
string = ''
|
| 1458 |
-
char = ''
|
| 1459 |
pos = self.pos.copy()
|
| 1460 |
-
string += self.current_char
|
| 1461 |
self.advance()
|
| 1462 |
-
has_error = False
|
| 1463 |
|
| 1464 |
-
# Skip leading whitespace
|
| 1465 |
while self.current_char is not None and self.current_char in ' \t':
|
| 1466 |
string += self.current_char
|
| 1467 |
self.advance()
|
| 1468 |
|
| 1469 |
-
# Read until closing quote (or end of file/line)
|
| 1470 |
while self.current_char is not None and self.current_char != "'":
|
| 1471 |
-
if self.current_char == '\n':
|
| 1472 |
break
|
| 1473 |
-
elif self.current_char == '\\':
|
| 1474 |
-
# Handle escape sequences for characters
|
| 1475 |
string += self.current_char
|
| 1476 |
self.advance()
|
| 1477 |
if self.current_char is None:
|
| 1478 |
-
break
|
| 1479 |
|
| 1480 |
-
# Only allow valid escape sequences
|
| 1481 |
if self.current_char in "'\\nt":
|
| 1482 |
char += f"\\{self.current_char}"
|
| 1483 |
string += self.current_char
|
| 1484 |
else:
|
| 1485 |
-
# Invalid escape sequence - report error
|
| 1486 |
errors.append(LexicalError(pos, f"Invalid escape sequence '\\{self.current_char}' in character literal"))
|
| 1487 |
has_error = True
|
| 1488 |
-
# Consume rest of char literal to avoid cascading errors
|
| 1489 |
while self.current_char is not None and self.current_char != "'":
|
| 1490 |
self.advance()
|
| 1491 |
if self.current_char == "'":
|
|
@@ -1496,11 +1318,9 @@ class Lexer:
|
|
| 1496 |
char += self.current_char
|
| 1497 |
self.advance()
|
| 1498 |
|
| 1499 |
-
# Skip trailing whitespace before closing quote
|
| 1500 |
while len(char) > 0 and char[-1] in ' \t':
|
| 1501 |
char = char[:-1]
|
| 1502 |
|
| 1503 |
-
# Skip further processing if we already reported an error
|
| 1504 |
if has_error:
|
| 1505 |
continue
|
| 1506 |
|
|
@@ -1508,7 +1328,6 @@ class Lexer:
|
|
| 1508 |
string += self.current_char
|
| 1509 |
self.advance()
|
| 1510 |
else:
|
| 1511 |
-
# Missing closing quote - report error and skip (don't create token)
|
| 1512 |
errors.append(LexicalError(pos, f"Missing closing quote for character literal"))
|
| 1513 |
continue
|
| 1514 |
|
|
@@ -1517,15 +1336,11 @@ class Lexer:
|
|
| 1517 |
self.advance()
|
| 1518 |
continue
|
| 1519 |
|
| 1520 |
-
# Character literal must contain exactly one character (or one escape sequence)
|
| 1521 |
-
# Strip quotes to get inner content
|
| 1522 |
inner = char.strip()
|
| 1523 |
if len(inner) == 0:
|
| 1524 |
-
# Empty char literal '' defaults to a space character
|
| 1525 |
string = "' '"
|
| 1526 |
inner = ' '
|
| 1527 |
elif inner.startswith('\\') and len(inner) == 2:
|
| 1528 |
-
# Valid escape sequence like \n, \t
|
| 1529 |
pass
|
| 1530 |
elif len(inner) > 1:
|
| 1531 |
errors.append(LexicalError(pos, f"Character literal must contain exactly one character, found '{inner}'"))
|
|
@@ -1538,7 +1353,6 @@ class Lexer:
|
|
| 1538 |
pos = self.pos.copy()
|
| 1539 |
ident_str = self.current_char
|
| 1540 |
self.advance()
|
| 1541 |
-
# Check for valid delimiter after `
|
| 1542 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ' + '\t' + '\n'):
|
| 1543 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 1544 |
self.advance()
|
|
@@ -1550,12 +1364,9 @@ class Lexer:
|
|
| 1550 |
pos = self.pos.copy()
|
| 1551 |
char = self.current_char
|
| 1552 |
|
| 1553 |
-
# Special handling for underscore followed by alphanumeric
|
| 1554 |
if char == '_':
|
| 1555 |
self.advance()
|
| 1556 |
-
# Check if followed by letters (reserved word or identifier)
|
| 1557 |
if self.current_char is not None and self.current_char in ALPHA:
|
| 1558 |
-
# Peek ahead to see if it's a reserved word
|
| 1559 |
temp_str = ''
|
| 1560 |
temp_index = self.pos.index
|
| 1561 |
while temp_index < len(self.source_code) and self.source_code[temp_index] in ALPHANUM:
|
|
@@ -1571,7 +1382,6 @@ class Lexer:
|
|
| 1571 |
else:
|
| 1572 |
errors.append(LexicalError(pos, f"Identifiers cannot start with a symbol: '_...'"))
|
| 1573 |
|
| 1574 |
-
# Consume the rest
|
| 1575 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 1576 |
self.advance()
|
| 1577 |
continue
|
|
@@ -1583,47 +1393,21 @@ class Lexer:
|
|
| 1583 |
errors.append(LexicalError(pos, f"Illegal Character '" + char + "'"))
|
| 1584 |
continue
|
| 1585 |
|
| 1586 |
-
# End of file reached - add EOF token
|
| 1587 |
if self.current_char is None:
|
| 1588 |
tokens.append(Token(TT_EOF, "", line, pos.col))
|
| 1589 |
|
| 1590 |
-
return tokens, errors
|
| 1591 |
|
| 1592 |
-
# ============================================================================
|
| 1593 |
-
# HELPER FUNCTIONS - Public interface for using the lexer
|
| 1594 |
-
# ============================================================================
|
| 1595 |
|
| 1596 |
def run(source_code):
|
| 1597 |
-
"""
|
| 1598 |
-
Legacy function - runs lexer and returns tokens and errors.
|
| 1599 |
-
|
| 1600 |
-
Args:
|
| 1601 |
-
source_code (str): GAL source code to tokenize
|
| 1602 |
-
|
| 1603 |
-
Returns:
|
| 1604 |
-
tuple: (tokens, errors) where both are lists
|
| 1605 |
-
"""
|
| 1606 |
lexer = Lexer(source_code)
|
| 1607 |
tokens, error = lexer.make_tokens()
|
| 1608 |
return tokens, error
|
| 1609 |
|
| 1610 |
def lex(source_code):
|
| 1611 |
-
"""
|
| 1612 |
-
Main entry point for lexical analysis (used by server.py).
|
| 1613 |
-
Tokenizes source code and converts errors to strings.
|
| 1614 |
-
|
| 1615 |
-
Args:
|
| 1616 |
-
source_code (str): GAL source code to tokenize
|
| 1617 |
-
|
| 1618 |
-
Returns:
|
| 1619 |
-
tuple: (tokens, str_errors) where:
|
| 1620 |
-
- tokens: list of Token objects
|
| 1621 |
-
- str_errors: list of error messages as strings
|
| 1622 |
-
"""
|
| 1623 |
lexer = Lexer(source_code)
|
| 1624 |
tokens, errors = lexer.make_tokens()
|
| 1625 |
|
| 1626 |
-
# Convert LexicalError objects to string messages
|
| 1627 |
str_errors = []
|
| 1628 |
for e in errors:
|
| 1629 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from shared.tokens import * # noqa: F401,F403 - TT_* constants used heavily by the FSM
|
| 2 |
from shared.tokens import Token, get_token_description # noqa: F401 - explicit re-export
|
| 3 |
from lexer.positions import Position
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
class Lexer:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def __init__(self, source_code):
|
| 17 |
+
self.source_code = source_code.replace('\r', '')
|
| 18 |
+
self.pos = Position(-1, 1, -1)
|
| 19 |
+
self.current_char = None
|
| 20 |
+
self.advance()
|
| 21 |
|
| 22 |
def advance(self):
|
|
|
|
| 23 |
self.pos.advance(self.current_char)
|
|
|
|
| 24 |
self.current_char = self.source_code[self.pos.index] if self.pos.index<len(self.source_code) else None
|
| 25 |
|
| 26 |
def make_tokens(self):
|
| 27 |
+
tokens = []
|
| 28 |
+
line = 1
|
| 29 |
+
errors = []
|
| 30 |
+
pos = self.pos.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
+
while self.current_char != None:
|
|
|
|
| 33 |
|
| 34 |
+
if self.current_char in ALPHA:
|
| 35 |
+
ident_str = ''
|
| 36 |
+
pos = self.pos.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
if self.current_char == "b":
|
| 39 |
+
ident_str += self.current_char
|
| 40 |
+
self.advance()
|
| 41 |
+
if self.current_char == "r":
|
| 42 |
+
ident_str += self.current_char
|
| 43 |
+
self.advance()
|
| 44 |
+
if self.current_char == "a":
|
| 45 |
+
ident_str += self.current_char
|
| 46 |
+
self.advance()
|
| 47 |
+
if self.current_char == "n":
|
| 48 |
+
ident_str += self.current_char
|
| 49 |
+
self.advance()
|
| 50 |
+
if self.current_char == "c":
|
| 51 |
+
ident_str += self.current_char
|
| 52 |
+
self.advance()
|
| 53 |
+
if self.current_char == "h":
|
| 54 |
+
ident_str += self.current_char
|
| 55 |
+
self.advance()
|
|
|
|
|
|
|
|
|
|
| 56 |
if self.current_char is None or self.current_char in space_delim or self.current_char == ';':
|
| 57 |
+
tokens.append(Token(TT_RW_BRANCH, ident_str, line, pos.col))
|
| 58 |
+
continue
|
| 59 |
+
elif self.current_char not in ALPHANUM:
|
| 60 |
+
tokens.append(Token(TT_RW_BRANCH, ident_str, line, pos.col))
|
| 61 |
+
continue
|
| 62 |
+
elif self.current_char == "u":
|
| 63 |
+
ident_str += self.current_char
|
| 64 |
+
self.advance()
|
| 65 |
+
if self.current_char == "d":
|
| 66 |
+
ident_str += self.current_char
|
| 67 |
+
self.advance()
|
|
|
|
| 68 |
if self.current_char is None or (self.current_char is not None and self.current_char.isspace()) or self.current_char in delim4:
|
| 69 |
+
tokens.append(Token(TT_RW_BUD, ident_str, line, pos.col))
|
| 70 |
+
continue
|
| 71 |
elif self.current_char is not None and not self.current_char.isspace() and self.current_char not in delim4 and self.current_char not in ALPHANUM:
|
|
|
|
| 72 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 73 |
+
self.advance()
|
| 74 |
+
continue
|
| 75 |
+
elif self.current_char == "n":
|
| 76 |
+
ident_str += self.current_char
|
| 77 |
+
self.advance()
|
| 78 |
+
if self.current_char == "d":
|
| 79 |
+
ident_str += self.current_char
|
| 80 |
+
self.advance()
|
| 81 |
+
if self.current_char == "l":
|
| 82 |
+
ident_str += self.current_char
|
| 83 |
+
self.advance()
|
| 84 |
+
if self.current_char == "e":
|
| 85 |
+
ident_str += self.current_char
|
| 86 |
+
self.advance()
|
| 87 |
+
if self.current_char is not None and self.current_char in space_delim:
|
| 88 |
+
tokens.append(Token(TT_RW_BUNDLE, ident_str, line, pos.col))
|
| 89 |
+
continue
|
| 90 |
elif self.current_char is not None and self.current_char not in space_delim and self.current_char not in ALPHANUM:
|
|
|
|
| 91 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 92 |
+
self.advance()
|
| 93 |
+
continue
|
| 94 |
|
| 95 |
+
elif self.current_char == "c":
|
| 96 |
+
ident_str += self.current_char
|
| 97 |
+
self.advance()
|
| 98 |
+
if self.current_char == "u":
|
| 99 |
+
ident_str += self.current_char
|
| 100 |
+
self.advance()
|
| 101 |
+
if self.current_char == "l":
|
| 102 |
+
ident_str += self.current_char
|
| 103 |
+
self.advance()
|
| 104 |
+
if self.current_char == "t":
|
| 105 |
+
ident_str += self.current_char
|
| 106 |
+
self.advance()
|
| 107 |
+
if self.current_char == "i":
|
| 108 |
+
ident_str += self.current_char
|
| 109 |
+
self.advance()
|
| 110 |
+
if self.current_char == "v":
|
| 111 |
+
ident_str += self.current_char
|
| 112 |
+
self.advance()
|
| 113 |
+
if self.current_char == "a":
|
| 114 |
+
ident_str += self.current_char
|
| 115 |
+
self.advance()
|
| 116 |
+
if self.current_char == "t":
|
| 117 |
+
ident_str += self.current_char
|
| 118 |
+
self.advance()
|
| 119 |
+
if self.current_char == "e":
|
| 120 |
+
ident_str += self.current_char
|
| 121 |
+
self.advance()
|
|
|
|
|
|
|
| 122 |
if self.current_char is None or (self.current_char is not None and self.current_char.isspace()) or self.current_char in delim4:
|
| 123 |
+
tokens.append(Token(TT_RW_CULTIVATE, ident_str, line, pos.col))
|
| 124 |
+
continue
|
| 125 |
elif self.current_char is not None and not self.current_char.isspace() and self.current_char not in delim4 and self.current_char not in ALPHANUM:
|
| 126 |
+
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 127 |
+
self.advance()
|
| 128 |
+
continue
|
| 129 |
|
| 130 |
+
elif self.current_char == "e":
|
| 131 |
+
ident_str += self.current_char
|
| 132 |
+
self.advance()
|
| 133 |
+
if self.current_char == "m":
|
| 134 |
+
ident_str += self.current_char
|
| 135 |
+
self.advance()
|
| 136 |
+
if self.current_char == "p":
|
| 137 |
+
ident_str += self.current_char
|
| 138 |
+
self.advance()
|
| 139 |
+
if self.current_char == "t":
|
| 140 |
+
ident_str += self.current_char
|
| 141 |
+
self.advance()
|
| 142 |
+
if self.current_char == "y":
|
| 143 |
+
ident_str += self.current_char
|
| 144 |
+
self.advance()
|
|
|
|
|
|
|
| 145 |
if self.current_char is None or (self.current_char is not None and self.current_char.isspace()) or self.current_char in space_delim:
|
| 146 |
+
tokens.append(Token(TT_RW_EMPTY, ident_str, line, pos.col))
|
| 147 |
+
continue
|
| 148 |
elif self.current_char is not None and not self.current_char.isspace() and self.current_char not in space_delim and self.current_char not in ALPHANUM:
|
| 149 |
+
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 150 |
+
self.advance()
|
| 151 |
+
continue
|
| 152 |
|
|
|
|
| 153 |
elif self.current_char == "f":
|
| 154 |
ident_str += self.current_char
|
| 155 |
self.advance()
|
| 156 |
+
if self.current_char == "r":
|
| 157 |
ident_str += self.current_char
|
| 158 |
self.advance()
|
| 159 |
if self.current_char == "o":
|
|
|
|
| 166 |
ident_str += self.current_char
|
| 167 |
self.advance()
|
| 168 |
if self.current_char is None or (self.current_char not in ALPHANUM and self.current_char != '_'):
|
| 169 |
+
tokens.append(Token(TT_BOOLLIT_FALSE, ident_str, line, pos.col))
|
| 170 |
continue
|
| 171 |
+
elif self.current_char == "e":
|
|
|
|
| 172 |
ident_str += self.current_char
|
| 173 |
self.advance()
|
| 174 |
if self.current_char == "r":
|
|
|
|
| 194 |
self.advance()
|
| 195 |
continue
|
| 196 |
|
|
|
|
| 197 |
elif self.current_char == "g":
|
| 198 |
ident_str += self.current_char
|
| 199 |
self.advance()
|
| 200 |
+
if self.current_char == "r":
|
| 201 |
ident_str += self.current_char
|
| 202 |
self.advance()
|
| 203 |
if self.current_char == "o":
|
|
|
|
| 214 |
self.advance()
|
| 215 |
continue
|
| 216 |
|
|
|
|
| 217 |
elif self.current_char == "h":
|
| 218 |
ident_str += self.current_char
|
| 219 |
self.advance()
|
| 220 |
+
if self.current_char == "a":
|
| 221 |
ident_str += self.current_char
|
| 222 |
self.advance()
|
| 223 |
if self.current_char == "r":
|
|
|
|
| 243 |
self.advance()
|
| 244 |
continue
|
| 245 |
|
|
|
|
| 246 |
elif self.current_char == "l":
|
| 247 |
ident_str += self.current_char
|
| 248 |
self.advance()
|
| 249 |
+
if self.current_char == "e":
|
| 250 |
ident_str += self.current_char
|
| 251 |
self.advance()
|
| 252 |
if self.current_char == "a":
|
|
|
|
| 255 |
if self.current_char == "f":
|
| 256 |
ident_str += self.current_char
|
| 257 |
self.advance()
|
|
|
|
| 258 |
if self.current_char is None or self.current_char in space_delim or self.current_char == ';':
|
| 259 |
tokens.append(Token(TT_RW_LEAF, ident_str, line, pos.col))
|
| 260 |
continue
|
|
|
|
| 262 |
tokens.append(Token(TT_RW_LEAF, ident_str, line, pos.col))
|
| 263 |
continue
|
| 264 |
|
|
|
|
| 265 |
elif self.current_char == "p":
|
| 266 |
ident_str += self.current_char
|
| 267 |
self.advance()
|
| 268 |
+
if self.current_char == "l":
|
| 269 |
ident_str += self.current_char
|
| 270 |
self.advance()
|
| 271 |
if self.current_char == "a":
|
|
|
|
| 284 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 285 |
self.advance()
|
| 286 |
continue
|
| 287 |
+
elif self.current_char == "o":
|
| 288 |
ident_str += self.current_char
|
| 289 |
self.advance()
|
| 290 |
if self.current_char == "l":
|
|
|
|
| 315 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 316 |
self.advance()
|
| 317 |
continue
|
| 318 |
+
elif self.current_char == "r":
|
| 319 |
ident_str += self.current_char
|
| 320 |
self.advance()
|
| 321 |
if self.current_char == "u":
|
|
|
|
| 335 |
self.advance()
|
| 336 |
continue
|
| 337 |
|
|
|
|
| 338 |
elif self.current_char == "r":
|
| 339 |
ident_str += self.current_char
|
| 340 |
self.advance()
|
| 341 |
+
if self.current_char == "e":
|
| 342 |
ident_str += self.current_char
|
| 343 |
self.advance()
|
| 344 |
if self.current_char == "c":
|
|
|
|
| 363 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 364 |
self.advance()
|
| 365 |
continue
|
| 366 |
+
elif self.current_char == "o":
|
| 367 |
ident_str += self.current_char
|
| 368 |
self.advance()
|
| 369 |
if self.current_char == "o":
|
|
|
|
| 377 |
continue
|
| 378 |
elif self.current_char is not None and not self.current_char.isspace() and self.current_char not in delim7 and self.current_char not in ALPHANUM:
|
| 379 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 380 |
+
self.advance()
|
| 381 |
continue
|
| 382 |
|
|
|
|
| 383 |
elif self.current_char == "s":
|
| 384 |
ident_str += self.current_char
|
| 385 |
self.advance()
|
| 386 |
+
if self.current_char == "e":
|
| 387 |
ident_str += self.current_char
|
| 388 |
self.advance()
|
| 389 |
if self.current_char == "e":
|
|
|
|
| 392 |
if self.current_char == "d":
|
| 393 |
ident_str += self.current_char
|
| 394 |
self.advance()
|
|
|
|
| 395 |
if self.current_char is None or self.current_char in space_delim or self.current_char == ';':
|
| 396 |
tokens.append(Token(TT_RW_SEED, ident_str, line, pos.col))
|
| 397 |
continue
|
| 398 |
elif self.current_char not in ALPHANUM:
|
| 399 |
tokens.append(Token(TT_RW_SEED, ident_str, line, pos.col))
|
| 400 |
continue
|
| 401 |
+
elif self.current_char == "k":
|
| 402 |
ident_str += self.current_char
|
| 403 |
self.advance()
|
| 404 |
if self.current_char == "i":
|
|
|
|
| 414 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 415 |
self.advance()
|
| 416 |
continue
|
| 417 |
+
elif self.current_char == "o":
|
| 418 |
ident_str += self.current_char
|
| 419 |
self.advance()
|
| 420 |
if self.current_char == "i":
|
|
|
|
| 430 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 431 |
self.advance()
|
| 432 |
continue
|
| 433 |
+
elif self.current_char == "p":
|
| 434 |
ident_str += self.current_char
|
| 435 |
self.advance()
|
| 436 |
if self.current_char == "r":
|
|
|
|
| 452 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 453 |
self.advance()
|
| 454 |
continue
|
| 455 |
+
elif self.current_char == "u":
|
| 456 |
ident_str += self.current_char
|
| 457 |
self.advance()
|
| 458 |
if self.current_char == "n":
|
|
|
|
| 474 |
ident_str += self.current_char
|
| 475 |
self.advance()
|
| 476 |
if self.current_char is None or self.current_char in delim23 or self.current_char in space_delim:
|
| 477 |
+
tokens.append(Token(TT_BOOLLIT_TRUE, ident_str, line, pos.col))
|
| 478 |
continue
|
| 479 |
elif self.current_char not in ALPHANUM:
|
| 480 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 481 |
self.advance()
|
| 482 |
continue
|
| 483 |
|
|
|
|
| 484 |
elif self.current_char == "t":
|
| 485 |
ident_str += self.current_char
|
| 486 |
self.advance()
|
| 487 |
+
if self.current_char == "e":
|
| 488 |
ident_str += self.current_char
|
| 489 |
self.advance()
|
| 490 |
if self.current_char == "n":
|
|
|
|
| 500 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 501 |
self.advance()
|
| 502 |
continue
|
| 503 |
+
elif self.current_char == "r":
|
| 504 |
ident_str += self.current_char
|
| 505 |
self.advance()
|
| 506 |
if self.current_char == "e":
|
|
|
|
| 509 |
if self.current_char == "e":
|
| 510 |
ident_str += self.current_char
|
| 511 |
self.advance()
|
|
|
|
| 512 |
if self.current_char is None or self.current_char in space_delim or self.current_char == ';':
|
| 513 |
tokens.append(Token(TT_RW_TREE, ident_str, line, pos.col))
|
| 514 |
continue
|
|
|
|
| 516 |
tokens.append(Token(TT_RW_TREE, ident_str, line, pos.col))
|
| 517 |
continue
|
| 518 |
|
|
|
|
| 519 |
elif self.current_char == "v":
|
| 520 |
ident_str += self.current_char
|
| 521 |
self.advance()
|
| 522 |
+
if self.current_char == "i":
|
| 523 |
ident_str += self.current_char
|
| 524 |
self.advance()
|
| 525 |
if self.current_char == "n":
|
|
|
|
| 534 |
elif self.current_char not in ALPHANUM:
|
| 535 |
tokens.append(Token(TT_RW_VINE, ident_str, line, pos.col))
|
| 536 |
continue
|
| 537 |
+
elif self.current_char == "a":
|
| 538 |
ident_str += self.current_char
|
| 539 |
self.advance()
|
| 540 |
if self.current_char == "r":
|
|
|
|
| 552 |
if self.current_char == "y":
|
| 553 |
ident_str += self.current_char
|
| 554 |
self.advance()
|
|
|
|
| 555 |
if self.current_char is None or self.current_char in space_delim:
|
| 556 |
tokens.append(Token(TT_RW_VARIETY, ident_str, line, pos.col))
|
| 557 |
continue
|
|
|
|
| 559 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 560 |
self.advance()
|
| 561 |
continue
|
|
|
|
| 562 |
|
|
|
|
| 563 |
elif self.current_char == "w":
|
| 564 |
ident_str += self.current_char
|
| 565 |
self.advance()
|
| 566 |
+
if self.current_char == "a":
|
| 567 |
ident_str += self.current_char
|
| 568 |
self.advance()
|
| 569 |
if self.current_char == "t":
|
|
|
|
| 582 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 583 |
self.advance()
|
| 584 |
continue
|
| 585 |
+
elif self.current_char == "i":
|
| 586 |
ident_str += self.current_char
|
| 587 |
self.advance()
|
| 588 |
if self.current_char == "t":
|
|
|
|
| 605 |
self.advance()
|
| 606 |
continue
|
| 607 |
|
| 608 |
+
maxIdentifierLength = 15
|
|
|
|
| 609 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 610 |
ident_str += self.current_char
|
| 611 |
self.advance()
|
| 612 |
|
|
|
|
| 613 |
if len(ident_str) > maxIdentifierLength:
|
|
|
|
|
|
|
| 614 |
i = 0
|
| 615 |
remaining = None
|
| 616 |
while i < len(ident_str):
|
|
|
|
| 617 |
if i + 15 <= len(ident_str):
|
|
|
|
| 618 |
errors.append(LexicalError(pos, f"Identifier exceeds maximum length of {maxIdentifierLength} characters"))
|
| 619 |
i += 15
|
| 620 |
else:
|
|
|
|
| 621 |
remaining = ident_str[i:]
|
| 622 |
if self.current_char is None or self.current_char in idf_delim:
|
| 623 |
tokens.append(Token(TT_IDENTIFIER, remaining, line, pos.col))
|
|
|
|
| 625 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after identifier"))
|
| 626 |
break
|
| 627 |
if remaining is None:
|
|
|
|
|
|
|
| 628 |
last_chunk = ident_str[i - 15:] if i >= 15 else ident_str
|
| 629 |
if self.current_char is None or self.current_char in idf_delim:
|
| 630 |
tokens.append(Token(TT_IDENTIFIER, last_chunk, line, pos.col))
|
| 631 |
continue
|
| 632 |
else:
|
|
|
|
| 633 |
if self.current_char is None or self.current_char in idf_delim:
|
| 634 |
tokens.append(Token(TT_IDENTIFIER, ident_str, line, pos.col))
|
| 635 |
continue
|
|
|
|
| 644 |
pos = self.pos.copy()
|
| 645 |
self.advance()
|
| 646 |
if self.current_char == "-":
|
|
|
|
| 647 |
ident_str += self.current_char
|
| 648 |
self.advance()
|
| 649 |
if self.current_char is not None and self.current_char not in delim25:
|
|
|
|
| 657 |
self.advance()
|
| 658 |
tokens.append(Token(TT_MINUSEQ, ident_str, line, pos.col))
|
| 659 |
continue
|
|
|
|
| 660 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ' + '\t' + '\n'):
|
| 661 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 662 |
+
self.advance()
|
| 663 |
+
continue
|
| 664 |
tokens.append(Token(TT_MINUS, ident_str, line, pos.col))
|
| 665 |
continue
|
| 666 |
|
| 667 |
+
elif self.current_char == "~":
|
| 668 |
ident_str = self.current_char
|
| 669 |
pos = self.pos.copy()
|
| 670 |
self.advance()
|
| 671 |
|
|
|
|
| 672 |
if self.current_char is not None and self.current_char in ZERODIGIT:
|
|
|
|
| 673 |
num_str = ""
|
| 674 |
integer_digit_count = 0
|
| 675 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
|
|
|
| 677 |
num_str += self.current_char
|
| 678 |
self.advance()
|
| 679 |
|
|
|
|
| 680 |
if self.current_char == ".":
|
| 681 |
num_str += self.current_char
|
| 682 |
self.advance()
|
|
|
|
| 691 |
if fractional_digit_count > 8:
|
| 692 |
errors.append(LexicalError(pos, f"Fractional part exceeds maximum of 8 digits"))
|
| 693 |
continue
|
|
|
|
| 694 |
parts = num_str.split(".")
|
| 695 |
integer_part = parts[0].lstrip("0") or "0"
|
| 696 |
fractional_part = (parts[1] if len(parts) > 1 else "").rstrip("0") or "0"
|
|
|
|
| 702 |
tokens.append(Token(TT_DOUBLELIT, ident_str, line, pos.col))
|
| 703 |
continue
|
| 704 |
else:
|
|
|
|
| 705 |
if integer_digit_count > 8:
|
| 706 |
errors.append(LexicalError(pos, f"Integer exceeds maximum of 8 digits"))
|
| 707 |
continue
|
|
|
|
| 710 |
tokens.append(Token(TT_INTEGERLIT, ident_str, line, pos.col))
|
| 711 |
continue
|
| 712 |
|
|
|
|
|
|
|
| 713 |
elif self.current_char is None or self.current_char in ALPHANUM + '( \t\n':
|
| 714 |
tokens.append(Token(TT_NEGATIVE, ident_str, line, pos.col))
|
| 715 |
continue
|
|
|
|
| 725 |
if self.current_char == "=":
|
| 726 |
ident_str += self.current_char
|
| 727 |
self.advance()
|
|
|
|
| 728 |
if len(tokens) > 0 and tokens[-1].type in [TT_GT, TT_LT, TT_EQTO, TT_NOTEQ, TT_GTEQ, TT_LTEQ]:
|
| 729 |
errors.append(LexicalError(pos, f"Consecutive operators '{tokens[-1].value} {ident_str}' are not allowed"))
|
| 730 |
continue
|
|
|
|
| 743 |
self.advance()
|
| 744 |
tokens.append(Token(TT_MODEQ, ident_str, line, pos.col))
|
| 745 |
continue
|
|
|
|
| 746 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ;' + '\t' + '\n'):
|
| 747 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 748 |
+
self.advance()
|
| 749 |
+
continue
|
| 750 |
tokens.append(Token(TT_MOD, ident_str, line, pos.col))
|
| 751 |
continue
|
| 752 |
|
|
|
|
| 760 |
tokens.append(Token(TT_AND, ident_str, line, pos.col))
|
| 761 |
continue
|
| 762 |
else:
|
|
|
|
| 763 |
tokens.append(Token(TT_SINGLE_AND, ident_str, line, pos.col))
|
| 764 |
continue
|
| 765 |
|
|
|
|
| 784 |
if self.current_char == "*":
|
| 785 |
ident_str += self.current_char
|
| 786 |
self.advance()
|
|
|
|
|
|
|
| 787 |
if self.current_char == "=":
|
| 788 |
ident_str += self.current_char
|
| 789 |
self.advance()
|
|
|
|
| 796 |
self.advance()
|
| 797 |
tokens.append(Token(TT_MULTIEQ, ident_str, line, pos.col))
|
| 798 |
continue
|
|
|
|
| 799 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ' + '\t' + '\n'):
|
| 800 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 801 |
+
self.advance()
|
| 802 |
+
continue
|
| 803 |
tokens.append(Token(TT_MUL, ident_str, line, pos.col))
|
| 804 |
continue
|
| 805 |
|
| 806 |
elif self.current_char == ",":
|
| 807 |
ident_str = self.current_char
|
| 808 |
pos = self.pos.copy()
|
|
|
|
| 809 |
if len(tokens) > 0 and tokens[-1].type == TT_COMMA:
|
| 810 |
errors.append(LexicalError(pos, f"Invalid delimiters ','"))
|
| 811 |
self.advance()
|
|
|
|
| 814 |
tokens.append(Token(TT_COMMA, ident_str, line, pos.col))
|
| 815 |
continue
|
| 816 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
|
| 818 |
elif self.current_char == ";":
|
| 819 |
ident_str = self.current_char
|
|
|
|
| 860 |
tokens.append(Token(TT_OR, ident_str, line, pos.col))
|
| 861 |
continue
|
| 862 |
else:
|
|
|
|
| 863 |
tokens.append(Token(TT_SINGLE_OR, ident_str, line, pos.col))
|
| 864 |
continue
|
| 865 |
|
|
|
|
| 868 |
pos = self.pos.copy()
|
| 869 |
self.advance()
|
| 870 |
if self.current_char == "+":
|
|
|
|
| 871 |
ident_str += self.current_char
|
| 872 |
self.advance()
|
| 873 |
if self.current_char is not None and self.current_char not in delim25:
|
|
|
|
| 885 |
continue
|
| 886 |
tokens.append(Token(TT_PLUSEQ, ident_str, line, pos.col))
|
| 887 |
continue
|
|
|
|
| 888 |
if self.current_char is not None and self.current_char not in delim24:
|
| 889 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 890 |
+
self.advance()
|
| 891 |
+
continue
|
| 892 |
tokens.append(Token(TT_PLUS, ident_str, line, pos.col))
|
| 893 |
continue
|
| 894 |
|
|
|
|
| 904 |
tokens.append(Token(TT_LT, ident_str, line, pos.col))
|
| 905 |
continue
|
| 906 |
|
|
|
|
|
|
|
|
|
|
| 907 |
elif self.current_char == "=":
|
| 908 |
ident_str = self.current_char
|
| 909 |
pos = self.pos.copy()
|
| 910 |
self.advance()
|
| 911 |
|
| 912 |
+
if self.current_char == "=":
|
| 913 |
ident_str += self.current_char
|
| 914 |
self.advance()
|
| 915 |
|
|
|
|
|
|
|
| 916 |
|
|
|
|
| 917 |
if len(tokens) > 0 and tokens[-1].type in [TT_GT, TT_LT, TT_EQTO, TT_NOTEQ, TT_GTEQ, TT_LTEQ]:
|
| 918 |
errors.append(LexicalError(pos, f"invalid delimiters '{tokens[-1].value} {ident_str}' are not allowed"))
|
| 919 |
continue
|
| 920 |
|
|
|
|
| 921 |
tokens.append(Token(TT_EQTO, ident_str, line, pos.col))
|
| 922 |
continue
|
| 923 |
|
|
|
|
| 924 |
tokens.append(Token(TT_EQ, ident_str, line, pos.col))
|
| 925 |
continue
|
| 926 |
|
|
|
|
| 931 |
if self.current_char == "=":
|
| 932 |
ident_str += self.current_char
|
| 933 |
self.advance()
|
|
|
|
| 934 |
if len(tokens) > 0 and tokens[-1].type in [TT_GT, TT_LT, TT_EQTO, TT_NOTEQ, TT_GTEQ, TT_LTEQ]:
|
| 935 |
errors.append(LexicalError(pos, f"invalid delimiters '{tokens[-1].value} {ident_str}' are not allowed"))
|
| 936 |
continue
|
| 937 |
tokens.append(Token(TT_GTEQ, ident_str, line, pos.col))
|
| 938 |
continue
|
|
|
|
| 939 |
if len(tokens) > 0 and tokens[-1].type in [TT_GT, TT_LT, TT_EQTO, TT_NOTEQ, TT_GTEQ, TT_LTEQ]:
|
| 940 |
errors.append(LexicalError(pos, f"invalid delimiters '{tokens[-1].value} {ident_str}' are not allowed"))
|
| 941 |
continue
|
|
|
|
| 972 |
self.advance()
|
| 973 |
continue
|
| 974 |
|
|
|
|
|
|
|
|
|
|
| 975 |
elif self.current_char == "/":
|
| 976 |
ident_str = self.current_char
|
| 977 |
pos = self.pos.copy()
|
| 978 |
self.advance()
|
| 979 |
|
| 980 |
+
if self.current_char == "/":
|
| 981 |
ident_str += self.current_char
|
| 982 |
self.advance()
|
|
|
|
| 983 |
while self.current_char is not None and self.current_char != "\n":
|
| 984 |
ident_str += self.current_char
|
| 985 |
self.advance()
|
|
|
|
| 986 |
continue
|
| 987 |
|
| 988 |
+
elif self.current_char == "*":
|
| 989 |
ident_str += self.current_char
|
| 990 |
self.advance()
|
| 991 |
found_close = False
|
|
|
|
| 992 |
while self.current_char is not None:
|
|
|
|
| 993 |
if self.current_char == "*" and self.pos.index + 1 < len(self.source_code) and self.source_code[self.pos.index + 1] == "/":
|
| 994 |
ident_str += "*/"
|
| 995 |
+
self.advance()
|
| 996 |
+
self.advance()
|
| 997 |
found_close = True
|
| 998 |
break
|
| 999 |
else:
|
| 1000 |
ident_str += self.current_char
|
| 1001 |
+
if self.current_char == "\n":
|
| 1002 |
line += 1
|
| 1003 |
self.advance()
|
|
|
|
| 1004 |
|
|
|
|
| 1005 |
if not found_close:
|
| 1006 |
errors.append(LexicalError(pos, f"Missing closing '*/' after '{ident_str}'"))
|
| 1007 |
continue
|
|
|
|
| 1012 |
tokens.append(Token(TT_DIVEQ, ident_str, line, pos.col))
|
| 1013 |
continue
|
| 1014 |
else:
|
|
|
|
| 1015 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ' + '\t' + '\n'):
|
| 1016 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 1017 |
+
self.advance()
|
| 1018 |
+
continue
|
| 1019 |
tokens.append(Token(TT_DIV, ident_str, line, pos.col))
|
| 1020 |
continue
|
| 1021 |
|
|
|
|
| 1034 |
if len(fractional_part + self.current_char) > 8:
|
| 1035 |
errors.append(LexicalError(pos, f"'{ident_str}' exceeds maximum number of decimal places"))
|
| 1036 |
overflow = True
|
|
|
|
| 1037 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
| 1038 |
self.advance()
|
| 1039 |
break
|
|
|
|
| 1060 |
tokens.append(Token(TT_COLON, ident_str, line, pos.col))
|
| 1061 |
continue
|
| 1062 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
elif self.current_char in ZERODIGIT:
|
| 1064 |
+
dot_count = 0
|
| 1065 |
+
ident_str = ""
|
| 1066 |
+
pos = self.pos.copy()
|
| 1067 |
+
integer_digit_count = 0
|
| 1068 |
+
fractional_digit_count = 0
|
| 1069 |
+
has_e = False
|
| 1070 |
|
| 1071 |
|
|
|
|
| 1072 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
| 1073 |
integer_digit_count += 1
|
| 1074 |
ident_str += self.current_char
|
| 1075 |
self.advance()
|
| 1076 |
|
|
|
|
| 1077 |
if self.current_char == ".":
|
|
|
|
| 1078 |
if integer_digit_count > 15:
|
|
|
|
|
|
|
| 1079 |
integer_part = ident_str
|
| 1080 |
i = 0
|
| 1081 |
while i < len(integer_part):
|
| 1082 |
if i + 15 < len(integer_part):
|
|
|
|
| 1083 |
errors.append(LexicalError(pos, f"Integer part of decimal exceeds maximum of 15 digits"))
|
| 1084 |
i += 15
|
| 1085 |
else:
|
|
|
|
| 1086 |
ident_str = integer_part[i:]
|
| 1087 |
break
|
| 1088 |
else:
|
|
|
|
| 1089 |
ident_str = "0"
|
| 1090 |
+
dot_count = 1
|
| 1091 |
ident_str += self.current_char
|
| 1092 |
self.advance()
|
| 1093 |
|
|
|
|
| 1094 |
if self.current_char is None or self.current_char not in ZERODIGIT:
|
| 1095 |
errors.append(LexicalError(pos, f"Invalid number '{ident_str}': decimal point must be followed by digits"))
|
| 1096 |
continue
|
| 1097 |
|
|
|
|
| 1098 |
fractional_part = ""
|
| 1099 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
| 1100 |
fractional_digit_count += 1
|
| 1101 |
fractional_part += self.current_char
|
| 1102 |
self.advance()
|
| 1103 |
|
|
|
|
| 1104 |
if fractional_digit_count > 8:
|
|
|
|
|
|
|
| 1105 |
i = 0
|
| 1106 |
final_fractional = ""
|
| 1107 |
while i < len(fractional_part):
|
| 1108 |
if i + 8 < len(fractional_part):
|
|
|
|
| 1109 |
errors.append(LexicalError(pos, f"Fractional part exceeds maximum of 8 digits"))
|
| 1110 |
i += 8
|
| 1111 |
else:
|
|
|
|
| 1112 |
final_fractional = fractional_part[i:]
|
| 1113 |
break
|
| 1114 |
ident_str += final_fractional
|
| 1115 |
else:
|
| 1116 |
ident_str += fractional_part
|
| 1117 |
|
|
|
|
|
|
|
| 1118 |
if dot_count == 0 and integer_digit_count > 8:
|
| 1119 |
i = 0
|
| 1120 |
remaining = None
|
| 1121 |
while i < len(ident_str):
|
|
|
|
| 1122 |
if i + 8 < len(ident_str):
|
|
|
|
| 1123 |
errors.append(LexicalError(pos, f"Integer exceeds maximum of 8 digits"))
|
| 1124 |
i += 8
|
| 1125 |
else:
|
|
|
|
| 1126 |
remaining = ident_str[i:]
|
|
|
|
| 1127 |
remaining = remaining.lstrip("0") or "0"
|
| 1128 |
tokens.append(Token(TT_INTEGERLIT, remaining, line, pos.col))
|
| 1129 |
break
|
| 1130 |
if remaining is None:
|
|
|
|
| 1131 |
tokens.append(Token(TT_INTEGERLIT, "0", line, pos.col))
|
| 1132 |
continue
|
| 1133 |
|
| 1134 |
if fractional_digit_count > 8:
|
|
|
|
|
|
|
| 1135 |
pass
|
| 1136 |
|
|
|
|
|
|
|
| 1137 |
if self.current_char is not None and self.current_char in 'eE' and dot_count == 1:
|
| 1138 |
has_e = True
|
| 1139 |
ident_str += self.current_char
|
| 1140 |
self.advance()
|
| 1141 |
|
|
|
|
| 1142 |
if self.current_char in '+-':
|
| 1143 |
ident_str += self.current_char
|
| 1144 |
self.advance()
|
| 1145 |
|
|
|
|
| 1146 |
if self.current_char is None or self.current_char not in ZERODIGIT:
|
| 1147 |
errors.append(LexicalError(pos, f"Invalid scientific notation: 'e' must be followed by digits."))
|
| 1148 |
continue
|
| 1149 |
|
|
|
|
| 1150 |
while self.current_char is not None and self.current_char in ZERODIGIT:
|
| 1151 |
ident_str += self.current_char
|
| 1152 |
self.advance()
|
| 1153 |
|
| 1154 |
if dot_count == 0 and not has_e:
|
|
|
|
| 1155 |
|
|
|
|
| 1156 |
if self.current_char is not None and self.current_char not in whlnum_delim:
|
|
|
|
| 1157 |
valid_int = ident_str.lstrip("0") or "0"
|
| 1158 |
tokens.append(Token(TT_INTEGERLIT, valid_int, line, pos.col))
|
| 1159 |
|
|
|
|
| 1160 |
if self.current_char in ALPHA:
|
|
|
|
| 1161 |
temp_str = ''
|
| 1162 |
temp_pos = self.pos.copy()
|
| 1163 |
temp_char = self.current_char
|
|
|
|
| 1166 |
temp_pos.advance(temp_char)
|
| 1167 |
temp_char = self.source_code[temp_pos.index] if temp_pos.index < len(self.source_code) else None
|
| 1168 |
|
|
|
|
| 1169 |
reserved_words = {'water', 'plant', 'seed', 'leaf', 'branch', 'tree', 'spring', 'wither', 'bud',
|
| 1170 |
'harvest', 'grow', 'cultivate', 'tend', 'empty', 'prune', 'skip', 'reclaim',
|
| 1171 |
'root', 'pollinate', 'variety', 'fertile', 'soil', 'bundle', 'string', 'vine', 'sunshine', 'frost'}
|
|
|
|
| 1175 |
else:
|
| 1176 |
errors.append(LexicalError(pos, f"Identifiers cannot start with a number: '{ident_str}{self.current_char}...'"))
|
| 1177 |
|
|
|
|
| 1178 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 1179 |
self.advance()
|
| 1180 |
elif self.current_char == '_':
|
|
|
|
| 1181 |
temp_index = self.pos.index + 1
|
| 1182 |
if temp_index < len(self.source_code) and self.source_code[temp_index] in ALPHA:
|
|
|
|
| 1183 |
temp_str = ''
|
| 1184 |
while temp_index < len(self.source_code) and self.source_code[temp_index] in ALPHANUM:
|
| 1185 |
temp_str += self.source_code[temp_index]
|
|
|
|
| 1194 |
else:
|
| 1195 |
errors.append(LexicalError(pos, f"Identifiers cannot start with a number: '{ident_str}_...'"))
|
| 1196 |
|
|
|
|
| 1197 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 1198 |
self.advance()
|
| 1199 |
else:
|
|
|
|
| 1200 |
errors.append(LexicalError(pos, f"Underscore cannot be used in numeric literals"))
|
|
|
|
| 1201 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 1202 |
self.advance()
|
| 1203 |
else:
|
|
|
|
| 1204 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
|
|
|
| 1205 |
continue
|
| 1206 |
|
| 1207 |
ident_str = ident_str.lstrip("0") or "0"
|
| 1208 |
tokens.append(Token(TT_INTEGERLIT, ident_str, line, pos.col))
|
| 1209 |
continue
|
| 1210 |
|
| 1211 |
+
else:
|
|
|
|
| 1212 |
if self.current_char is not None and self.current_char not in decim_delim:
|
| 1213 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 1214 |
self.advance()
|
| 1215 |
continue
|
| 1216 |
+
if not has_e:
|
| 1217 |
parts = ident_str.split(".")
|
| 1218 |
integer_part = parts[0].lstrip("0") or "0"
|
| 1219 |
fractional_part = (parts[1] if len(parts) > 1 else "").rstrip("0") or "0"
|
| 1220 |
if fractional_part == "0":
|
| 1221 |
+
ident_str = f"{integer_part}.0"
|
| 1222 |
else:
|
| 1223 |
ident_str = f"{integer_part}.{fractional_part}"
|
| 1224 |
|
| 1225 |
tokens.append(Token(TT_DOUBLELIT, ident_str, line, pos.col))
|
| 1226 |
continue
|
| 1227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1228 |
elif self.current_char == '"':
|
| 1229 |
+
string = ''
|
| 1230 |
+
pos = self.pos.copy()
|
| 1231 |
+
escape_character = False
|
| 1232 |
+
string += self.current_char
|
| 1233 |
self.advance()
|
| 1234 |
|
|
|
|
| 1235 |
escape_characters = {
|
| 1236 |
+
'n': '\n',
|
| 1237 |
+
't': '\t',
|
| 1238 |
+
'{': '\\{',
|
| 1239 |
+
'}': '\\}',
|
| 1240 |
+
'"': '"',
|
| 1241 |
+
'\\': '\\',
|
| 1242 |
}
|
| 1243 |
|
| 1244 |
has_string_error = False
|
|
|
|
| 1245 |
while self.current_char is not None and (self.current_char != '"' or escape_character):
|
| 1246 |
if escape_character:
|
|
|
|
| 1247 |
if self.current_char in escape_characters:
|
| 1248 |
string += escape_characters[self.current_char]
|
| 1249 |
else:
|
|
|
|
| 1260 |
self.advance()
|
| 1261 |
|
| 1262 |
if has_string_error:
|
|
|
|
| 1263 |
while self.current_char is not None and self.current_char != '"':
|
| 1264 |
self.advance()
|
| 1265 |
if self.current_char == '"':
|
|
|
|
| 1270 |
string += self.current_char
|
| 1271 |
self.advance()
|
| 1272 |
else:
|
|
|
|
| 1273 |
errors.append(LexicalError(pos, f"Missing closing '\"' for string literal"))
|
| 1274 |
continue
|
| 1275 |
|
|
|
|
| 1278 |
self.advance()
|
| 1279 |
continue
|
| 1280 |
|
|
|
|
|
|
|
| 1281 |
tokens.append(Token(TT_STRINGLIT, string, line, pos.col))
|
| 1282 |
continue
|
| 1283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1284 |
elif self.current_char == "'":
|
| 1285 |
+
string = ''
|
| 1286 |
+
char = ''
|
| 1287 |
pos = self.pos.copy()
|
| 1288 |
+
string += self.current_char
|
| 1289 |
self.advance()
|
| 1290 |
+
has_error = False
|
| 1291 |
|
|
|
|
| 1292 |
while self.current_char is not None and self.current_char in ' \t':
|
| 1293 |
string += self.current_char
|
| 1294 |
self.advance()
|
| 1295 |
|
|
|
|
| 1296 |
while self.current_char is not None and self.current_char != "'":
|
| 1297 |
+
if self.current_char == '\n':
|
| 1298 |
break
|
| 1299 |
+
elif self.current_char == '\\':
|
|
|
|
| 1300 |
string += self.current_char
|
| 1301 |
self.advance()
|
| 1302 |
if self.current_char is None:
|
| 1303 |
+
break
|
| 1304 |
|
|
|
|
| 1305 |
if self.current_char in "'\\nt":
|
| 1306 |
char += f"\\{self.current_char}"
|
| 1307 |
string += self.current_char
|
| 1308 |
else:
|
|
|
|
| 1309 |
errors.append(LexicalError(pos, f"Invalid escape sequence '\\{self.current_char}' in character literal"))
|
| 1310 |
has_error = True
|
|
|
|
| 1311 |
while self.current_char is not None and self.current_char != "'":
|
| 1312 |
self.advance()
|
| 1313 |
if self.current_char == "'":
|
|
|
|
| 1318 |
char += self.current_char
|
| 1319 |
self.advance()
|
| 1320 |
|
|
|
|
| 1321 |
while len(char) > 0 and char[-1] in ' \t':
|
| 1322 |
char = char[:-1]
|
| 1323 |
|
|
|
|
| 1324 |
if has_error:
|
| 1325 |
continue
|
| 1326 |
|
|
|
|
| 1328 |
string += self.current_char
|
| 1329 |
self.advance()
|
| 1330 |
else:
|
|
|
|
| 1331 |
errors.append(LexicalError(pos, f"Missing closing quote for character literal"))
|
| 1332 |
continue
|
| 1333 |
|
|
|
|
| 1336 |
self.advance()
|
| 1337 |
continue
|
| 1338 |
|
|
|
|
|
|
|
| 1339 |
inner = char.strip()
|
| 1340 |
if len(inner) == 0:
|
|
|
|
| 1341 |
string = "' '"
|
| 1342 |
inner = ' '
|
| 1343 |
elif inner.startswith('\\') and len(inner) == 2:
|
|
|
|
| 1344 |
pass
|
| 1345 |
elif len(inner) > 1:
|
| 1346 |
errors.append(LexicalError(pos, f"Character literal must contain exactly one character, found '{inner}'"))
|
|
|
|
| 1353 |
pos = self.pos.copy()
|
| 1354 |
ident_str = self.current_char
|
| 1355 |
self.advance()
|
|
|
|
| 1356 |
if self.current_char is not None and self.current_char not in set(ALPHANUM + '(~!"\' ' + '\t' + '\n'):
|
| 1357 |
errors.append(LexicalError(pos, f"Invalid delimiter '{self.current_char}' after '{ident_str}'"))
|
| 1358 |
self.advance()
|
|
|
|
| 1364 |
pos = self.pos.copy()
|
| 1365 |
char = self.current_char
|
| 1366 |
|
|
|
|
| 1367 |
if char == '_':
|
| 1368 |
self.advance()
|
|
|
|
| 1369 |
if self.current_char is not None and self.current_char in ALPHA:
|
|
|
|
| 1370 |
temp_str = ''
|
| 1371 |
temp_index = self.pos.index
|
| 1372 |
while temp_index < len(self.source_code) and self.source_code[temp_index] in ALPHANUM:
|
|
|
|
| 1382 |
else:
|
| 1383 |
errors.append(LexicalError(pos, f"Identifiers cannot start with a symbol: '_...'"))
|
| 1384 |
|
|
|
|
| 1385 |
while self.current_char is not None and self.current_char in ALPHANUM:
|
| 1386 |
self.advance()
|
| 1387 |
continue
|
|
|
|
| 1393 |
errors.append(LexicalError(pos, f"Illegal Character '" + char + "'"))
|
| 1394 |
continue
|
| 1395 |
|
|
|
|
| 1396 |
if self.current_char is None:
|
| 1397 |
tokens.append(Token(TT_EOF, "", line, pos.col))
|
| 1398 |
|
| 1399 |
+
return tokens, errors
|
| 1400 |
|
|
|
|
|
|
|
|
|
|
| 1401 |
|
| 1402 |
def run(source_code):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1403 |
lexer = Lexer(source_code)
|
| 1404 |
tokens, error = lexer.make_tokens()
|
| 1405 |
return tokens, error
|
| 1406 |
|
| 1407 |
def lex(source_code):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1408 |
lexer = Lexer(source_code)
|
| 1409 |
tokens, errors = lexer.make_tokens()
|
| 1410 |
|
|
|
|
| 1411 |
str_errors = []
|
| 1412 |
for e in errors:
|
| 1413 |
try:
|
Backend/parser/__init__.py
CHANGED
|
@@ -1,12 +1,2 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# PARSER PACKAGE - Public API for syntax analysis + AST construction
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Re-exports LL1Parser so external callers (server.py) can keep using:
|
| 5 |
-
# from parser import LL1Parser
|
| 6 |
-
# unchanged after the restructure.
|
| 7 |
-
#
|
| 8 |
-
# In a later phase (5b), parser/builder.py will hold build_ast and the parser
|
| 9 |
-
# will import it as a sibling rather than reaching across to GALsemantic.py.
|
| 10 |
-
# ============================================================================
|
| 11 |
|
| 12 |
from .parser import LL1Parser # noqa: F401 - main public class
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
from .parser import LL1Parser # noqa: F401 - main public class
|
Backend/parser/builder.py
CHANGED
|
@@ -1,36 +1,12 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# AST BUILDER - build_ast + all parse_* helpers + analyze_semantics
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from GALsemantic.py during the modular restructure.
|
| 5 |
-
#
|
| 6 |
-
# This module turns a validated token stream into an AST. The parser (in
|
| 7 |
-
# parser/parser.py) does LL(1) syntax checking, then hands the tokens to
|
| 8 |
-
# build_ast() here. During construction the parse_* helpers also perform
|
| 9 |
-
# many semantic checks (undeclared variables, duplicate functions, type
|
| 10 |
-
# mismatches in expressions). A dedicated tree-walking validator in
|
| 11 |
-
# semantic/analyzer.py runs as the final semantic pass.
|
| 12 |
-
#
|
| 13 |
-
# Why this lives under parser/ and not semantic/:
|
| 14 |
-
# The natural output of parsing is an AST. Putting build_ast under parser/
|
| 15 |
-
# makes the data flow textbook-clean:
|
| 16 |
-
# tokens -> parser (syntax + AST construction) -> semantic (validate)
|
| 17 |
-
# This is more correct than the original layout where GALsemantic.py was
|
| 18 |
-
# doing both jobs.
|
| 19 |
-
# ============================================================================
|
| 20 |
import copy
|
| 21 |
import re
|
| 22 |
|
| 23 |
-
# Shared types & constants
|
| 24 |
from shared.tokens import * # noqa: F401,F403 - TT_* constants used throughout parse_*
|
| 25 |
from semantic.errors import SemanticError
|
| 26 |
from shared.ast_nodes import * # noqa: F401,F403 - all AST node classes
|
| 27 |
-
# SymbolTable lives in semantic/symbol_table.py.
|
| 28 |
from semantic.symbol_table import SymbolTable
|
| 29 |
|
| 30 |
|
| 31 |
-
# Vestigial placeholder from an earlier design; the real semantic checks happen
|
| 32 |
-
# in the parse_* helpers below and in semantic/analyzer.py. Kept (locally) only
|
| 33 |
-
# because semantic_analyzer is referenced as a module-level singleton.
|
| 34 |
class SemanticAnalyzer:
|
| 35 |
def __init__(self, symbol_table):
|
| 36 |
self.symbol_table = symbol_table
|
|
@@ -41,26 +17,14 @@ symbol_table = SymbolTable()
|
|
| 41 |
semantic_analyzer = SemanticAnalyzer(symbol_table)
|
| 42 |
context_stack = []
|
| 43 |
|
| 44 |
-
# ============================================================================
|
| 45 |
-
# BUILD AST - Main entry point for AST construction
|
| 46 |
-
# ============================================================================
|
| 47 |
-
# Walks the token stream produced by the lexer (after parser validation)
|
| 48 |
-
# and constructs a tree of ASTNode objects. While walking it also performs
|
| 49 |
-
# many semantic checks via the parse_* helpers below (undeclared variables,
|
| 50 |
-
# duplicate functions, type mismatches in expressions, etc.).
|
| 51 |
-
#
|
| 52 |
-
# Called by Gal_Parser.parse_and_build right after LL(1) syntax validation.
|
| 53 |
-
# Returns the root ProgramNode. Errors are raised as SemanticError.
|
| 54 |
-
# ============================================================================
|
| 55 |
|
| 56 |
def build_ast(tokens):
|
| 57 |
-
"""Constructs an AST from the token list after LL(1) parsing."""
|
| 58 |
root = ProgramNode()
|
| 59 |
-
symbol_table.variables = {}
|
| 60 |
-
symbol_table.functions = {}
|
| 61 |
symbol_table.scopes = [{}]
|
| 62 |
symbol_table.function_variables = {}
|
| 63 |
-
symbol_table.bundle_types = {}
|
| 64 |
context_stack = []
|
| 65 |
index = 0
|
| 66 |
symbol_table.current_func_name = None
|
|
@@ -68,7 +32,6 @@ def build_ast(tokens):
|
|
| 68 |
while index < len(tokens):
|
| 69 |
token = tokens[index]
|
| 70 |
|
| 71 |
-
# Skip semicolons between top-level declarations
|
| 72 |
if token.type == ";":
|
| 73 |
index += 1
|
| 74 |
continue
|
|
@@ -112,7 +75,6 @@ def build_ast(tokens):
|
|
| 112 |
root.add_child(node)
|
| 113 |
|
| 114 |
elif tokens[index].type == "id" and tokens[index].value in symbol_table.bundle_types:
|
| 115 |
-
# Bundle return type: pollinate Pair makePair(...) { ... }
|
| 116 |
id_type = tokens[index].value
|
| 117 |
index += 1
|
| 118 |
if tokens[index].type != "id":
|
|
@@ -147,11 +109,10 @@ def build_ast(tokens):
|
|
| 147 |
|
| 148 |
elif token.value == "bundle":
|
| 149 |
bundle_name = tokens[index + 1].value
|
| 150 |
-
index += 2
|
| 151 |
|
| 152 |
if tokens[index].type == "{":
|
| 153 |
-
|
| 154 |
-
index += 1 # skip '{'
|
| 155 |
members = {}
|
| 156 |
while tokens[index].type != "}":
|
| 157 |
if tokens[index].value in {"seed", "tree", "vine", "leaf", "branch"}:
|
|
@@ -160,22 +121,21 @@ def build_ast(tokens):
|
|
| 160 |
if member_name in members:
|
| 161 |
raise SemanticError(f"Semantic Error: Duplicate member '{member_name}' in bundle '{bundle_name}'.", tokens[index].line)
|
| 162 |
members[member_name] = member_type
|
| 163 |
-
index += 2
|
| 164 |
if tokens[index].type == ";":
|
| 165 |
-
index += 1
|
| 166 |
elif tokens[index].type == "id" and tokens[index].value in symbol_table.bundle_types:
|
| 167 |
-
# Nested bundle member: Address addr;
|
| 168 |
member_type = tokens[index].value
|
| 169 |
member_name = tokens[index + 1].value
|
| 170 |
if member_name in members:
|
| 171 |
raise SemanticError(f"Semantic Error: Duplicate member '{member_name}' in bundle '{bundle_name}'.", tokens[index].line)
|
| 172 |
members[member_name] = member_type
|
| 173 |
-
index += 2
|
| 174 |
if tokens[index].type == ";":
|
| 175 |
-
index += 1
|
| 176 |
else:
|
| 177 |
raise SemanticError(f"Semantic Error: Invalid member type '{tokens[index].value}' in bundle definition.", tokens[index].line)
|
| 178 |
-
index += 1
|
| 179 |
|
| 180 |
if bundle_name in symbol_table.bundle_types:
|
| 181 |
raise SemanticError(f"Semantic Error: Bundle type '{bundle_name}' already defined.", token.line)
|
|
@@ -185,7 +145,6 @@ def build_ast(tokens):
|
|
| 185 |
root.add_child(node)
|
| 186 |
|
| 187 |
else:
|
| 188 |
-
# Global bundle variable: bundle Person p;
|
| 189 |
var_name = tokens[index].value
|
| 190 |
index += 1
|
| 191 |
|
|
@@ -233,12 +192,6 @@ def parse_functionOrVariable(tokens, index):
|
|
| 233 |
|
| 234 |
return None, index
|
| 235 |
|
| 236 |
-
# ============================================================================
|
| 237 |
-
# DECLARATION PARSERS - parse_function, parse_variable
|
| 238 |
-
# These build the AST nodes for top-level declarations and register them
|
| 239 |
-
# in the symbol table. parse_function handles both root() and pollinate
|
| 240 |
-
# functions; parse_variable handles seed/tree/leaf/branch/vine declarations.
|
| 241 |
-
# ============================================================================
|
| 242 |
def parse_function(tokens, index, func_name, func_type):
|
| 243 |
line = tokens[index].line
|
| 244 |
|
|
@@ -252,12 +205,10 @@ def parse_function(tokens, index, func_name, func_type):
|
|
| 252 |
error = f"Semantic Error: '{func_name}' already declared."
|
| 253 |
raise SemanticError(error, tokens[index].line)
|
| 254 |
|
| 255 |
-
# Entry point: root() (legacy alias: skibidi{...})
|
| 256 |
if func_name in {"root"}:
|
| 257 |
symbol_table.enter_scope()
|
| 258 |
-
index += 1
|
| 259 |
|
| 260 |
-
# root() requires empty parameter list
|
| 261 |
if tokens[index].type == "(":
|
| 262 |
index += 1
|
| 263 |
if tokens[index].type != ")":
|
|
@@ -289,13 +240,12 @@ def parse_function(tokens, index, func_name, func_type):
|
|
| 289 |
param_node.add_child(ASTNode("Identifier", param_name))
|
| 290 |
index += 1
|
| 291 |
|
| 292 |
-
# Check for array parameter: seed arr[]
|
| 293 |
is_list = False
|
| 294 |
if tokens[index].type == "[":
|
| 295 |
-
index += 1
|
| 296 |
if tokens[index].type != "]":
|
| 297 |
raise SemanticError(f"Semantic Error: Expected ']' after '[' in array parameter.", line)
|
| 298 |
-
index += 1
|
| 299 |
is_list = True
|
| 300 |
param_node.add_child(ASTNode("ArrayParam", "true"))
|
| 301 |
|
|
@@ -312,7 +262,6 @@ def parse_function(tokens, index, func_name, func_type):
|
|
| 312 |
raise SemanticError(error, line)
|
| 313 |
|
| 314 |
elif tokens[index].type == "id" and tokens[index].value in symbol_table.bundle_types:
|
| 315 |
-
# Bundle parameter: Pair p
|
| 316 |
param_type = tokens[index].value
|
| 317 |
index += 1
|
| 318 |
if tokens[index].type == "id":
|
|
@@ -401,17 +350,17 @@ def parse_variable(tokens, index, var_name, var_type):
|
|
| 401 |
|
| 402 |
elif tokens[index].value == "water":
|
| 403 |
water_line = tokens[index].line
|
| 404 |
-
index += 1
|
| 405 |
if tokens[index].type != "(":
|
| 406 |
raise SemanticError(f"Semantic Error: Expected '(' after water.", water_line)
|
| 407 |
-
index += 1
|
| 408 |
water_type = None
|
| 409 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 410 |
water_type = tokens[index].value
|
| 411 |
index += 1
|
| 412 |
if tokens[index].type != ")":
|
| 413 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter (e.g., water(seed)).", water_line)
|
| 414 |
-
index += 1
|
| 415 |
if water_type and not _types_compatible(var_type, water_type):
|
| 416 |
raise SemanticError(f"Semantic Error: Type mismatch — cannot assign water({water_type}) to '{var_type}' variable.", water_line)
|
| 417 |
value_node = ASTNode("Input", f"water({water_type})" if water_type else "water()", line=water_line)
|
|
@@ -422,24 +371,21 @@ def parse_variable(tokens, index, var_name, var_type):
|
|
| 422 |
var_node.add_child(value_node)
|
| 423 |
|
| 424 |
elif tokens[index].type == "[":
|
| 425 |
-
# List declaration with size: seed nums[3]; or seed nums[];
|
| 426 |
-
# Also supports multi-dimensional: seed matrix[2][3];
|
| 427 |
is_list = True
|
| 428 |
dimensions = []
|
| 429 |
while tokens[index].type == "[":
|
| 430 |
-
index += 1
|
| 431 |
dim_size = 0
|
| 432 |
if tokens[index].type == "dbllit":
|
| 433 |
raise SemanticError(f"Semantic Error: Array size must be of type 'seed' (integer), got 'tree' (float) '{tokens[index].value}'.", line)
|
| 434 |
if tokens[index].type == "intlit":
|
| 435 |
dim_size = int(tokens[index].value)
|
| 436 |
-
index += 1
|
| 437 |
if tokens[index].type != "]":
|
| 438 |
raise SemanticError(f"Syntax Error: Expected ']' after list size.", line)
|
| 439 |
-
index += 1
|
| 440 |
dimensions.append(dim_size)
|
| 441 |
|
| 442 |
-
# Build nested List nodes for multi-dimensional arrays
|
| 443 |
default_literals = {"seed": "0", "tree": "0.0", "leaf": "''", "vine": '""', "branch": "false"}
|
| 444 |
|
| 445 |
def build_list_node(dims):
|
|
@@ -455,26 +401,23 @@ def parse_variable(tokens, index, var_name, var_type):
|
|
| 455 |
list_node = build_list_node(dimensions)
|
| 456 |
var_node.add_child(list_node)
|
| 457 |
|
| 458 |
-
# Handle optional initialization after size: seed nums[3] = {10, 20, 30} ;
|
| 459 |
if tokens[index].type == "=":
|
| 460 |
-
index += 1
|
| 461 |
if tokens[index].type == "{":
|
| 462 |
-
index += 1
|
| 463 |
elements = []
|
| 464 |
while tokens[index].type != "}":
|
| 465 |
expr, index = parse_expression_type(tokens, index, var_type)
|
| 466 |
elements.append(expr)
|
| 467 |
if tokens[index].type == ",":
|
| 468 |
index += 1
|
| 469 |
-
index += 1
|
| 470 |
value_node = ListNode(elements=elements, line=line)
|
| 471 |
-
# Replace the default list_node with the initialized values
|
| 472 |
var_node.children[-1] = value_node
|
| 473 |
else:
|
| 474 |
raise SemanticError(f"Syntax Error: Expected '{{' after '=' in array initialization.", line)
|
| 475 |
|
| 476 |
else:
|
| 477 |
-
# Uninitialized declaration: vine name; or seed x;
|
| 478 |
pass
|
| 479 |
|
| 480 |
error = symbol_table.declare_variable(var_name, var_type, is_list = is_list)
|
|
@@ -502,30 +445,20 @@ def parse_variable(tokens, index, var_name, var_type):
|
|
| 502 |
|
| 503 |
|
| 504 |
def _skip_semicolons(tokens, index):
|
| 505 |
-
"""Advance past any consecutive ';' tokens."""
|
| 506 |
while index < len(tokens) and tokens[index].type == ";":
|
| 507 |
index += 1
|
| 508 |
return index
|
| 509 |
|
| 510 |
|
| 511 |
-
# ============================================================================
|
| 512 |
-
# STATEMENT PARSER - The big dispatch
|
| 513 |
-
# Walks one statement at a time inside a function body. Recognizes every
|
| 514 |
-
# statement form: variable declaration, assignment, function call,
|
| 515 |
-
# spring/bud/wither, grow/cultivate/tend, harvest, plant, water,
|
| 516 |
-
# reclaim, prune, skip. Returns (ast_node, new_index).
|
| 517 |
-
# ============================================================================
|
| 518 |
def parse_statement(tokens, index, func_type = None):
|
| 519 |
token = tokens[index]
|
| 520 |
|
| 521 |
-
# Skip stray semicolons before the actual statement
|
| 522 |
if token.type == ";":
|
| 523 |
return None, index + 1
|
| 524 |
|
| 525 |
line = token.line
|
| 526 |
|
| 527 |
if token.value in {"seed", "tree", "vine", "leaf", "branch"}:
|
| 528 |
-
# Normalize GAL type keywords to internal types
|
| 529 |
var_type = token.value
|
| 530 |
var_name = tokens[index + 1].value
|
| 531 |
index += 2
|
|
@@ -539,30 +472,27 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 539 |
return node, index
|
| 540 |
|
| 541 |
elif token.value == "bundle":
|
| 542 |
-
# Local bundle variable declaration: bundle Person p; or bundle Person p[2];
|
| 543 |
bundle_type_name = tokens[index + 1].value
|
| 544 |
if bundle_type_name not in symbol_table.bundle_types:
|
| 545 |
raise SemanticError(f"Semantic Error: Bundle type '{bundle_type_name}' is not defined.", token.line)
|
| 546 |
var_name = tokens[index + 2].value
|
| 547 |
-
index += 3
|
| 548 |
|
| 549 |
members = symbol_table.bundle_types[bundle_type_name]
|
| 550 |
_defaults = {"seed": 0, "tree": 0.0, "leaf": '', "vine": "", "branch": False}
|
| 551 |
|
| 552 |
if tokens[index].type == "[":
|
| 553 |
-
|
| 554 |
-
index += 1 # skip '['
|
| 555 |
if tokens[index].type == "dbllit":
|
| 556 |
raise SemanticError(f"Semantic Error: Array size must be of type 'seed' (integer), got 'tree' (float) '{tokens[index].value}'.", token.line)
|
| 557 |
array_size = 0
|
| 558 |
if tokens[index].type == "intlit":
|
| 559 |
array_size = int(tokens[index].value)
|
| 560 |
-
index += 1
|
| 561 |
if tokens[index].type != "]":
|
| 562 |
raise SemanticError(f"Syntax Error: Expected ']' after array size.", token.line)
|
| 563 |
-
index += 1
|
| 564 |
|
| 565 |
-
# Build list of bundle default values
|
| 566 |
list_node = ASTNode("List", line=token.line)
|
| 567 |
for _ in range(array_size):
|
| 568 |
bundle_val_node = ASTNode("BundleDefault", line=token.line)
|
|
@@ -625,17 +555,15 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 625 |
list_access_node, index = parse_list_access(tokens, index)
|
| 626 |
|
| 627 |
if tokens[index + 1].type == "." and var_type in symbol_table.bundle_types:
|
| 628 |
-
|
| 629 |
-
index += 2 # skip ']' and '.'
|
| 630 |
member_name = tokens[index].value
|
| 631 |
bundle_members = symbol_table.bundle_types[var_type]
|
| 632 |
if member_name not in bundle_members:
|
| 633 |
raise SemanticError(f"Semantic Error: Bundle type '{var_type}' has no member '{member_name}'.", line)
|
| 634 |
member_type = bundle_members[member_name]
|
| 635 |
target = ArrayMemberAccessNode(list_access_node, member_name, line=line)
|
| 636 |
-
index += 1
|
| 637 |
|
| 638 |
-
# Handle chained access for nested bundles: p[0].addr.zip
|
| 639 |
while tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 640 |
next_member = tokens[index + 1].value
|
| 641 |
nested_members = symbol_table.bundle_types[member_type]
|
|
@@ -643,10 +571,10 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 643 |
raise SemanticError(f"Semantic Error: Bundle type '{member_type}' has no member '{next_member}'.", line)
|
| 644 |
member_type = nested_members[next_member]
|
| 645 |
target = MemberAccessNode(target, next_member, line=line)
|
| 646 |
-
index += 2
|
| 647 |
|
| 648 |
if tokens[index].type == "=":
|
| 649 |
-
index += 1
|
| 650 |
value_node, index = parse_expression_type(tokens, index, member_type)
|
| 651 |
assign_node = AssignmentNode(target, value_node, line=line)
|
| 652 |
assignments_node.add_child(assign_node)
|
|
@@ -667,17 +595,17 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 667 |
index += 2
|
| 668 |
if tokens[index].value == "water":
|
| 669 |
water_line = tokens[index].line
|
| 670 |
-
index += 1
|
| 671 |
if tokens[index].type != "(":
|
| 672 |
raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
|
| 673 |
-
index += 1
|
| 674 |
water_type = None
|
| 675 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 676 |
water_type = tokens[index].value
|
| 677 |
index += 1
|
| 678 |
if tokens[index].type != ")":
|
| 679 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter.", water_line)
|
| 680 |
-
index += 1
|
| 681 |
if water_type and not _types_compatible(var_type, water_type):
|
| 682 |
raise SemanticError(f"Semantic Error: Type mismatch — cannot assign water({water_type}) to '{var_type}' list element.", water_line)
|
| 683 |
value_node = ASTNode("Input", f"water({water_type})" if water_type else "water()", line=water_line)
|
|
@@ -698,20 +626,17 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 698 |
|
| 699 |
|
| 700 |
elif tokens[index + 1].type == ".":
|
| 701 |
-
# Bundle member assignment: p.age = 19; or p.addr.zip = 1000;
|
| 702 |
obj_name = tokens[index].value
|
| 703 |
member_name = tokens[index + 2].value
|
| 704 |
-
# Validate bundle type and member
|
| 705 |
if var_type not in symbol_table.bundle_types:
|
| 706 |
raise SemanticError(f"Semantic Error: Variable '{obj_name}' is not a bundle type.", line)
|
| 707 |
bundle_members = symbol_table.bundle_types[var_type]
|
| 708 |
if member_name not in bundle_members:
|
| 709 |
raise SemanticError(f"Semantic Error: Bundle type '{var_type}' has no member '{member_name}'.", line)
|
| 710 |
member_type = bundle_members[member_name]
|
| 711 |
-
index += 3
|
| 712 |
target = MemberAccessNode(obj_name, member_name, line=line)
|
| 713 |
|
| 714 |
-
# Handle chained access for nested bundles: p.addr.zip
|
| 715 |
while tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 716 |
next_member = tokens[index + 1].value
|
| 717 |
nested_members = symbol_table.bundle_types[member_type]
|
|
@@ -719,24 +644,23 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 719 |
raise SemanticError(f"Semantic Error: Bundle type '{member_type}' has no member '{next_member}'.", line)
|
| 720 |
member_type = nested_members[next_member]
|
| 721 |
target = MemberAccessNode(target, next_member, line=line)
|
| 722 |
-
index += 2
|
| 723 |
|
| 724 |
if tokens[index].type == "=":
|
| 725 |
-
index += 1
|
| 726 |
-
# Handle water() input for bundle members
|
| 727 |
if tokens[index].value == "water":
|
| 728 |
water_line = tokens[index].line
|
| 729 |
-
index += 1
|
| 730 |
if tokens[index].type != "(":
|
| 731 |
raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
|
| 732 |
-
index += 1
|
| 733 |
water_type = None
|
| 734 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 735 |
water_type = tokens[index].value
|
| 736 |
index += 1
|
| 737 |
if tokens[index].type != ")":
|
| 738 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter (e.g., water(seed)).", water_line)
|
| 739 |
-
index += 1
|
| 740 |
if water_type and not _types_compatible(member_type, water_type):
|
| 741 |
raise SemanticError(f"Semantic Error: Type mismatch — cannot assign water({water_type}) to '{member_type}' member '{member_name}'.", water_line)
|
| 742 |
value_node = ASTNode("Input", f"water({water_type})" if water_type else "water()", line=water_line)
|
|
@@ -745,22 +669,20 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 745 |
assign_node = AssignmentNode(target, value_node, line=line)
|
| 746 |
assignments_node.add_child(assign_node)
|
| 747 |
elif tokens[index].type in {"+=", "-=", "*=", "/=", "%=", "**="}:
|
| 748 |
-
# Compound assignment on bundle member: p.age += 1
|
| 749 |
if member_type not in {"seed", "tree"}:
|
| 750 |
raise SemanticError(f"Semantic Error: Compound assignment '{tokens[index].value}' is not valid for '{member_type}' member '{member_name}'.", line)
|
| 751 |
compound_op = tokens[index].value
|
| 752 |
base_op = compound_op[:-1]
|
| 753 |
-
index += 1
|
| 754 |
rhs_node, index = parse_expression_type(tokens, index, member_type)
|
| 755 |
value_node = BinaryOpNode(target, base_op, rhs_node, line=line)
|
| 756 |
assign_node = AssignmentNode(target, value_node, line=line)
|
| 757 |
assignments_node.add_child(assign_node)
|
| 758 |
elif tokens[index].type in {"++", "--"}:
|
| 759 |
-
# Postfix increment/decrement on bundle member: p.age++;
|
| 760 |
if member_type not in {"seed", "tree"}:
|
| 761 |
raise SemanticError(f"Semantic Error: Cannot apply '{tokens[index].value}' to member '{member_name}' of type '{member_type}'.", line)
|
| 762 |
operator = tokens[index].value
|
| 763 |
-
index += 1
|
| 764 |
assignments_node.add_child(UnaryOpNode(operator, target, "post", line=line))
|
| 765 |
else:
|
| 766 |
raise SemanticError(f"Semantic Error: Expected '=' after '{obj_name}.{member_name}'.", line)
|
|
@@ -789,9 +711,8 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 789 |
assignments_node.add_child(UnaryOpNode(operator, operand, "post", line=line))
|
| 790 |
|
| 791 |
elif tokens[index + 1].type in {"+=", "-=", "*=", "/=", "%=", "**="}:
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
base_op = compound_op[:-1] # e.g. "+" from "+=", "**" from "**="
|
| 795 |
cur_var_name = tokens[index].value
|
| 796 |
cur_var_info = symbol_table.lookup_variable(cur_var_name)
|
| 797 |
if isinstance(cur_var_info, str):
|
|
@@ -801,15 +722,13 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 801 |
cur_var_type = cur_var_info["type"]
|
| 802 |
if cur_var_type not in {"seed", "tree"}:
|
| 803 |
raise SemanticError(f"Semantic Error: Cannot use compound assignment on '{cur_var_name}' of type '{cur_var_type}'.", line)
|
| 804 |
-
# ── modulo-assign guard: %= requires seed ──
|
| 805 |
if base_op == "%" and cur_var_type != "seed":
|
| 806 |
raise SemanticError(
|
| 807 |
f"Semantic Error: Modulo operator '%' requires 'seed' (integer) operands, "
|
| 808 |
f"but '{cur_var_name}' is of type 'tree'.",
|
| 809 |
line,
|
| 810 |
)
|
| 811 |
-
|
| 812 |
-
index += 2 # skip id and compound op
|
| 813 |
rhs_node, index, rhs_type = parse_expression(tokens, index)
|
| 814 |
if rhs_type not in {"seed", "tree"}:
|
| 815 |
raise SemanticError(
|
|
@@ -834,15 +753,13 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 834 |
if isinstance(var_info, str):
|
| 835 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' used before declaration.", line)
|
| 836 |
|
| 837 |
-
# ++arr[i]; — prefix on indexed array element
|
| 838 |
if tokens[index + 1].type == "[":
|
| 839 |
if var_info["type"] not in {"seed", "tree"}:
|
| 840 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_info['type']} in expression.", line)
|
| 841 |
list_access_node, index = parse_list_access(tokens, index)
|
| 842 |
-
index += 1
|
| 843 |
assignments_node.add_child(UnaryOpNode(operator, list_access_node, "pre", line=line))
|
| 844 |
|
| 845 |
-
# ++obj.field; — prefix on bundle member
|
| 846 |
elif tokens[index + 1].type == ".":
|
| 847 |
obj_name = tokens[index].value
|
| 848 |
if var_info["type"] not in symbol_table.bundle_types:
|
|
@@ -852,9 +769,8 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 852 |
if member_name not in bundle_members:
|
| 853 |
raise SemanticError(f"Semantic Error: Bundle type '{var_info['type']}' has no member '{member_name}'.", line)
|
| 854 |
member_type = bundle_members[member_name]
|
| 855 |
-
index += 3
|
| 856 |
target = MemberAccessNode(obj_name, member_name, line=line)
|
| 857 |
-
# nested member access: ++p.addr.zip
|
| 858 |
while tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 859 |
next_member = tokens[index + 1].value
|
| 860 |
nested_members = symbol_table.bundle_types[member_type]
|
|
@@ -867,7 +783,6 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 867 |
raise SemanticError(f"Semantic Error: Cannot apply '{operator}' to member '{member_name}' of type '{member_type}'.", line)
|
| 868 |
assignments_node.add_child(UnaryOpNode(operator, target, "pre", line=line))
|
| 869 |
|
| 870 |
-
# ++x; — prefix on plain identifier (original path)
|
| 871 |
else:
|
| 872 |
if var_info["type"] not in {"seed", "tree"}:
|
| 873 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_info['type']} in expression.", line)
|
|
@@ -943,14 +858,13 @@ def parse_statement(tokens, index, func_type = None):
|
|
| 943 |
raise SemanticError(f"Semantic Error: '{token.value}' statement used outside a 'harvest' block.", line)
|
| 944 |
|
| 945 |
elif token.type == "{":
|
| 946 |
-
|
| 947 |
-
index += 1 # skip '{'
|
| 948 |
block_node = ASTNode("Block", line=line)
|
| 949 |
while tokens[index].type != "}":
|
| 950 |
stmt, index = parse_statement(tokens, index, func_type)
|
| 951 |
if stmt:
|
| 952 |
block_node.add_child(stmt)
|
| 953 |
-
index += 1
|
| 954 |
return block_node, index
|
| 955 |
|
| 956 |
else:
|
|
@@ -973,7 +887,6 @@ def parse_list_access(tokens, index):
|
|
| 973 |
|
| 974 |
index_node, index, idx_type = parse_equality(tokens, index)
|
| 975 |
|
| 976 |
-
# Array indices must be seed (integer)
|
| 977 |
if idx_type is not None and idx_type != "seed":
|
| 978 |
raise SemanticError(
|
| 979 |
f"Semantic Error: List index must be of type 'seed', got '{idx_type}'.",
|
|
@@ -988,9 +901,8 @@ def parse_list_access(tokens, index):
|
|
| 988 |
|
| 989 |
node = ListAccessNode(list_name, index_wrapper, line=line)
|
| 990 |
|
| 991 |
-
# Handle multi-dimensional access: m[1][2]
|
| 992 |
while tokens[index + 1].type == "[":
|
| 993 |
-
index += 2
|
| 994 |
inner_index_node, index, inner_idx_type = parse_equality(tokens, index)
|
| 995 |
if inner_idx_type is not None and inner_idx_type != "seed":
|
| 996 |
raise SemanticError(
|
|
@@ -1007,7 +919,6 @@ def parse_list_access(tokens, index):
|
|
| 1007 |
|
| 1008 |
|
| 1009 |
def parse_list_assignment(tokens, index):
|
| 1010 |
-
"""Parses full list assignment, list operations, or function calls on a list."""
|
| 1011 |
line = tokens[index].line
|
| 1012 |
var_name = tokens[index].value
|
| 1013 |
|
|
@@ -1025,7 +936,6 @@ def parse_list_assignment(tokens, index):
|
|
| 1025 |
if var_info.get("is_fertile", False):
|
| 1026 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' is declared as fertile.", line)
|
| 1027 |
|
| 1028 |
-
# List functions
|
| 1029 |
if tokens[index].value == "append":
|
| 1030 |
value_node, index = parse_append(tokens, index, var_name, var_type)
|
| 1031 |
|
|
@@ -1035,7 +945,6 @@ def parse_list_assignment(tokens, index):
|
|
| 1035 |
elif tokens[index].value == "remove":
|
| 1036 |
value_node, index = parse_remove(tokens, index, var_name, var_type)
|
| 1037 |
|
| 1038 |
-
# Assignment from another list
|
| 1039 |
elif tokens[index].type == "id":
|
| 1040 |
source_var = tokens[index].value
|
| 1041 |
source_info = symbol_table.lookup_variable(source_var)
|
|
@@ -1076,7 +985,6 @@ def parse_list_assignment(tokens, index):
|
|
| 1076 |
value_node = ASTNode("Value", source_var, line=line)
|
| 1077 |
index += 1
|
| 1078 |
|
| 1079 |
-
# Assignment from list literal
|
| 1080 |
elif tokens[index].type == "[":
|
| 1081 |
value_node, index = parse_list(tokens, index, var_type)
|
| 1082 |
|
|
@@ -1087,13 +995,10 @@ def parse_list_assignment(tokens, index):
|
|
| 1087 |
|
| 1088 |
|
| 1089 |
def _types_compatible(declared, inferred):
|
| 1090 |
-
"""Check whether *inferred* expression type can be stored in a *declared* variable."""
|
| 1091 |
if declared == inferred:
|
| 1092 |
return True
|
| 1093 |
-
# seed (int) and tree (float) are mutually compatible
|
| 1094 |
if declared in {"seed", "tree"} and inferred in {"seed", "tree"}:
|
| 1095 |
return True
|
| 1096 |
-
# Bundle types are compatible if they are the same type (checked above)
|
| 1097 |
return False
|
| 1098 |
|
| 1099 |
|
|
@@ -1105,7 +1010,6 @@ def parse_expression_type(tokens, index, var_type):
|
|
| 1105 |
|
| 1106 |
node, index, expr_type = parse_assignment_expression(tokens, index)
|
| 1107 |
|
| 1108 |
-
# --- type-mismatch guard ---
|
| 1109 |
if expr_type is None:
|
| 1110 |
raise SemanticError(
|
| 1111 |
"Semantic Error: Could not determine the type of the expression.",
|
|
@@ -1172,7 +1076,7 @@ def parse_expression_leaf(tokens, index):
|
|
| 1172 |
func_name = tokens[index].value
|
| 1173 |
func_info = symbol_table.lookup_function(func_name)
|
| 1174 |
|
| 1175 |
-
if isinstance(func_info, str):
|
| 1176 |
raise SemanticError(f"Semantic Error: Function '{func_name}' is not defined.", line)
|
| 1177 |
|
| 1178 |
func_return_type = func_info["return_type"]
|
|
@@ -1204,15 +1108,14 @@ def parse_expression_leaf(tokens, index):
|
|
| 1204 |
index += 1
|
| 1205 |
node = ListAccessNode(list_name, index_node, line=token.line)
|
| 1206 |
|
| 1207 |
-
# Handle multi-dimensional access: m[1][2]
|
| 1208 |
while index < len(tokens) and tokens[index].type == "[":
|
| 1209 |
-
index += 1
|
| 1210 |
inner_expr, index, _ = parse_expression(tokens, index)
|
| 1211 |
if tokens[index].type != "]":
|
| 1212 |
raise SemanticError("Syntax Error: Missing closing bracket.", token.line)
|
| 1213 |
inner_index = ASTNode("Index", line=token.line)
|
| 1214 |
inner_index.add_child(inner_expr)
|
| 1215 |
-
index += 1
|
| 1216 |
node = ListAccessNode(node, inner_index, line=token.line)
|
| 1217 |
|
| 1218 |
elif tokens[index].type == "id":
|
|
@@ -1245,7 +1148,7 @@ def parse_expression_leaf(tokens, index):
|
|
| 1245 |
func_name = tokens[index].value
|
| 1246 |
func_info = symbol_table.lookup_function(func_name)
|
| 1247 |
|
| 1248 |
-
if isinstance(func_info, str):
|
| 1249 |
raise SemanticError(f"Semantic Error: Function '{func_name}' is not defined.", line)
|
| 1250 |
|
| 1251 |
func_return_type = func_info["return_type"]
|
|
@@ -1302,21 +1205,7 @@ def parse_expression_leaf(tokens, index):
|
|
| 1302 |
return left_node, index
|
| 1303 |
|
| 1304 |
|
| 1305 |
-
# ============================================================================
|
| 1306 |
-
# EXPRESSION PARSER - Operator-precedence climbing
|
| 1307 |
-
# Standard precedence ladder built from these layered functions:
|
| 1308 |
-
# parse_expression : + - ` (lowest precedence)
|
| 1309 |
-
# parse_term : * / %
|
| 1310 |
-
# parse_power : ** (right-associative)
|
| 1311 |
-
# parse_unary : ~ ! ++ --
|
| 1312 |
-
# parse_factor : literals, identifiers, function calls,
|
| 1313 |
-
# parenthesized expressions (highest)
|
| 1314 |
-
# Each layer calls the next-higher-precedence layer for its operands.
|
| 1315 |
-
# All return (ast_node, new_index, inferred_type).
|
| 1316 |
-
# ============================================================================
|
| 1317 |
def parse_expression(tokens, index):
|
| 1318 |
-
"""Parses an expression with +, -, and ` (string concat).
|
| 1319 |
-
Returns (node, index, type_str)."""
|
| 1320 |
left_node, index, left_type = parse_term(tokens, index)
|
| 1321 |
|
| 1322 |
while tokens[index].type in {"+", "-", "`"}:
|
|
@@ -1324,7 +1213,6 @@ def parse_expression(tokens, index):
|
|
| 1324 |
token = tokens[index]
|
| 1325 |
|
| 1326 |
if op == "`":
|
| 1327 |
-
# Backtick joins vines and leaves; the resulting string is a vine.
|
| 1328 |
if left_type not in {"vine", "leaf"}:
|
| 1329 |
raise SemanticError(
|
| 1330 |
f"Semantic Error: Cannot concatenate - left operand is of type '{left_type}', expected 'vine' or 'leaf'.",
|
|
@@ -1340,7 +1228,6 @@ def parse_expression(tokens, index):
|
|
| 1340 |
left_node = BinaryOpNode(left_node, op, right_node)
|
| 1341 |
left_type = "vine"
|
| 1342 |
else:
|
| 1343 |
-
# + or -: both operands must be numeric (seed or tree)
|
| 1344 |
if left_type not in {"seed", "tree"}:
|
| 1345 |
raise SemanticError(
|
| 1346 |
f"Semantic Error: Cannot use '{op}' on type '{left_type}'. Expected 'seed' or 'tree'.",
|
|
@@ -1354,29 +1241,24 @@ def parse_expression(tokens, index):
|
|
| 1354 |
token.line,
|
| 1355 |
)
|
| 1356 |
left_node = BinaryOpNode(left_node, op, right_node)
|
| 1357 |
-
# Promote to tree if either operand is tree
|
| 1358 |
if left_type == "tree" or right_type == "tree":
|
| 1359 |
left_type = "tree"
|
| 1360 |
|
| 1361 |
return left_node, index, left_type
|
| 1362 |
|
| 1363 |
def parse_term(tokens, index):
|
| 1364 |
-
"""Parses multiplication, division, and modulus with type checking.
|
| 1365 |
-
Returns (node, index, type_str)."""
|
| 1366 |
left_node, index, left_type = parse_power(tokens, index)
|
| 1367 |
|
| 1368 |
while tokens[index].type in {"*", "/", "%"}:
|
| 1369 |
op = tokens[index].value
|
| 1370 |
token = tokens[index]
|
| 1371 |
|
| 1372 |
-
# Validate left operand is numeric
|
| 1373 |
if left_type not in {"seed", "tree"}:
|
| 1374 |
raise SemanticError(
|
| 1375 |
f"Semantic Error: Cannot use '{op}' on type '{left_type}'. Expected 'seed' or 'tree'.",
|
| 1376 |
token.line,
|
| 1377 |
)
|
| 1378 |
|
| 1379 |
-
# ── modulo type guard: % requires seed (integer) operands ──
|
| 1380 |
if op == "%":
|
| 1381 |
if left_type == "tree":
|
| 1382 |
raise SemanticError(
|
|
@@ -1388,7 +1270,6 @@ def parse_term(tokens, index):
|
|
| 1388 |
index += 1
|
| 1389 |
right_node, index, right_type = parse_power(tokens, index)
|
| 1390 |
|
| 1391 |
-
# Validate right operand is numeric
|
| 1392 |
if right_type not in {"seed", "tree"}:
|
| 1393 |
raise SemanticError(
|
| 1394 |
f"Semantic Error: Cannot use '{op}' on type '{right_type}'. Expected 'seed' or 'tree'.",
|
|
@@ -1410,15 +1291,12 @@ def parse_term(tokens, index):
|
|
| 1410 |
pass
|
| 1411 |
|
| 1412 |
left_node = BinaryOpNode(left_node, op, right_node)
|
| 1413 |
-
# Promote to tree if either operand is tree
|
| 1414 |
if left_type == "tree" or right_type == "tree":
|
| 1415 |
left_type = "tree"
|
| 1416 |
|
| 1417 |
return left_node, index, left_type
|
| 1418 |
|
| 1419 |
def parse_power(tokens, index):
|
| 1420 |
-
"""Parses right-associative exponentiation (**).
|
| 1421 |
-
Returns (node, index, type_str)."""
|
| 1422 |
left_node, index, left_type = parse_unary(tokens, index)
|
| 1423 |
|
| 1424 |
if tokens[index].type == "**":
|
|
@@ -1447,7 +1325,6 @@ def parse_power(tokens, index):
|
|
| 1447 |
return left_node, index, left_type
|
| 1448 |
|
| 1449 |
def parse_unary(tokens, index):
|
| 1450 |
-
"""Returns (node, index, type_str)."""
|
| 1451 |
|
| 1452 |
if tokens[index].type in {"++", "--", "-", "~"}:
|
| 1453 |
op = tokens[index].value
|
|
@@ -1480,9 +1357,6 @@ def parse_unary(tokens, index):
|
|
| 1480 |
return node, index, factor_type
|
| 1481 |
|
| 1482 |
def parse_cast(tokens, index):
|
| 1483 |
-
"""Parses explicit type casting: (type)expression.
|
| 1484 |
-
Supports all GAL types: seed, tree, leaf, branch, vine.
|
| 1485 |
-
Returns (node, index, type_str)."""
|
| 1486 |
token = tokens[index]
|
| 1487 |
cast_types = {"seed", "tree", "leaf", "branch", "vine"}
|
| 1488 |
if token.type == "(" and tokens[index + 1].value in cast_types:
|
|
@@ -1500,8 +1374,6 @@ def parse_cast(tokens, index):
|
|
| 1500 |
|
| 1501 |
|
| 1502 |
def parse_factor(tokens, index):
|
| 1503 |
-
"""Parses literals, identifiers, parenthesized expressions, and postfix operators.
|
| 1504 |
-
Returns (node, index, type_str)."""
|
| 1505 |
token = tokens[index]
|
| 1506 |
|
| 1507 |
if token.type == "(" and tokens[index + 1].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
|
@@ -1521,7 +1393,6 @@ def parse_factor(tokens, index):
|
|
| 1521 |
index += 1
|
| 1522 |
return node, index, infer_literal_type(token.type)
|
| 1523 |
|
| 1524 |
-
# water() is an I/O statement, not an expression
|
| 1525 |
if token.value == "water":
|
| 1526 |
raise SemanticError(f"Semantic Error: water() is an I/O statement, not an expression. It cannot be used inside an expression.", token.line)
|
| 1527 |
|
|
@@ -1582,7 +1453,6 @@ def parse_factor(tokens, index):
|
|
| 1582 |
tokens[index].type == "id" and
|
| 1583 |
tokens[index + 1].type == "."
|
| 1584 |
):
|
| 1585 |
-
# Bundle member access in expression: p.age, p.name
|
| 1586 |
obj_name = tokens[index].value
|
| 1587 |
member_name = tokens[index + 2].value
|
| 1588 |
|
|
@@ -1599,10 +1469,9 @@ def parse_factor(tokens, index):
|
|
| 1599 |
raise SemanticError(f"Semantic Error: Bundle type '{var_type}' has no member '{member_name}'.", token.line)
|
| 1600 |
|
| 1601 |
member_type = bundle_members[member_name]
|
| 1602 |
-
index += 3
|
| 1603 |
node = MemberAccessNode(obj_name, member_name, line=token.line)
|
| 1604 |
|
| 1605 |
-
# Handle chained access for nested bundles: p.addr.zip
|
| 1606 |
while index < len(tokens) and tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 1607 |
next_member = tokens[index + 1].value
|
| 1608 |
nested_members = symbol_table.bundle_types[member_type]
|
|
@@ -1610,7 +1479,7 @@ def parse_factor(tokens, index):
|
|
| 1610 |
raise SemanticError(f"Semantic Error: Bundle type '{member_type}' has no member '{next_member}'.", token.line)
|
| 1611 |
member_type = nested_members[next_member]
|
| 1612 |
node = MemberAccessNode(node, next_member, line=token.line)
|
| 1613 |
-
index += 2
|
| 1614 |
|
| 1615 |
return node, index, member_type
|
| 1616 |
|
|
@@ -1637,29 +1506,26 @@ def parse_factor(tokens, index):
|
|
| 1637 |
index += 1
|
| 1638 |
list_access_node = ListAccessNode(list_name, index_node, line=token.line)
|
| 1639 |
|
| 1640 |
-
# Handle multi-dimensional access: m[1][2]
|
| 1641 |
while index < len(tokens) and tokens[index].type == "[":
|
| 1642 |
-
index += 1
|
| 1643 |
inner_expr, index, _ = parse_expression(tokens, index)
|
| 1644 |
if tokens[index].type != "]":
|
| 1645 |
raise SemanticError("Syntax Error: Missing closing bracket.", token.line)
|
| 1646 |
inner_index = ASTNode("Index", line=token.line)
|
| 1647 |
inner_index.add_child(inner_expr)
|
| 1648 |
-
index += 1
|
| 1649 |
list_access_node = ListAccessNode(list_access_node, inner_index, line=token.line)
|
| 1650 |
|
| 1651 |
-
# Handle bundle array member access: p[0].x or p[0].addr.zip
|
| 1652 |
if index < len(tokens) and tokens[index].type == "." and list_info["type"] in symbol_table.bundle_types:
|
| 1653 |
-
index += 1
|
| 1654 |
member_name = tokens[index].value
|
| 1655 |
bundle_members = symbol_table.bundle_types[list_info["type"]]
|
| 1656 |
if member_name not in bundle_members:
|
| 1657 |
raise SemanticError(f"Semantic Error: Bundle type '{list_info['type']}' has no member '{member_name}'.", token.line)
|
| 1658 |
member_type = bundle_members[member_name]
|
| 1659 |
-
index += 1
|
| 1660 |
node = ArrayMemberAccessNode(list_access_node, member_name, line=token.line)
|
| 1661 |
|
| 1662 |
-
# Handle chained access for nested bundles: p[0].addr.zip
|
| 1663 |
while index < len(tokens) and tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 1664 |
next_member = tokens[index + 1].value
|
| 1665 |
nested_members = symbol_table.bundle_types[member_type]
|
|
@@ -1667,7 +1533,7 @@ def parse_factor(tokens, index):
|
|
| 1667 |
raise SemanticError(f"Semantic Error: Bundle type '{member_type}' has no member '{next_member}'.", token.line)
|
| 1668 |
member_type = nested_members[next_member]
|
| 1669 |
node = MemberAccessNode(node, next_member, line=token.line)
|
| 1670 |
-
index += 2
|
| 1671 |
|
| 1672 |
return node, index, member_type
|
| 1673 |
|
|
@@ -1704,7 +1570,6 @@ def parse_factor(tokens, index):
|
|
| 1704 |
|
| 1705 |
|
| 1706 |
def _assignment_root_name(node):
|
| 1707 |
-
"""Return the declared root variable of an assignable AST node."""
|
| 1708 |
if node.node_type in {"Identifier", "Value", "Object", "ListName"}:
|
| 1709 |
if isinstance(node.value, ASTNode):
|
| 1710 |
return _assignment_root_name(node.value)
|
|
@@ -1715,7 +1580,6 @@ def _assignment_root_name(node):
|
|
| 1715 |
|
| 1716 |
|
| 1717 |
def _assignment_target(node, line):
|
| 1718 |
-
"""Validate and normalize an assignment-expression left-hand side."""
|
| 1719 |
root_name = _assignment_root_name(node)
|
| 1720 |
valid_node_types = {"Value", "Identifier", "ListAccess", "MemberAccess", "ArrayMemberAccess"}
|
| 1721 |
var_info = symbol_table.lookup_variable(root_name) if root_name is not None else None
|
|
@@ -1737,7 +1601,6 @@ def _assignment_target(node, line):
|
|
| 1737 |
|
| 1738 |
|
| 1739 |
def parse_assignment_expression(tokens, index):
|
| 1740 |
-
"""Parse right-associative assignments and infer their stored value type."""
|
| 1741 |
line = tokens[index].line
|
| 1742 |
left_node, index, left_type = parse_logical_expression(tokens, index)
|
| 1743 |
if tokens[index].type not in {"=", "+=", "-=", "*=", "/=", "%="}:
|
|
@@ -1772,12 +1635,10 @@ def parse_assignment_expression(tokens, index):
|
|
| 1772 |
|
| 1773 |
|
| 1774 |
def parse_expression_branch(tokens, index):
|
| 1775 |
-
"""Parse the full expression form used by conditions and parentheses."""
|
| 1776 |
return parse_assignment_expression(tokens, index)
|
| 1777 |
|
| 1778 |
|
| 1779 |
def parse_logical_expression(tokens, index):
|
| 1780 |
-
"""Parses logical expressions with &&/|| operators for branch type."""
|
| 1781 |
line = tokens[index].line
|
| 1782 |
left_node, index, left_type = parse_equality(tokens, index)
|
| 1783 |
|
|
@@ -1800,7 +1661,6 @@ def parse_logical_expression(tokens, index):
|
|
| 1800 |
|
| 1801 |
|
| 1802 |
def parse_equality(tokens, index):
|
| 1803 |
-
"""Parses equality expressions with == or !=."""
|
| 1804 |
line = tokens[index].line
|
| 1805 |
left_node, index, left_type = parse_relational(tokens, index)
|
| 1806 |
|
|
@@ -1809,7 +1669,6 @@ def parse_equality(tokens, index):
|
|
| 1809 |
index += 1
|
| 1810 |
right_node, index, right_type = parse_relational(tokens, index)
|
| 1811 |
|
| 1812 |
-
# ── type guard: equality operators require compatible types ──
|
| 1813 |
if left_type is None or right_type is None:
|
| 1814 |
raise SemanticError(
|
| 1815 |
"Semantic Error: Could not determine the type of an operand in equality check.",
|
|
@@ -1821,7 +1680,6 @@ def parse_equality(tokens, index):
|
|
| 1821 |
f"Semantic Error: Cannot compare '{left_type}' with '{right_type}' using '{operator}'.",
|
| 1822 |
line,
|
| 1823 |
)
|
| 1824 |
-
# ──────────────────────────────────────────────────────────────
|
| 1825 |
|
| 1826 |
left_node = BinaryOpNode(left_node, operator, right_node, line=line)
|
| 1827 |
left_type = "branch"
|
|
@@ -1830,7 +1688,6 @@ def parse_equality(tokens, index):
|
|
| 1830 |
|
| 1831 |
|
| 1832 |
def parse_relational(tokens, index):
|
| 1833 |
-
"""Parses relational expressions with <, >, <=, >= or ! unary operator."""
|
| 1834 |
line = tokens[index].line
|
| 1835 |
|
| 1836 |
if tokens[index].type == "!":
|
|
@@ -1855,7 +1712,6 @@ def parse_relational(tokens, index):
|
|
| 1855 |
line,
|
| 1856 |
)
|
| 1857 |
|
| 1858 |
-
# ── type guard: relational operators require compatible types ──
|
| 1859 |
if left_type and right_type:
|
| 1860 |
numeric = {"seed", "tree"}
|
| 1861 |
if left_type in numeric and right_type not in numeric:
|
|
@@ -1873,7 +1729,6 @@ def parse_relational(tokens, index):
|
|
| 1873 |
f"Semantic Error: Cannot compare '{left_type}' with '{right_type}' using '{operator}'.",
|
| 1874 |
line,
|
| 1875 |
)
|
| 1876 |
-
# ──────────────────────────────────────────────────────────────
|
| 1877 |
|
| 1878 |
left_node = BinaryOpNode(left_node, operator, right_node, line=line)
|
| 1879 |
left_type = "branch"
|
|
@@ -1905,15 +1760,12 @@ def check_lwk(tokens, index):
|
|
| 1905 |
return op_found, start_index
|
| 1906 |
|
| 1907 |
def parse_operand(tokens, index):
|
| 1908 |
-
"""Determines the parsing function based on the operand type."""
|
| 1909 |
token = tokens[index]
|
| 1910 |
line = token.line
|
| 1911 |
|
| 1912 |
-
# water() is an I/O statement, not an expression
|
| 1913 |
if token.value == "water":
|
| 1914 |
raise SemanticError(f"Semantic Error: water() is an I/O statement, not an expression. It cannot be used inside an expression.", token.line)
|
| 1915 |
|
| 1916 |
-
# Handle parentheses
|
| 1917 |
if token.type == "(":
|
| 1918 |
if tokens[index+1].value in {"seed", "tree"}:
|
| 1919 |
expr_type = tokens[index+1].value
|
|
@@ -1946,18 +1798,15 @@ def parse_operand(tokens, index):
|
|
| 1946 |
index += 1
|
| 1947 |
return expr_node, index, expr_type
|
| 1948 |
|
| 1949 |
-
# Chungus or Chudeluxe (arithmetic types)
|
| 1950 |
if token.type in {"intlit", "dbllit"}:
|
| 1951 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1952 |
return expr_node, index, infer_literal_type(token.type)
|
| 1953 |
|
| 1954 |
-
# Forsencd (String concatenation or manipulation)
|
| 1955 |
if token.type in {"chrlit", "stringlit"}:
|
| 1956 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1957 |
return expr_node, index, infer_literal_type(token.type)
|
| 1958 |
|
| 1959 |
|
| 1960 |
-
# Lwk (Boolean literal)
|
| 1961 |
if token.type in {"sunshine", "frost"}:
|
| 1962 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1963 |
return expr_node, index, infer_literal_type(token.type)
|
|
@@ -1985,36 +1834,29 @@ def parse_operand(tokens, index):
|
|
| 1985 |
if not list_info["is_list"] and list_info.get("type") != "vine":
|
| 1986 |
raise SemanticError(f"Semantic Error: '{list_name}' is not a list.", token.line)
|
| 1987 |
|
| 1988 |
-
# Delegate to parse_expression so arithmetic (a[0] + a[1]) is handled
|
| 1989 |
expr_node, index, expr_type = parse_expression(tokens, index)
|
| 1990 |
return expr_node, index, expr_type
|
| 1991 |
|
| 1992 |
-
# Chungus or Chudeluxe (arithmetic types)
|
| 1993 |
if token.type in {"intlit", "dbllit"}:
|
| 1994 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1995 |
return expr_node, index, infer_literal_type(token.type)
|
| 1996 |
|
| 1997 |
-
# Forsencd (String concatenation or manipulation)
|
| 1998 |
if token.type == "chrlit":
|
| 1999 |
expr_node, index = parse_expression_leaf(tokens, index)
|
| 2000 |
return expr_node, index, infer_literal_type(token.type)
|
| 2001 |
|
| 2002 |
-
# Forsen (String literal)
|
| 2003 |
if token.type == "stringlit":
|
| 2004 |
return ASTNode("Value", token.value, line=line), index + 1, "vine"
|
| 2005 |
|
| 2006 |
-
# Lwk (Boolean literal)
|
| 2007 |
if token.type in {"sunshine", "frost"}:
|
| 2008 |
return ASTNode("Value", token.value, line=line), index + 1, "branch"
|
| 2009 |
|
| 2010 |
-
# Bundle member access in operand context: p.age > 18 or p.addr.zip > 0
|
| 2011 |
if token.type == "id" and tokens[index + 1].type == ".":
|
| 2012 |
var_info = symbol_table.lookup_variable(token.value)
|
| 2013 |
if not isinstance(var_info, str) and var_info["type"] in symbol_table.bundle_types:
|
| 2014 |
expr_node, index, expr_type = parse_expression(tokens, index)
|
| 2015 |
return expr_node, index, expr_type
|
| 2016 |
|
| 2017 |
-
# Identifiers (Variables)
|
| 2018 |
if token.type == "id":
|
| 2019 |
var_info = symbol_table.lookup_variable(token.value)
|
| 2020 |
if isinstance(var_info, str):
|
|
@@ -2024,7 +1866,6 @@ def parse_operand(tokens, index):
|
|
| 2024 |
is_list = var_info.get("is_list", False)
|
| 2025 |
|
| 2026 |
if is_list and tokens[index + 1].type != "[":
|
| 2027 |
-
# Check if this is an array <op> array case (e.g., x = a + b)
|
| 2028 |
if tokens[index + 1].type in {"+", "-", "*", "/", "%", "**", "==", "!=", ">", "<", ">=", "<="}:
|
| 2029 |
op_token = tokens[index + 1]
|
| 2030 |
if index + 2 < len(tokens) and tokens[index + 2].type == "id":
|
|
@@ -2036,7 +1877,6 @@ def parse_operand(tokens, index):
|
|
| 2036 |
)
|
| 2037 |
raise SemanticError(f"Semantic Error: List '{token.value}' must be indexed with '[]' in expressions.", line)
|
| 2038 |
|
| 2039 |
-
# Dispatch to specific parsers based on type
|
| 2040 |
if var_type in {"seed", "tree", "branch"}:
|
| 2041 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 2042 |
|
|
@@ -2054,7 +1894,6 @@ def parse_operand(tokens, index):
|
|
| 2054 |
return ASTNode("Value", token.value, line=line), index + 1, var_type
|
| 2055 |
|
| 2056 |
elif var_type in symbol_table.bundle_types:
|
| 2057 |
-
# Bundle variable used directly — dispatch through expression parser
|
| 2058 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 2059 |
return expr_node, index, var_type
|
| 2060 |
|
|
@@ -2067,7 +1906,6 @@ def parse_operand(tokens, index):
|
|
| 2067 |
|
| 2068 |
|
| 2069 |
def infer_literal_type(token_type):
|
| 2070 |
-
"""Returns the type string for a given literal token type."""
|
| 2071 |
if token_type == "intlit":
|
| 2072 |
return "seed"
|
| 2073 |
if token_type == "dbllit":
|
|
@@ -2089,17 +1927,17 @@ def parse_assignment(tokens, index, var_name, var_type):
|
|
| 2089 |
|
| 2090 |
if tokens[index].value == "water":
|
| 2091 |
water_line = tokens[index].line
|
| 2092 |
-
index += 1
|
| 2093 |
if tokens[index].type != "(":
|
| 2094 |
raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
|
| 2095 |
-
index += 1
|
| 2096 |
water_type = None
|
| 2097 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 2098 |
water_type = tokens[index].value
|
| 2099 |
index += 1
|
| 2100 |
if tokens[index].type != ")":
|
| 2101 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter (e.g., water(seed)).", water_line)
|
| 2102 |
-
index += 1
|
| 2103 |
if water_type and not _types_compatible(var_type, water_type):
|
| 2104 |
raise SemanticError(f"Semantic Error: Type mismatch — cannot assign water({water_type}) to '{var_type}' variable.", water_line)
|
| 2105 |
value_node = ASTNode("Input", f"water({water_type})" if water_type else "water()", line=water_line)
|
|
@@ -2125,12 +1963,10 @@ def parse_function_call(tokens, index, func_name, func_type, func_params):
|
|
| 2125 |
|
| 2126 |
expected_type = expected_params[len(provided_args)].children[0].value
|
| 2127 |
|
| 2128 |
-
# Check if the expected parameter is an array parameter
|
| 2129 |
expected_param = expected_params[len(provided_args)]
|
| 2130 |
is_array_param = any(child.node_type == "ArrayParam" for child in expected_param.children)
|
| 2131 |
|
| 2132 |
if is_array_param:
|
| 2133 |
-
# Array parameter: expect an array identifier
|
| 2134 |
if tokens[index].type != "id":
|
| 2135 |
raise SemanticError(f"Semantic Error: Expected array variable for parameter {len(provided_args) + 1} of '{func_name}'.", line)
|
| 2136 |
arg_name = tokens[index].value
|
|
@@ -2165,7 +2001,7 @@ def parse_function_call(tokens, index, func_name, func_type, func_params):
|
|
| 2165 |
raise SemanticError(f"Semantic Error: Function '{func_name}' expects {len(expected_params)} arguments, but {len(provided_args)} were provided.", line)
|
| 2166 |
|
| 2167 |
for i, (arg_node, arg_type) in enumerate(provided_args):
|
| 2168 |
-
expected_type = expected_params[i].children[0].value
|
| 2169 |
|
| 2170 |
if expected_type in {"seed", "tree"} and arg_type == "seed":
|
| 2171 |
continue
|
|
@@ -2177,39 +2013,34 @@ def parse_function_call(tokens, index, func_name, func_type, func_params):
|
|
| 2177 |
|
| 2178 |
|
| 2179 |
def parse_water_statement(tokens, index):
|
| 2180 |
-
"""Parse water(variable); — reads input into an existing variable."""
|
| 2181 |
line = tokens[index].line
|
| 2182 |
-
index += 1
|
| 2183 |
|
| 2184 |
if tokens[index].type != "(":
|
| 2185 |
raise SemanticError(f"Syntax Error: Expected '(' after water.", line)
|
| 2186 |
-
index += 1
|
| 2187 |
|
| 2188 |
-
# Case 1: water() or water(data_type) — no variable target (expression form used as statement)
|
| 2189 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 2190 |
water_type = tokens[index].value
|
| 2191 |
index += 1
|
| 2192 |
if tokens[index].type != ")":
|
| 2193 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter or a variable name.", line)
|
| 2194 |
-
index += 1
|
| 2195 |
if tokens[index].type != ";":
|
| 2196 |
raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
|
| 2197 |
-
index += 1
|
| 2198 |
-
# Standalone typed water with no target — just returns input (discarded)
|
| 2199 |
input_node = ASTNode("Input", f"water({water_type})", line=line)
|
| 2200 |
return input_node, index
|
| 2201 |
|
| 2202 |
elif tokens[index].type == ")":
|
| 2203 |
-
|
| 2204 |
-
index += 1 # skip ')'
|
| 2205 |
if tokens[index].type != ";":
|
| 2206 |
raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
|
| 2207 |
-
index += 1
|
| 2208 |
input_node = ASTNode("Input", "water()", line=line)
|
| 2209 |
return input_node, index
|
| 2210 |
|
| 2211 |
elif tokens[index].type == "id":
|
| 2212 |
-
# water(variable) — read input into variable
|
| 2213 |
var_name = tokens[index].value
|
| 2214 |
var_info = symbol_table.lookup_variable(var_name)
|
| 2215 |
if isinstance(var_info, str):
|
|
@@ -2217,56 +2048,52 @@ def parse_water_statement(tokens, index):
|
|
| 2217 |
if var_info.get("is_fertile", False):
|
| 2218 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' is declared as fertile and cannot be re-assigned a value.", line)
|
| 2219 |
var_type = var_info["type"]
|
| 2220 |
-
index += 1
|
| 2221 |
|
| 2222 |
-
# Check for list element access: water(arr[i])
|
| 2223 |
if tokens[index].type == "[":
|
| 2224 |
if not var_info.get("is_list", False) and var_info.get("type") != "vine":
|
| 2225 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' is not a list.", line)
|
| 2226 |
-
index += 1
|
| 2227 |
index_expr, index, idx_type = parse_equality(tokens, index)
|
| 2228 |
if idx_type is not None and idx_type != "seed":
|
| 2229 |
raise SemanticError(f"Semantic Error: List index must be of type 'seed', got '{idx_type}'.", line)
|
| 2230 |
if tokens[index].type != "]":
|
| 2231 |
raise SemanticError(f"Syntax Error: Expected ']' after list index.", line)
|
| 2232 |
-
index += 1
|
| 2233 |
|
| 2234 |
-
# Build first dimension: arr[i]
|
| 2235 |
index_wrapper = ASTNode("Index", line=line)
|
| 2236 |
index_wrapper.add_child(index_expr)
|
| 2237 |
list_access_node = ListAccessNode(var_name, index_wrapper, line=line)
|
| 2238 |
|
| 2239 |
-
# Handle additional dimensions: water(arr[i][j][k]...)
|
| 2240 |
while tokens[index].type == "[":
|
| 2241 |
-
index += 1
|
| 2242 |
inner_expr, index, inner_type = parse_equality(tokens, index)
|
| 2243 |
if inner_type is not None and inner_type != "seed":
|
| 2244 |
raise SemanticError(f"Semantic Error: List index must be of type 'seed', got '{inner_type}'.", line)
|
| 2245 |
if tokens[index].type != "]":
|
| 2246 |
raise SemanticError(f"Syntax Error: Expected ']' after list index.", line)
|
| 2247 |
-
index += 1
|
| 2248 |
inner_wrapper = ASTNode("Index", line=line)
|
| 2249 |
inner_wrapper.add_child(inner_expr)
|
| 2250 |
list_access_node = ListAccessNode(list_access_node, inner_wrapper, line=line)
|
| 2251 |
|
| 2252 |
if tokens[index].type != ")":
|
| 2253 |
raise SemanticError(f"Semantic Error: Expected ')' after water(arr[i]).", line)
|
| 2254 |
-
index += 1
|
| 2255 |
if tokens[index].type != ";":
|
| 2256 |
raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
|
| 2257 |
-
index += 1
|
| 2258 |
input_node = ASTNode("Input", f"water({var_type})", line=line)
|
| 2259 |
assignment_node = AssignmentNode(list_access_node, input_node, line=line)
|
| 2260 |
return assignment_node, index
|
| 2261 |
|
| 2262 |
if tokens[index].type != ")":
|
| 2263 |
raise SemanticError(f"Semantic Error: water() accepts only a single variable name or type parameter.", line)
|
| 2264 |
-
index += 1
|
| 2265 |
if tokens[index].type != ";":
|
| 2266 |
raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
|
| 2267 |
-
index += 1
|
| 2268 |
|
| 2269 |
-
# Build as: variable = water(type) — an assignment with Input as the value
|
| 2270 |
input_node = ASTNode("Input", f"water({var_type})", line=line)
|
| 2271 |
value_ident = ASTNode("Identifier", var_name, line=line)
|
| 2272 |
assignment_node = AssignmentNode(var_name, input_node, line=line)
|
|
@@ -2356,7 +2183,6 @@ def parse_print(tokens, index):
|
|
| 2356 |
raise SemanticError(f"Semantic Error: Variable '{identif_name}' used before declaration.", line)
|
| 2357 |
|
| 2358 |
if tokens[index + 1].type == "." and arg_info["type"] in symbol_table.bundle_types:
|
| 2359 |
-
# Bundle member access in plant: plant(p.name)
|
| 2360 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 2361 |
args.append(expr_node)
|
| 2362 |
|
|
@@ -2468,7 +2294,6 @@ def parse_print(tokens, index):
|
|
| 2468 |
raise SemanticError(f"Semantic Error: List '{arg_name}' must be indexed with '[]' in expressions.", line)
|
| 2469 |
|
| 2470 |
if tokens[index + 1].type == "." and arg_info["type"] in symbol_table.bundle_types:
|
| 2471 |
-
# Bundle member access in plant args: plant("{}", p.name)
|
| 2472 |
arg_node, index, _ = parse_expression(tokens, index)
|
| 2473 |
actual_args.append(arg_node)
|
| 2474 |
|
|
@@ -2510,7 +2335,6 @@ def parse_print(tokens, index):
|
|
| 2510 |
if placeholder_count > 15:
|
| 2511 |
raise SemanticError(f"Semantic Error: Exceeded maximum amount of 15 arguments in plant statement.", line)
|
| 2512 |
|
| 2513 |
-
# Only enforce placeholder matching when the format string contains {}
|
| 2514 |
if placeholder_count > 0 and placeholder_count != len(actual_args):
|
| 2515 |
raise SemanticError(f"Semantic Error: Found {len(actual_args)} argument(s). Expected {placeholder_count} argument(s).", line)
|
| 2516 |
|
|
@@ -2623,21 +2447,15 @@ def parse_fertile(tokens, index):
|
|
| 2623 |
|
| 2624 |
return FertileDeclarationNode(var_type, var_name, value_node, line=line), index
|
| 2625 |
|
| 2626 |
-
# ============================================================================
|
| 2627 |
-
# CONTROL-FLOW PARSERS - parse_if / parse_for / parse_while / parse_do /
|
| 2628 |
-
# parse_switch. Each builds the matching AST node (IfStatementNode,
|
| 2629 |
-
# ForLoopNode, etc.) and recursively parses the body via parse_statement.
|
| 2630 |
-
# Conditions must evaluate to a branch (boolean) — checked here.
|
| 2631 |
-
# ============================================================================
|
| 2632 |
def parse_if(tokens, index, func_type):
|
| 2633 |
line = tokens[index].line
|
| 2634 |
-
index += 1
|
| 2635 |
|
| 2636 |
if tokens[index].type != "(":
|
| 2637 |
raise SemanticError(f"Syntax Error: Expected '(' after 'spring'.", line)
|
| 2638 |
index += 1
|
| 2639 |
|
| 2640 |
-
condition_expr, index, cond_type = parse_expression_branch(tokens, index)
|
| 2641 |
|
| 2642 |
if cond_type != "branch":
|
| 2643 |
raise SemanticError(f"Semantic Error: spring condition must be branch, got {cond_type}.", line)
|
|
@@ -2674,7 +2492,6 @@ def parse_if(tokens, index, func_type):
|
|
| 2674 |
raise SemanticError(f"Syntax Error: Expected '{{' after 'spring' condition.", line)
|
| 2675 |
|
| 2676 |
|
| 2677 |
-
# Else-if chains: bud (<cond>) { ... }
|
| 2678 |
while tokens[index].value == "bud":
|
| 2679 |
index += 1
|
| 2680 |
|
|
@@ -2719,7 +2536,6 @@ def parse_if(tokens, index, func_type):
|
|
| 2719 |
raise SemanticError(f"Syntax Error: Expected '{{' after else-if condition.", line)
|
| 2720 |
|
| 2721 |
|
| 2722 |
-
# Else: wither { ... }
|
| 2723 |
if tokens[index].value == "wither":
|
| 2724 |
index += 1
|
| 2725 |
|
|
@@ -2757,10 +2573,10 @@ def parse_return(tokens, index, func_type):
|
|
| 2757 |
elif tokens[index].type == "id":
|
| 2758 |
identifier = tokens[index].value
|
| 2759 |
|
| 2760 |
-
if tokens[index+1].type == "(":
|
| 2761 |
func_info = symbol_table.lookup_function(identifier)
|
| 2762 |
|
| 2763 |
-
if isinstance(func_info, str):
|
| 2764 |
raise SemanticError(f"Semantic Error: Function '{identifier}' is not defined.", line)
|
| 2765 |
|
| 2766 |
return_type = func_info["return_type"]
|
|
@@ -2769,13 +2585,11 @@ def parse_return(tokens, index, func_type):
|
|
| 2769 |
|
| 2770 |
return_expr, index = parse_expression_type(tokens, index, func_type)
|
| 2771 |
|
| 2772 |
-
else:
|
| 2773 |
var_info = symbol_table.lookup_variable(identifier)
|
| 2774 |
if isinstance(var_info, str):
|
| 2775 |
raise SemanticError(f"Semantic Error: Variable '{identifier}' used before declaration.", line)
|
| 2776 |
|
| 2777 |
-
# Skip preliminary type check when the variable is a bundle with member access (e.g., p.a)
|
| 2778 |
-
# because the expression type depends on the member, not the base variable
|
| 2779 |
is_member_access = var_info["type"] in symbol_table.bundle_types and tokens[index+1].type == "."
|
| 2780 |
if not is_member_access:
|
| 2781 |
if var_info["type"] not in [func_type, "seed", "tree"] and var_info["type"] != "seed" and var_info["type"] != "tree":
|
|
@@ -2789,7 +2603,6 @@ def parse_return(tokens, index, func_type):
|
|
| 2789 |
return ReturnNode(return_expr, line=line), index
|
| 2790 |
|
| 2791 |
|
| 2792 |
-
|
| 2793 |
def parse_for(tokens, index, func_type):
|
| 2794 |
line = tokens[index].line
|
| 2795 |
index += 1
|
|
@@ -2910,11 +2723,10 @@ def parse_update(tokens, index):
|
|
| 2910 |
assign_node = AssignmentNode(list_access_node, value_node, line=tokens[index].line)
|
| 2911 |
assignments_node.add_child(assign_node)
|
| 2912 |
elif tokens[index + 1].type in {"++", "--"}:
|
| 2913 |
-
# arr[i]++ in for-update (cultivate(...; ...; arr[i]++))
|
| 2914 |
if var_type not in {"seed", "tree"}:
|
| 2915 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_type} in expression.", line)
|
| 2916 |
operator = tokens[index + 1].value
|
| 2917 |
-
index += 2
|
| 2918 |
assignments_node.add_child(UnaryOpNode(operator, list_access_node, "post", line=line))
|
| 2919 |
else:
|
| 2920 |
raise SemanticError("Semantic Error: Expected '=' or '++'/'--' after list access.", tokens[index + 1].line)
|
|
@@ -2945,7 +2757,6 @@ def parse_update(tokens, index):
|
|
| 2945 |
assignments_node.add_child(node)
|
| 2946 |
|
| 2947 |
elif tokens[index + 1].type in {"+=", "-=", "*=", "/=", "%=", "**="}:
|
| 2948 |
-
# Compound assignment in for-loop update: i += 2
|
| 2949 |
compound_op = tokens[index + 1].value
|
| 2950 |
base_op = compound_op[:-1]
|
| 2951 |
cur_var_name = tokens[index].value
|
|
@@ -2957,15 +2768,13 @@ def parse_update(tokens, index):
|
|
| 2957 |
cur_var_type = cur_var_info["type"]
|
| 2958 |
if cur_var_type not in {"seed", "tree"}:
|
| 2959 |
raise SemanticError(f"Semantic Error: Cannot use compound assignment on '{cur_var_name}' of type '{cur_var_type}'.", line)
|
| 2960 |
-
# ── modulo-assign guard: %= requires seed ──
|
| 2961 |
if base_op == "%" and cur_var_type != "seed":
|
| 2962 |
raise SemanticError(
|
| 2963 |
f"Semantic Error: Modulo operator '%' requires 'seed' (integer) operands, "
|
| 2964 |
f"but '{cur_var_name}' is of type 'tree'.",
|
| 2965 |
line,
|
| 2966 |
)
|
| 2967 |
-
|
| 2968 |
-
index += 2 # skip id and compound op
|
| 2969 |
rhs_node, index, rhs_type = parse_expression(tokens, index)
|
| 2970 |
if rhs_type not in {"seed", "tree"}:
|
| 2971 |
raise SemanticError(
|
|
@@ -2990,15 +2799,13 @@ def parse_update(tokens, index):
|
|
| 2990 |
if isinstance(var_info, str):
|
| 2991 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' used before declaration.", line)
|
| 2992 |
|
| 2993 |
-
# ++arr[i]; — prefix on indexed array element
|
| 2994 |
if tokens[index + 1].type == "[":
|
| 2995 |
if var_info["type"] not in {"seed", "tree"}:
|
| 2996 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_info['type']} in expression.", line)
|
| 2997 |
list_access_node, index = parse_list_access(tokens, index)
|
| 2998 |
-
index += 1
|
| 2999 |
assignments_node.add_child(UnaryOpNode(operator, list_access_node, "pre", line=line))
|
| 3000 |
|
| 3001 |
-
# ++obj.field; — prefix on bundle member
|
| 3002 |
elif tokens[index + 1].type == ".":
|
| 3003 |
obj_name = tokens[index].value
|
| 3004 |
if var_info["type"] not in symbol_table.bundle_types:
|
|
@@ -3008,9 +2815,8 @@ def parse_update(tokens, index):
|
|
| 3008 |
if member_name not in bundle_members:
|
| 3009 |
raise SemanticError(f"Semantic Error: Bundle type '{var_info['type']}' has no member '{member_name}'.", line)
|
| 3010 |
member_type = bundle_members[member_name]
|
| 3011 |
-
index += 3
|
| 3012 |
target = MemberAccessNode(obj_name, member_name, line=line)
|
| 3013 |
-
# nested member access: ++p.addr.zip
|
| 3014 |
while tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 3015 |
next_member = tokens[index + 1].value
|
| 3016 |
nested_members = symbol_table.bundle_types[member_type]
|
|
@@ -3023,7 +2829,6 @@ def parse_update(tokens, index):
|
|
| 3023 |
raise SemanticError(f"Semantic Error: Cannot apply '{operator}' to member '{member_name}' of type '{member_type}'.", line)
|
| 3024 |
assignments_node.add_child(UnaryOpNode(operator, target, "pre", line=line))
|
| 3025 |
|
| 3026 |
-
# ++x; — prefix on plain identifier (original path)
|
| 3027 |
else:
|
| 3028 |
if var_info["type"] not in {"seed", "tree"}:
|
| 3029 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_info['type']} in expression.", line)
|
|
@@ -3173,7 +2978,7 @@ def parse_switch(tokens, index, func_type):
|
|
| 3173 |
raise SemanticError(f"Syntax Error: Expected '(' after 'harvest'.", line)
|
| 3174 |
index += 1
|
| 3175 |
|
| 3176 |
-
switch_type = None
|
| 3177 |
|
| 3178 |
if tokens[index].type == "id":
|
| 3179 |
var_info = symbol_table.lookup_variable(tokens[index].value)
|
|
@@ -3187,7 +2992,6 @@ def parse_switch(tokens, index, func_type):
|
|
| 3187 |
|
| 3188 |
|
| 3189 |
elif tokens[index].type in {"intlit", "chrlit", "sunshine", "frost"} or tokens[index].type in {"--", "++", "-", "("}:
|
| 3190 |
-
# infer type from the leading token
|
| 3191 |
if tokens[index].type == "intlit" or tokens[index].type in {"--", "++", "-"}:
|
| 3192 |
switch_type = "seed"
|
| 3193 |
elif tokens[index].type == "chrlit":
|
|
@@ -3195,7 +2999,7 @@ def parse_switch(tokens, index, func_type):
|
|
| 3195 |
elif tokens[index].type in {"sunshine", "frost"}:
|
| 3196 |
switch_type = "branch"
|
| 3197 |
elif tokens[index].type == "(":
|
| 3198 |
-
switch_type = "seed"
|
| 3199 |
switch_expr, index, _ = parse_expression(tokens, index)
|
| 3200 |
|
| 3201 |
elif tokens[index].type in {"stringlit"}:
|
|
@@ -3219,7 +3023,7 @@ def parse_switch(tokens, index, func_type):
|
|
| 3219 |
|
| 3220 |
case_nodes = []
|
| 3221 |
default_case = None
|
| 3222 |
-
seen_case_values = set()
|
| 3223 |
|
| 3224 |
while tokens[index].value in {"variety"}:
|
| 3225 |
case_line = tokens[index].line
|
|
@@ -3229,7 +3033,6 @@ def parse_switch(tokens, index, func_type):
|
|
| 3229 |
if tokens[index].type not in {"chrlit", "stringlit", "sunshine", "frost", "intlit", "dbllit"}:
|
| 3230 |
raise SemanticError(f"Semantic Error: Expected valid literal value after 'variety'.", line)
|
| 3231 |
|
| 3232 |
-
# --- type-check the variety literal against the harvest expression ---
|
| 3233 |
lit_type_map = {
|
| 3234 |
"intlit": "seed",
|
| 3235 |
"dbllit": "tree",
|
|
@@ -3245,7 +3048,6 @@ def parse_switch(tokens, index, func_type):
|
|
| 3245 |
tokens[index].line,
|
| 3246 |
)
|
| 3247 |
|
| 3248 |
-
# --- check for duplicate variety values ---
|
| 3249 |
case_val_key = tokens[index].value
|
| 3250 |
if case_val_key in seen_case_values:
|
| 3251 |
raise SemanticError(
|
|
@@ -3382,7 +3184,6 @@ def parse_insert(tokens, index, var_name, expected_type):
|
|
| 3382 |
index += 1
|
| 3383 |
|
| 3384 |
expr_node, index, idx_type = parse_equality(tokens, index)
|
| 3385 |
-
# Insert index must be seed (integer)
|
| 3386 |
if idx_type is not None and idx_type != "seed":
|
| 3387 |
raise SemanticError(
|
| 3388 |
f"Semantic Error: List index must be of type 'seed', got '{idx_type}'.",
|
|
@@ -3422,7 +3223,6 @@ def parse_remove(tokens, index, var_name, expected_type):
|
|
| 3422 |
index += 1
|
| 3423 |
|
| 3424 |
expr_node, index, idx_type = parse_equality(tokens, index)
|
| 3425 |
-
# Remove index must be seed (integer)
|
| 3426 |
if idx_type is not None and idx_type != "seed":
|
| 3427 |
raise SemanticError(
|
| 3428 |
f"Semantic Error: List index must be of type 'seed', got '{idx_type}'.",
|
|
@@ -3442,17 +3242,7 @@ def is_inside_loop_or_switch_stack():
|
|
| 3442 |
return any(ctx in {"WhileNode", "DoWhileNode", "SwitchNode", "ForNode"} for ctx in context_stack)
|
| 3443 |
|
| 3444 |
|
| 3445 |
-
# ============================================================================
|
| 3446 |
-
# analyze_semantics() - LEGACY ONE-SHOT ENTRY POINT
|
| 3447 |
-
# Older API that does build_ast in one call. The current pipeline uses
|
| 3448 |
-
# the two-step parse_and_build + validate_ast instead so the IDE can
|
| 3449 |
-
# distinguish 'syntax' from 'semantic' error stages.
|
| 3450 |
-
# ============================================================================
|
| 3451 |
def analyze_semantics(tokens):
|
| 3452 |
-
"""
|
| 3453 |
-
Legacy API — builds AST from tokens and validates in one pass.
|
| 3454 |
-
Kept for backward compatibility.
|
| 3455 |
-
"""
|
| 3456 |
try:
|
| 3457 |
filtered = [t for t in tokens if t.type != '\n']
|
| 3458 |
ast = build_ast(filtered)
|
|
@@ -3501,15 +3291,3 @@ def analyze_semantics(tokens):
|
|
| 3501 |
}
|
| 3502 |
|
| 3503 |
|
| 3504 |
-
# ============================================================================
|
| 3505 |
-
# SEMANTIC VALIDATION — Tree-Walking Pass
|
| 3506 |
-
# ============================================================================
|
| 3507 |
-
# After the parser builds the AST, this validator walks the tree to verify
|
| 3508 |
-
# semantic correctness. The compiler pipeline is:
|
| 3509 |
-
#
|
| 3510 |
-
# Lexer → Parser (LL1 + AST build) → Semantic (validate AST) → Interpreter
|
| 3511 |
-
#
|
| 3512 |
-
# The parser already performs primary checks during AST construction (variable
|
| 3513 |
-
# declarations, type dispatch, scope tracking). This tree-walking pass serves
|
| 3514 |
-
# as the dedicated semantic analysis phase that validates the completed AST.
|
| 3515 |
-
# ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import copy
|
| 2 |
import re
|
| 3 |
|
|
|
|
| 4 |
from shared.tokens import * # noqa: F401,F403 - TT_* constants used throughout parse_*
|
| 5 |
from semantic.errors import SemanticError
|
| 6 |
from shared.ast_nodes import * # noqa: F401,F403 - all AST node classes
|
|
|
|
| 7 |
from semantic.symbol_table import SymbolTable
|
| 8 |
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
class SemanticAnalyzer:
|
| 11 |
def __init__(self, symbol_table):
|
| 12 |
self.symbol_table = symbol_table
|
|
|
|
| 17 |
semantic_analyzer = SemanticAnalyzer(symbol_table)
|
| 18 |
context_stack = []
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
def build_ast(tokens):
|
|
|
|
| 22 |
root = ProgramNode()
|
| 23 |
+
symbol_table.variables = {}
|
| 24 |
+
symbol_table.functions = {}
|
| 25 |
symbol_table.scopes = [{}]
|
| 26 |
symbol_table.function_variables = {}
|
| 27 |
+
symbol_table.bundle_types = {}
|
| 28 |
context_stack = []
|
| 29 |
index = 0
|
| 30 |
symbol_table.current_func_name = None
|
|
|
|
| 32 |
while index < len(tokens):
|
| 33 |
token = tokens[index]
|
| 34 |
|
|
|
|
| 35 |
if token.type == ";":
|
| 36 |
index += 1
|
| 37 |
continue
|
|
|
|
| 75 |
root.add_child(node)
|
| 76 |
|
| 77 |
elif tokens[index].type == "id" and tokens[index].value in symbol_table.bundle_types:
|
|
|
|
| 78 |
id_type = tokens[index].value
|
| 79 |
index += 1
|
| 80 |
if tokens[index].type != "id":
|
|
|
|
| 109 |
|
| 110 |
elif token.value == "bundle":
|
| 111 |
bundle_name = tokens[index + 1].value
|
| 112 |
+
index += 2
|
| 113 |
|
| 114 |
if tokens[index].type == "{":
|
| 115 |
+
index += 1
|
|
|
|
| 116 |
members = {}
|
| 117 |
while tokens[index].type != "}":
|
| 118 |
if tokens[index].value in {"seed", "tree", "vine", "leaf", "branch"}:
|
|
|
|
| 121 |
if member_name in members:
|
| 122 |
raise SemanticError(f"Semantic Error: Duplicate member '{member_name}' in bundle '{bundle_name}'.", tokens[index].line)
|
| 123 |
members[member_name] = member_type
|
| 124 |
+
index += 2
|
| 125 |
if tokens[index].type == ";":
|
| 126 |
+
index += 1
|
| 127 |
elif tokens[index].type == "id" and tokens[index].value in symbol_table.bundle_types:
|
|
|
|
| 128 |
member_type = tokens[index].value
|
| 129 |
member_name = tokens[index + 1].value
|
| 130 |
if member_name in members:
|
| 131 |
raise SemanticError(f"Semantic Error: Duplicate member '{member_name}' in bundle '{bundle_name}'.", tokens[index].line)
|
| 132 |
members[member_name] = member_type
|
| 133 |
+
index += 2
|
| 134 |
if tokens[index].type == ";":
|
| 135 |
+
index += 1
|
| 136 |
else:
|
| 137 |
raise SemanticError(f"Semantic Error: Invalid member type '{tokens[index].value}' in bundle definition.", tokens[index].line)
|
| 138 |
+
index += 1
|
| 139 |
|
| 140 |
if bundle_name in symbol_table.bundle_types:
|
| 141 |
raise SemanticError(f"Semantic Error: Bundle type '{bundle_name}' already defined.", token.line)
|
|
|
|
| 145 |
root.add_child(node)
|
| 146 |
|
| 147 |
else:
|
|
|
|
| 148 |
var_name = tokens[index].value
|
| 149 |
index += 1
|
| 150 |
|
|
|
|
| 192 |
|
| 193 |
return None, index
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
def parse_function(tokens, index, func_name, func_type):
|
| 196 |
line = tokens[index].line
|
| 197 |
|
|
|
|
| 205 |
error = f"Semantic Error: '{func_name}' already declared."
|
| 206 |
raise SemanticError(error, tokens[index].line)
|
| 207 |
|
|
|
|
| 208 |
if func_name in {"root"}:
|
| 209 |
symbol_table.enter_scope()
|
| 210 |
+
index += 1
|
| 211 |
|
|
|
|
| 212 |
if tokens[index].type == "(":
|
| 213 |
index += 1
|
| 214 |
if tokens[index].type != ")":
|
|
|
|
| 240 |
param_node.add_child(ASTNode("Identifier", param_name))
|
| 241 |
index += 1
|
| 242 |
|
|
|
|
| 243 |
is_list = False
|
| 244 |
if tokens[index].type == "[":
|
| 245 |
+
index += 1
|
| 246 |
if tokens[index].type != "]":
|
| 247 |
raise SemanticError(f"Semantic Error: Expected ']' after '[' in array parameter.", line)
|
| 248 |
+
index += 1
|
| 249 |
is_list = True
|
| 250 |
param_node.add_child(ASTNode("ArrayParam", "true"))
|
| 251 |
|
|
|
|
| 262 |
raise SemanticError(error, line)
|
| 263 |
|
| 264 |
elif tokens[index].type == "id" and tokens[index].value in symbol_table.bundle_types:
|
|
|
|
| 265 |
param_type = tokens[index].value
|
| 266 |
index += 1
|
| 267 |
if tokens[index].type == "id":
|
|
|
|
| 350 |
|
| 351 |
elif tokens[index].value == "water":
|
| 352 |
water_line = tokens[index].line
|
| 353 |
+
index += 1
|
| 354 |
if tokens[index].type != "(":
|
| 355 |
raise SemanticError(f"Semantic Error: Expected '(' after water.", water_line)
|
| 356 |
+
index += 1
|
| 357 |
water_type = None
|
| 358 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 359 |
water_type = tokens[index].value
|
| 360 |
index += 1
|
| 361 |
if tokens[index].type != ")":
|
| 362 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter (e.g., water(seed)).", water_line)
|
| 363 |
+
index += 1
|
| 364 |
if water_type and not _types_compatible(var_type, water_type):
|
| 365 |
raise SemanticError(f"Semantic Error: Type mismatch — cannot assign water({water_type}) to '{var_type}' variable.", water_line)
|
| 366 |
value_node = ASTNode("Input", f"water({water_type})" if water_type else "water()", line=water_line)
|
|
|
|
| 371 |
var_node.add_child(value_node)
|
| 372 |
|
| 373 |
elif tokens[index].type == "[":
|
|
|
|
|
|
|
| 374 |
is_list = True
|
| 375 |
dimensions = []
|
| 376 |
while tokens[index].type == "[":
|
| 377 |
+
index += 1
|
| 378 |
dim_size = 0
|
| 379 |
if tokens[index].type == "dbllit":
|
| 380 |
raise SemanticError(f"Semantic Error: Array size must be of type 'seed' (integer), got 'tree' (float) '{tokens[index].value}'.", line)
|
| 381 |
if tokens[index].type == "intlit":
|
| 382 |
dim_size = int(tokens[index].value)
|
| 383 |
+
index += 1
|
| 384 |
if tokens[index].type != "]":
|
| 385 |
raise SemanticError(f"Syntax Error: Expected ']' after list size.", line)
|
| 386 |
+
index += 1
|
| 387 |
dimensions.append(dim_size)
|
| 388 |
|
|
|
|
| 389 |
default_literals = {"seed": "0", "tree": "0.0", "leaf": "''", "vine": '""', "branch": "false"}
|
| 390 |
|
| 391 |
def build_list_node(dims):
|
|
|
|
| 401 |
list_node = build_list_node(dimensions)
|
| 402 |
var_node.add_child(list_node)
|
| 403 |
|
|
|
|
| 404 |
if tokens[index].type == "=":
|
| 405 |
+
index += 1
|
| 406 |
if tokens[index].type == "{":
|
| 407 |
+
index += 1
|
| 408 |
elements = []
|
| 409 |
while tokens[index].type != "}":
|
| 410 |
expr, index = parse_expression_type(tokens, index, var_type)
|
| 411 |
elements.append(expr)
|
| 412 |
if tokens[index].type == ",":
|
| 413 |
index += 1
|
| 414 |
+
index += 1
|
| 415 |
value_node = ListNode(elements=elements, line=line)
|
|
|
|
| 416 |
var_node.children[-1] = value_node
|
| 417 |
else:
|
| 418 |
raise SemanticError(f"Syntax Error: Expected '{{' after '=' in array initialization.", line)
|
| 419 |
|
| 420 |
else:
|
|
|
|
| 421 |
pass
|
| 422 |
|
| 423 |
error = symbol_table.declare_variable(var_name, var_type, is_list = is_list)
|
|
|
|
| 445 |
|
| 446 |
|
| 447 |
def _skip_semicolons(tokens, index):
|
|
|
|
| 448 |
while index < len(tokens) and tokens[index].type == ";":
|
| 449 |
index += 1
|
| 450 |
return index
|
| 451 |
|
| 452 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
def parse_statement(tokens, index, func_type = None):
|
| 454 |
token = tokens[index]
|
| 455 |
|
|
|
|
| 456 |
if token.type == ";":
|
| 457 |
return None, index + 1
|
| 458 |
|
| 459 |
line = token.line
|
| 460 |
|
| 461 |
if token.value in {"seed", "tree", "vine", "leaf", "branch"}:
|
|
|
|
| 462 |
var_type = token.value
|
| 463 |
var_name = tokens[index + 1].value
|
| 464 |
index += 2
|
|
|
|
| 472 |
return node, index
|
| 473 |
|
| 474 |
elif token.value == "bundle":
|
|
|
|
| 475 |
bundle_type_name = tokens[index + 1].value
|
| 476 |
if bundle_type_name not in symbol_table.bundle_types:
|
| 477 |
raise SemanticError(f"Semantic Error: Bundle type '{bundle_type_name}' is not defined.", token.line)
|
| 478 |
var_name = tokens[index + 2].value
|
| 479 |
+
index += 3
|
| 480 |
|
| 481 |
members = symbol_table.bundle_types[bundle_type_name]
|
| 482 |
_defaults = {"seed": 0, "tree": 0.0, "leaf": '', "vine": "", "branch": False}
|
| 483 |
|
| 484 |
if tokens[index].type == "[":
|
| 485 |
+
index += 1
|
|
|
|
| 486 |
if tokens[index].type == "dbllit":
|
| 487 |
raise SemanticError(f"Semantic Error: Array size must be of type 'seed' (integer), got 'tree' (float) '{tokens[index].value}'.", token.line)
|
| 488 |
array_size = 0
|
| 489 |
if tokens[index].type == "intlit":
|
| 490 |
array_size = int(tokens[index].value)
|
| 491 |
+
index += 1
|
| 492 |
if tokens[index].type != "]":
|
| 493 |
raise SemanticError(f"Syntax Error: Expected ']' after array size.", token.line)
|
| 494 |
+
index += 1
|
| 495 |
|
|
|
|
| 496 |
list_node = ASTNode("List", line=token.line)
|
| 497 |
for _ in range(array_size):
|
| 498 |
bundle_val_node = ASTNode("BundleDefault", line=token.line)
|
|
|
|
| 555 |
list_access_node, index = parse_list_access(tokens, index)
|
| 556 |
|
| 557 |
if tokens[index + 1].type == "." and var_type in symbol_table.bundle_types:
|
| 558 |
+
index += 2
|
|
|
|
| 559 |
member_name = tokens[index].value
|
| 560 |
bundle_members = symbol_table.bundle_types[var_type]
|
| 561 |
if member_name not in bundle_members:
|
| 562 |
raise SemanticError(f"Semantic Error: Bundle type '{var_type}' has no member '{member_name}'.", line)
|
| 563 |
member_type = bundle_members[member_name]
|
| 564 |
target = ArrayMemberAccessNode(list_access_node, member_name, line=line)
|
| 565 |
+
index += 1
|
| 566 |
|
|
|
|
| 567 |
while tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 568 |
next_member = tokens[index + 1].value
|
| 569 |
nested_members = symbol_table.bundle_types[member_type]
|
|
|
|
| 571 |
raise SemanticError(f"Semantic Error: Bundle type '{member_type}' has no member '{next_member}'.", line)
|
| 572 |
member_type = nested_members[next_member]
|
| 573 |
target = MemberAccessNode(target, next_member, line=line)
|
| 574 |
+
index += 2
|
| 575 |
|
| 576 |
if tokens[index].type == "=":
|
| 577 |
+
index += 1
|
| 578 |
value_node, index = parse_expression_type(tokens, index, member_type)
|
| 579 |
assign_node = AssignmentNode(target, value_node, line=line)
|
| 580 |
assignments_node.add_child(assign_node)
|
|
|
|
| 595 |
index += 2
|
| 596 |
if tokens[index].value == "water":
|
| 597 |
water_line = tokens[index].line
|
| 598 |
+
index += 1
|
| 599 |
if tokens[index].type != "(":
|
| 600 |
raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
|
| 601 |
+
index += 1
|
| 602 |
water_type = None
|
| 603 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 604 |
water_type = tokens[index].value
|
| 605 |
index += 1
|
| 606 |
if tokens[index].type != ")":
|
| 607 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter.", water_line)
|
| 608 |
+
index += 1
|
| 609 |
if water_type and not _types_compatible(var_type, water_type):
|
| 610 |
raise SemanticError(f"Semantic Error: Type mismatch — cannot assign water({water_type}) to '{var_type}' list element.", water_line)
|
| 611 |
value_node = ASTNode("Input", f"water({water_type})" if water_type else "water()", line=water_line)
|
|
|
|
| 626 |
|
| 627 |
|
| 628 |
elif tokens[index + 1].type == ".":
|
|
|
|
| 629 |
obj_name = tokens[index].value
|
| 630 |
member_name = tokens[index + 2].value
|
|
|
|
| 631 |
if var_type not in symbol_table.bundle_types:
|
| 632 |
raise SemanticError(f"Semantic Error: Variable '{obj_name}' is not a bundle type.", line)
|
| 633 |
bundle_members = symbol_table.bundle_types[var_type]
|
| 634 |
if member_name not in bundle_members:
|
| 635 |
raise SemanticError(f"Semantic Error: Bundle type '{var_type}' has no member '{member_name}'.", line)
|
| 636 |
member_type = bundle_members[member_name]
|
| 637 |
+
index += 3
|
| 638 |
target = MemberAccessNode(obj_name, member_name, line=line)
|
| 639 |
|
|
|
|
| 640 |
while tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 641 |
next_member = tokens[index + 1].value
|
| 642 |
nested_members = symbol_table.bundle_types[member_type]
|
|
|
|
| 644 |
raise SemanticError(f"Semantic Error: Bundle type '{member_type}' has no member '{next_member}'.", line)
|
| 645 |
member_type = nested_members[next_member]
|
| 646 |
target = MemberAccessNode(target, next_member, line=line)
|
| 647 |
+
index += 2
|
| 648 |
|
| 649 |
if tokens[index].type == "=":
|
| 650 |
+
index += 1
|
|
|
|
| 651 |
if tokens[index].value == "water":
|
| 652 |
water_line = tokens[index].line
|
| 653 |
+
index += 1
|
| 654 |
if tokens[index].type != "(":
|
| 655 |
raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
|
| 656 |
+
index += 1
|
| 657 |
water_type = None
|
| 658 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 659 |
water_type = tokens[index].value
|
| 660 |
index += 1
|
| 661 |
if tokens[index].type != ")":
|
| 662 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter (e.g., water(seed)).", water_line)
|
| 663 |
+
index += 1
|
| 664 |
if water_type and not _types_compatible(member_type, water_type):
|
| 665 |
raise SemanticError(f"Semantic Error: Type mismatch — cannot assign water({water_type}) to '{member_type}' member '{member_name}'.", water_line)
|
| 666 |
value_node = ASTNode("Input", f"water({water_type})" if water_type else "water()", line=water_line)
|
|
|
|
| 669 |
assign_node = AssignmentNode(target, value_node, line=line)
|
| 670 |
assignments_node.add_child(assign_node)
|
| 671 |
elif tokens[index].type in {"+=", "-=", "*=", "/=", "%=", "**="}:
|
|
|
|
| 672 |
if member_type not in {"seed", "tree"}:
|
| 673 |
raise SemanticError(f"Semantic Error: Compound assignment '{tokens[index].value}' is not valid for '{member_type}' member '{member_name}'.", line)
|
| 674 |
compound_op = tokens[index].value
|
| 675 |
base_op = compound_op[:-1]
|
| 676 |
+
index += 1
|
| 677 |
rhs_node, index = parse_expression_type(tokens, index, member_type)
|
| 678 |
value_node = BinaryOpNode(target, base_op, rhs_node, line=line)
|
| 679 |
assign_node = AssignmentNode(target, value_node, line=line)
|
| 680 |
assignments_node.add_child(assign_node)
|
| 681 |
elif tokens[index].type in {"++", "--"}:
|
|
|
|
| 682 |
if member_type not in {"seed", "tree"}:
|
| 683 |
raise SemanticError(f"Semantic Error: Cannot apply '{tokens[index].value}' to member '{member_name}' of type '{member_type}'.", line)
|
| 684 |
operator = tokens[index].value
|
| 685 |
+
index += 1
|
| 686 |
assignments_node.add_child(UnaryOpNode(operator, target, "post", line=line))
|
| 687 |
else:
|
| 688 |
raise SemanticError(f"Semantic Error: Expected '=' after '{obj_name}.{member_name}'.", line)
|
|
|
|
| 711 |
assignments_node.add_child(UnaryOpNode(operator, operand, "post", line=line))
|
| 712 |
|
| 713 |
elif tokens[index + 1].type in {"+=", "-=", "*=", "/=", "%=", "**="}:
|
| 714 |
+
compound_op = tokens[index + 1].value
|
| 715 |
+
base_op = compound_op[:-1]
|
|
|
|
| 716 |
cur_var_name = tokens[index].value
|
| 717 |
cur_var_info = symbol_table.lookup_variable(cur_var_name)
|
| 718 |
if isinstance(cur_var_info, str):
|
|
|
|
| 722 |
cur_var_type = cur_var_info["type"]
|
| 723 |
if cur_var_type not in {"seed", "tree"}:
|
| 724 |
raise SemanticError(f"Semantic Error: Cannot use compound assignment on '{cur_var_name}' of type '{cur_var_type}'.", line)
|
|
|
|
| 725 |
if base_op == "%" and cur_var_type != "seed":
|
| 726 |
raise SemanticError(
|
| 727 |
f"Semantic Error: Modulo operator '%' requires 'seed' (integer) operands, "
|
| 728 |
f"but '{cur_var_name}' is of type 'tree'.",
|
| 729 |
line,
|
| 730 |
)
|
| 731 |
+
index += 2
|
|
|
|
| 732 |
rhs_node, index, rhs_type = parse_expression(tokens, index)
|
| 733 |
if rhs_type not in {"seed", "tree"}:
|
| 734 |
raise SemanticError(
|
|
|
|
| 753 |
if isinstance(var_info, str):
|
| 754 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' used before declaration.", line)
|
| 755 |
|
|
|
|
| 756 |
if tokens[index + 1].type == "[":
|
| 757 |
if var_info["type"] not in {"seed", "tree"}:
|
| 758 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_info['type']} in expression.", line)
|
| 759 |
list_access_node, index = parse_list_access(tokens, index)
|
| 760 |
+
index += 1
|
| 761 |
assignments_node.add_child(UnaryOpNode(operator, list_access_node, "pre", line=line))
|
| 762 |
|
|
|
|
| 763 |
elif tokens[index + 1].type == ".":
|
| 764 |
obj_name = tokens[index].value
|
| 765 |
if var_info["type"] not in symbol_table.bundle_types:
|
|
|
|
| 769 |
if member_name not in bundle_members:
|
| 770 |
raise SemanticError(f"Semantic Error: Bundle type '{var_info['type']}' has no member '{member_name}'.", line)
|
| 771 |
member_type = bundle_members[member_name]
|
| 772 |
+
index += 3
|
| 773 |
target = MemberAccessNode(obj_name, member_name, line=line)
|
|
|
|
| 774 |
while tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 775 |
next_member = tokens[index + 1].value
|
| 776 |
nested_members = symbol_table.bundle_types[member_type]
|
|
|
|
| 783 |
raise SemanticError(f"Semantic Error: Cannot apply '{operator}' to member '{member_name}' of type '{member_type}'.", line)
|
| 784 |
assignments_node.add_child(UnaryOpNode(operator, target, "pre", line=line))
|
| 785 |
|
|
|
|
| 786 |
else:
|
| 787 |
if var_info["type"] not in {"seed", "tree"}:
|
| 788 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_info['type']} in expression.", line)
|
|
|
|
| 858 |
raise SemanticError(f"Semantic Error: '{token.value}' statement used outside a 'harvest' block.", line)
|
| 859 |
|
| 860 |
elif token.type == "{":
|
| 861 |
+
index += 1
|
|
|
|
| 862 |
block_node = ASTNode("Block", line=line)
|
| 863 |
while tokens[index].type != "}":
|
| 864 |
stmt, index = parse_statement(tokens, index, func_type)
|
| 865 |
if stmt:
|
| 866 |
block_node.add_child(stmt)
|
| 867 |
+
index += 1
|
| 868 |
return block_node, index
|
| 869 |
|
| 870 |
else:
|
|
|
|
| 887 |
|
| 888 |
index_node, index, idx_type = parse_equality(tokens, index)
|
| 889 |
|
|
|
|
| 890 |
if idx_type is not None and idx_type != "seed":
|
| 891 |
raise SemanticError(
|
| 892 |
f"Semantic Error: List index must be of type 'seed', got '{idx_type}'.",
|
|
|
|
| 901 |
|
| 902 |
node = ListAccessNode(list_name, index_wrapper, line=line)
|
| 903 |
|
|
|
|
| 904 |
while tokens[index + 1].type == "[":
|
| 905 |
+
index += 2
|
| 906 |
inner_index_node, index, inner_idx_type = parse_equality(tokens, index)
|
| 907 |
if inner_idx_type is not None and inner_idx_type != "seed":
|
| 908 |
raise SemanticError(
|
|
|
|
| 919 |
|
| 920 |
|
| 921 |
def parse_list_assignment(tokens, index):
|
|
|
|
| 922 |
line = tokens[index].line
|
| 923 |
var_name = tokens[index].value
|
| 924 |
|
|
|
|
| 936 |
if var_info.get("is_fertile", False):
|
| 937 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' is declared as fertile.", line)
|
| 938 |
|
|
|
|
| 939 |
if tokens[index].value == "append":
|
| 940 |
value_node, index = parse_append(tokens, index, var_name, var_type)
|
| 941 |
|
|
|
|
| 945 |
elif tokens[index].value == "remove":
|
| 946 |
value_node, index = parse_remove(tokens, index, var_name, var_type)
|
| 947 |
|
|
|
|
| 948 |
elif tokens[index].type == "id":
|
| 949 |
source_var = tokens[index].value
|
| 950 |
source_info = symbol_table.lookup_variable(source_var)
|
|
|
|
| 985 |
value_node = ASTNode("Value", source_var, line=line)
|
| 986 |
index += 1
|
| 987 |
|
|
|
|
| 988 |
elif tokens[index].type == "[":
|
| 989 |
value_node, index = parse_list(tokens, index, var_type)
|
| 990 |
|
|
|
|
| 995 |
|
| 996 |
|
| 997 |
def _types_compatible(declared, inferred):
|
|
|
|
| 998 |
if declared == inferred:
|
| 999 |
return True
|
|
|
|
| 1000 |
if declared in {"seed", "tree"} and inferred in {"seed", "tree"}:
|
| 1001 |
return True
|
|
|
|
| 1002 |
return False
|
| 1003 |
|
| 1004 |
|
|
|
|
| 1010 |
|
| 1011 |
node, index, expr_type = parse_assignment_expression(tokens, index)
|
| 1012 |
|
|
|
|
| 1013 |
if expr_type is None:
|
| 1014 |
raise SemanticError(
|
| 1015 |
"Semantic Error: Could not determine the type of the expression.",
|
|
|
|
| 1076 |
func_name = tokens[index].value
|
| 1077 |
func_info = symbol_table.lookup_function(func_name)
|
| 1078 |
|
| 1079 |
+
if isinstance(func_info, str):
|
| 1080 |
raise SemanticError(f"Semantic Error: Function '{func_name}' is not defined.", line)
|
| 1081 |
|
| 1082 |
func_return_type = func_info["return_type"]
|
|
|
|
| 1108 |
index += 1
|
| 1109 |
node = ListAccessNode(list_name, index_node, line=token.line)
|
| 1110 |
|
|
|
|
| 1111 |
while index < len(tokens) and tokens[index].type == "[":
|
| 1112 |
+
index += 1
|
| 1113 |
inner_expr, index, _ = parse_expression(tokens, index)
|
| 1114 |
if tokens[index].type != "]":
|
| 1115 |
raise SemanticError("Syntax Error: Missing closing bracket.", token.line)
|
| 1116 |
inner_index = ASTNode("Index", line=token.line)
|
| 1117 |
inner_index.add_child(inner_expr)
|
| 1118 |
+
index += 1
|
| 1119 |
node = ListAccessNode(node, inner_index, line=token.line)
|
| 1120 |
|
| 1121 |
elif tokens[index].type == "id":
|
|
|
|
| 1148 |
func_name = tokens[index].value
|
| 1149 |
func_info = symbol_table.lookup_function(func_name)
|
| 1150 |
|
| 1151 |
+
if isinstance(func_info, str):
|
| 1152 |
raise SemanticError(f"Semantic Error: Function '{func_name}' is not defined.", line)
|
| 1153 |
|
| 1154 |
func_return_type = func_info["return_type"]
|
|
|
|
| 1205 |
return left_node, index
|
| 1206 |
|
| 1207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1208 |
def parse_expression(tokens, index):
|
|
|
|
|
|
|
| 1209 |
left_node, index, left_type = parse_term(tokens, index)
|
| 1210 |
|
| 1211 |
while tokens[index].type in {"+", "-", "`"}:
|
|
|
|
| 1213 |
token = tokens[index]
|
| 1214 |
|
| 1215 |
if op == "`":
|
|
|
|
| 1216 |
if left_type not in {"vine", "leaf"}:
|
| 1217 |
raise SemanticError(
|
| 1218 |
f"Semantic Error: Cannot concatenate - left operand is of type '{left_type}', expected 'vine' or 'leaf'.",
|
|
|
|
| 1228 |
left_node = BinaryOpNode(left_node, op, right_node)
|
| 1229 |
left_type = "vine"
|
| 1230 |
else:
|
|
|
|
| 1231 |
if left_type not in {"seed", "tree"}:
|
| 1232 |
raise SemanticError(
|
| 1233 |
f"Semantic Error: Cannot use '{op}' on type '{left_type}'. Expected 'seed' or 'tree'.",
|
|
|
|
| 1241 |
token.line,
|
| 1242 |
)
|
| 1243 |
left_node = BinaryOpNode(left_node, op, right_node)
|
|
|
|
| 1244 |
if left_type == "tree" or right_type == "tree":
|
| 1245 |
left_type = "tree"
|
| 1246 |
|
| 1247 |
return left_node, index, left_type
|
| 1248 |
|
| 1249 |
def parse_term(tokens, index):
|
|
|
|
|
|
|
| 1250 |
left_node, index, left_type = parse_power(tokens, index)
|
| 1251 |
|
| 1252 |
while tokens[index].type in {"*", "/", "%"}:
|
| 1253 |
op = tokens[index].value
|
| 1254 |
token = tokens[index]
|
| 1255 |
|
|
|
|
| 1256 |
if left_type not in {"seed", "tree"}:
|
| 1257 |
raise SemanticError(
|
| 1258 |
f"Semantic Error: Cannot use '{op}' on type '{left_type}'. Expected 'seed' or 'tree'.",
|
| 1259 |
token.line,
|
| 1260 |
)
|
| 1261 |
|
|
|
|
| 1262 |
if op == "%":
|
| 1263 |
if left_type == "tree":
|
| 1264 |
raise SemanticError(
|
|
|
|
| 1270 |
index += 1
|
| 1271 |
right_node, index, right_type = parse_power(tokens, index)
|
| 1272 |
|
|
|
|
| 1273 |
if right_type not in {"seed", "tree"}:
|
| 1274 |
raise SemanticError(
|
| 1275 |
f"Semantic Error: Cannot use '{op}' on type '{right_type}'. Expected 'seed' or 'tree'.",
|
|
|
|
| 1291 |
pass
|
| 1292 |
|
| 1293 |
left_node = BinaryOpNode(left_node, op, right_node)
|
|
|
|
| 1294 |
if left_type == "tree" or right_type == "tree":
|
| 1295 |
left_type = "tree"
|
| 1296 |
|
| 1297 |
return left_node, index, left_type
|
| 1298 |
|
| 1299 |
def parse_power(tokens, index):
|
|
|
|
|
|
|
| 1300 |
left_node, index, left_type = parse_unary(tokens, index)
|
| 1301 |
|
| 1302 |
if tokens[index].type == "**":
|
|
|
|
| 1325 |
return left_node, index, left_type
|
| 1326 |
|
| 1327 |
def parse_unary(tokens, index):
|
|
|
|
| 1328 |
|
| 1329 |
if tokens[index].type in {"++", "--", "-", "~"}:
|
| 1330 |
op = tokens[index].value
|
|
|
|
| 1357 |
return node, index, factor_type
|
| 1358 |
|
| 1359 |
def parse_cast(tokens, index):
|
|
|
|
|
|
|
|
|
|
| 1360 |
token = tokens[index]
|
| 1361 |
cast_types = {"seed", "tree", "leaf", "branch", "vine"}
|
| 1362 |
if token.type == "(" and tokens[index + 1].value in cast_types:
|
|
|
|
| 1374 |
|
| 1375 |
|
| 1376 |
def parse_factor(tokens, index):
|
|
|
|
|
|
|
| 1377 |
token = tokens[index]
|
| 1378 |
|
| 1379 |
if token.type == "(" and tokens[index + 1].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
|
|
|
| 1393 |
index += 1
|
| 1394 |
return node, index, infer_literal_type(token.type)
|
| 1395 |
|
|
|
|
| 1396 |
if token.value == "water":
|
| 1397 |
raise SemanticError(f"Semantic Error: water() is an I/O statement, not an expression. It cannot be used inside an expression.", token.line)
|
| 1398 |
|
|
|
|
| 1453 |
tokens[index].type == "id" and
|
| 1454 |
tokens[index + 1].type == "."
|
| 1455 |
):
|
|
|
|
| 1456 |
obj_name = tokens[index].value
|
| 1457 |
member_name = tokens[index + 2].value
|
| 1458 |
|
|
|
|
| 1469 |
raise SemanticError(f"Semantic Error: Bundle type '{var_type}' has no member '{member_name}'.", token.line)
|
| 1470 |
|
| 1471 |
member_type = bundle_members[member_name]
|
| 1472 |
+
index += 3
|
| 1473 |
node = MemberAccessNode(obj_name, member_name, line=token.line)
|
| 1474 |
|
|
|
|
| 1475 |
while index < len(tokens) and tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 1476 |
next_member = tokens[index + 1].value
|
| 1477 |
nested_members = symbol_table.bundle_types[member_type]
|
|
|
|
| 1479 |
raise SemanticError(f"Semantic Error: Bundle type '{member_type}' has no member '{next_member}'.", token.line)
|
| 1480 |
member_type = nested_members[next_member]
|
| 1481 |
node = MemberAccessNode(node, next_member, line=token.line)
|
| 1482 |
+
index += 2
|
| 1483 |
|
| 1484 |
return node, index, member_type
|
| 1485 |
|
|
|
|
| 1506 |
index += 1
|
| 1507 |
list_access_node = ListAccessNode(list_name, index_node, line=token.line)
|
| 1508 |
|
|
|
|
| 1509 |
while index < len(tokens) and tokens[index].type == "[":
|
| 1510 |
+
index += 1
|
| 1511 |
inner_expr, index, _ = parse_expression(tokens, index)
|
| 1512 |
if tokens[index].type != "]":
|
| 1513 |
raise SemanticError("Syntax Error: Missing closing bracket.", token.line)
|
| 1514 |
inner_index = ASTNode("Index", line=token.line)
|
| 1515 |
inner_index.add_child(inner_expr)
|
| 1516 |
+
index += 1
|
| 1517 |
list_access_node = ListAccessNode(list_access_node, inner_index, line=token.line)
|
| 1518 |
|
|
|
|
| 1519 |
if index < len(tokens) and tokens[index].type == "." and list_info["type"] in symbol_table.bundle_types:
|
| 1520 |
+
index += 1
|
| 1521 |
member_name = tokens[index].value
|
| 1522 |
bundle_members = symbol_table.bundle_types[list_info["type"]]
|
| 1523 |
if member_name not in bundle_members:
|
| 1524 |
raise SemanticError(f"Semantic Error: Bundle type '{list_info['type']}' has no member '{member_name}'.", token.line)
|
| 1525 |
member_type = bundle_members[member_name]
|
| 1526 |
+
index += 1
|
| 1527 |
node = ArrayMemberAccessNode(list_access_node, member_name, line=token.line)
|
| 1528 |
|
|
|
|
| 1529 |
while index < len(tokens) and tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 1530 |
next_member = tokens[index + 1].value
|
| 1531 |
nested_members = symbol_table.bundle_types[member_type]
|
|
|
|
| 1533 |
raise SemanticError(f"Semantic Error: Bundle type '{member_type}' has no member '{next_member}'.", token.line)
|
| 1534 |
member_type = nested_members[next_member]
|
| 1535 |
node = MemberAccessNode(node, next_member, line=token.line)
|
| 1536 |
+
index += 2
|
| 1537 |
|
| 1538 |
return node, index, member_type
|
| 1539 |
|
|
|
|
| 1570 |
|
| 1571 |
|
| 1572 |
def _assignment_root_name(node):
|
|
|
|
| 1573 |
if node.node_type in {"Identifier", "Value", "Object", "ListName"}:
|
| 1574 |
if isinstance(node.value, ASTNode):
|
| 1575 |
return _assignment_root_name(node.value)
|
|
|
|
| 1580 |
|
| 1581 |
|
| 1582 |
def _assignment_target(node, line):
|
|
|
|
| 1583 |
root_name = _assignment_root_name(node)
|
| 1584 |
valid_node_types = {"Value", "Identifier", "ListAccess", "MemberAccess", "ArrayMemberAccess"}
|
| 1585 |
var_info = symbol_table.lookup_variable(root_name) if root_name is not None else None
|
|
|
|
| 1601 |
|
| 1602 |
|
| 1603 |
def parse_assignment_expression(tokens, index):
|
|
|
|
| 1604 |
line = tokens[index].line
|
| 1605 |
left_node, index, left_type = parse_logical_expression(tokens, index)
|
| 1606 |
if tokens[index].type not in {"=", "+=", "-=", "*=", "/=", "%="}:
|
|
|
|
| 1635 |
|
| 1636 |
|
| 1637 |
def parse_expression_branch(tokens, index):
|
|
|
|
| 1638 |
return parse_assignment_expression(tokens, index)
|
| 1639 |
|
| 1640 |
|
| 1641 |
def parse_logical_expression(tokens, index):
|
|
|
|
| 1642 |
line = tokens[index].line
|
| 1643 |
left_node, index, left_type = parse_equality(tokens, index)
|
| 1644 |
|
|
|
|
| 1661 |
|
| 1662 |
|
| 1663 |
def parse_equality(tokens, index):
|
|
|
|
| 1664 |
line = tokens[index].line
|
| 1665 |
left_node, index, left_type = parse_relational(tokens, index)
|
| 1666 |
|
|
|
|
| 1669 |
index += 1
|
| 1670 |
right_node, index, right_type = parse_relational(tokens, index)
|
| 1671 |
|
|
|
|
| 1672 |
if left_type is None or right_type is None:
|
| 1673 |
raise SemanticError(
|
| 1674 |
"Semantic Error: Could not determine the type of an operand in equality check.",
|
|
|
|
| 1680 |
f"Semantic Error: Cannot compare '{left_type}' with '{right_type}' using '{operator}'.",
|
| 1681 |
line,
|
| 1682 |
)
|
|
|
|
| 1683 |
|
| 1684 |
left_node = BinaryOpNode(left_node, operator, right_node, line=line)
|
| 1685 |
left_type = "branch"
|
|
|
|
| 1688 |
|
| 1689 |
|
| 1690 |
def parse_relational(tokens, index):
|
|
|
|
| 1691 |
line = tokens[index].line
|
| 1692 |
|
| 1693 |
if tokens[index].type == "!":
|
|
|
|
| 1712 |
line,
|
| 1713 |
)
|
| 1714 |
|
|
|
|
| 1715 |
if left_type and right_type:
|
| 1716 |
numeric = {"seed", "tree"}
|
| 1717 |
if left_type in numeric and right_type not in numeric:
|
|
|
|
| 1729 |
f"Semantic Error: Cannot compare '{left_type}' with '{right_type}' using '{operator}'.",
|
| 1730 |
line,
|
| 1731 |
)
|
|
|
|
| 1732 |
|
| 1733 |
left_node = BinaryOpNode(left_node, operator, right_node, line=line)
|
| 1734 |
left_type = "branch"
|
|
|
|
| 1760 |
return op_found, start_index
|
| 1761 |
|
| 1762 |
def parse_operand(tokens, index):
|
|
|
|
| 1763 |
token = tokens[index]
|
| 1764 |
line = token.line
|
| 1765 |
|
|
|
|
| 1766 |
if token.value == "water":
|
| 1767 |
raise SemanticError(f"Semantic Error: water() is an I/O statement, not an expression. It cannot be used inside an expression.", token.line)
|
| 1768 |
|
|
|
|
| 1769 |
if token.type == "(":
|
| 1770 |
if tokens[index+1].value in {"seed", "tree"}:
|
| 1771 |
expr_type = tokens[index+1].value
|
|
|
|
| 1798 |
index += 1
|
| 1799 |
return expr_node, index, expr_type
|
| 1800 |
|
|
|
|
| 1801 |
if token.type in {"intlit", "dbllit"}:
|
| 1802 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1803 |
return expr_node, index, infer_literal_type(token.type)
|
| 1804 |
|
|
|
|
| 1805 |
if token.type in {"chrlit", "stringlit"}:
|
| 1806 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1807 |
return expr_node, index, infer_literal_type(token.type)
|
| 1808 |
|
| 1809 |
|
|
|
|
| 1810 |
if token.type in {"sunshine", "frost"}:
|
| 1811 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1812 |
return expr_node, index, infer_literal_type(token.type)
|
|
|
|
| 1834 |
if not list_info["is_list"] and list_info.get("type") != "vine":
|
| 1835 |
raise SemanticError(f"Semantic Error: '{list_name}' is not a list.", token.line)
|
| 1836 |
|
|
|
|
| 1837 |
expr_node, index, expr_type = parse_expression(tokens, index)
|
| 1838 |
return expr_node, index, expr_type
|
| 1839 |
|
|
|
|
| 1840 |
if token.type in {"intlit", "dbllit"}:
|
| 1841 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1842 |
return expr_node, index, infer_literal_type(token.type)
|
| 1843 |
|
|
|
|
| 1844 |
if token.type == "chrlit":
|
| 1845 |
expr_node, index = parse_expression_leaf(tokens, index)
|
| 1846 |
return expr_node, index, infer_literal_type(token.type)
|
| 1847 |
|
|
|
|
| 1848 |
if token.type == "stringlit":
|
| 1849 |
return ASTNode("Value", token.value, line=line), index + 1, "vine"
|
| 1850 |
|
|
|
|
| 1851 |
if token.type in {"sunshine", "frost"}:
|
| 1852 |
return ASTNode("Value", token.value, line=line), index + 1, "branch"
|
| 1853 |
|
|
|
|
| 1854 |
if token.type == "id" and tokens[index + 1].type == ".":
|
| 1855 |
var_info = symbol_table.lookup_variable(token.value)
|
| 1856 |
if not isinstance(var_info, str) and var_info["type"] in symbol_table.bundle_types:
|
| 1857 |
expr_node, index, expr_type = parse_expression(tokens, index)
|
| 1858 |
return expr_node, index, expr_type
|
| 1859 |
|
|
|
|
| 1860 |
if token.type == "id":
|
| 1861 |
var_info = symbol_table.lookup_variable(token.value)
|
| 1862 |
if isinstance(var_info, str):
|
|
|
|
| 1866 |
is_list = var_info.get("is_list", False)
|
| 1867 |
|
| 1868 |
if is_list and tokens[index + 1].type != "[":
|
|
|
|
| 1869 |
if tokens[index + 1].type in {"+", "-", "*", "/", "%", "**", "==", "!=", ">", "<", ">=", "<="}:
|
| 1870 |
op_token = tokens[index + 1]
|
| 1871 |
if index + 2 < len(tokens) and tokens[index + 2].type == "id":
|
|
|
|
| 1877 |
)
|
| 1878 |
raise SemanticError(f"Semantic Error: List '{token.value}' must be indexed with '[]' in expressions.", line)
|
| 1879 |
|
|
|
|
| 1880 |
if var_type in {"seed", "tree", "branch"}:
|
| 1881 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1882 |
|
|
|
|
| 1894 |
return ASTNode("Value", token.value, line=line), index + 1, var_type
|
| 1895 |
|
| 1896 |
elif var_type in symbol_table.bundle_types:
|
|
|
|
| 1897 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 1898 |
return expr_node, index, var_type
|
| 1899 |
|
|
|
|
| 1906 |
|
| 1907 |
|
| 1908 |
def infer_literal_type(token_type):
|
|
|
|
| 1909 |
if token_type == "intlit":
|
| 1910 |
return "seed"
|
| 1911 |
if token_type == "dbllit":
|
|
|
|
| 1927 |
|
| 1928 |
if tokens[index].value == "water":
|
| 1929 |
water_line = tokens[index].line
|
| 1930 |
+
index += 1
|
| 1931 |
if tokens[index].type != "(":
|
| 1932 |
raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
|
| 1933 |
+
index += 1
|
| 1934 |
water_type = None
|
| 1935 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 1936 |
water_type = tokens[index].value
|
| 1937 |
index += 1
|
| 1938 |
if tokens[index].type != ")":
|
| 1939 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter (e.g., water(seed)).", water_line)
|
| 1940 |
+
index += 1
|
| 1941 |
if water_type and not _types_compatible(var_type, water_type):
|
| 1942 |
raise SemanticError(f"Semantic Error: Type mismatch — cannot assign water({water_type}) to '{var_type}' variable.", water_line)
|
| 1943 |
value_node = ASTNode("Input", f"water({water_type})" if water_type else "water()", line=water_line)
|
|
|
|
| 1963 |
|
| 1964 |
expected_type = expected_params[len(provided_args)].children[0].value
|
| 1965 |
|
|
|
|
| 1966 |
expected_param = expected_params[len(provided_args)]
|
| 1967 |
is_array_param = any(child.node_type == "ArrayParam" for child in expected_param.children)
|
| 1968 |
|
| 1969 |
if is_array_param:
|
|
|
|
| 1970 |
if tokens[index].type != "id":
|
| 1971 |
raise SemanticError(f"Semantic Error: Expected array variable for parameter {len(provided_args) + 1} of '{func_name}'.", line)
|
| 1972 |
arg_name = tokens[index].value
|
|
|
|
| 2001 |
raise SemanticError(f"Semantic Error: Function '{func_name}' expects {len(expected_params)} arguments, but {len(provided_args)} were provided.", line)
|
| 2002 |
|
| 2003 |
for i, (arg_node, arg_type) in enumerate(provided_args):
|
| 2004 |
+
expected_type = expected_params[i].children[0].value
|
| 2005 |
|
| 2006 |
if expected_type in {"seed", "tree"} and arg_type == "seed":
|
| 2007 |
continue
|
|
|
|
| 2013 |
|
| 2014 |
|
| 2015 |
def parse_water_statement(tokens, index):
|
|
|
|
| 2016 |
line = tokens[index].line
|
| 2017 |
+
index += 1
|
| 2018 |
|
| 2019 |
if tokens[index].type != "(":
|
| 2020 |
raise SemanticError(f"Syntax Error: Expected '(' after water.", line)
|
| 2021 |
+
index += 1
|
| 2022 |
|
|
|
|
| 2023 |
if tokens[index].value in {"seed", "tree", "leaf", "branch", "vine"}:
|
| 2024 |
water_type = tokens[index].value
|
| 2025 |
index += 1
|
| 2026 |
if tokens[index].type != ")":
|
| 2027 |
raise SemanticError(f"Semantic Error: water() accepts only an optional type parameter or a variable name.", line)
|
| 2028 |
+
index += 1
|
| 2029 |
if tokens[index].type != ";":
|
| 2030 |
raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
|
| 2031 |
+
index += 1
|
|
|
|
| 2032 |
input_node = ASTNode("Input", f"water({water_type})", line=line)
|
| 2033 |
return input_node, index
|
| 2034 |
|
| 2035 |
elif tokens[index].type == ")":
|
| 2036 |
+
index += 1
|
|
|
|
| 2037 |
if tokens[index].type != ";":
|
| 2038 |
raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
|
| 2039 |
+
index += 1
|
| 2040 |
input_node = ASTNode("Input", "water()", line=line)
|
| 2041 |
return input_node, index
|
| 2042 |
|
| 2043 |
elif tokens[index].type == "id":
|
|
|
|
| 2044 |
var_name = tokens[index].value
|
| 2045 |
var_info = symbol_table.lookup_variable(var_name)
|
| 2046 |
if isinstance(var_info, str):
|
|
|
|
| 2048 |
if var_info.get("is_fertile", False):
|
| 2049 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' is declared as fertile and cannot be re-assigned a value.", line)
|
| 2050 |
var_type = var_info["type"]
|
| 2051 |
+
index += 1
|
| 2052 |
|
|
|
|
| 2053 |
if tokens[index].type == "[":
|
| 2054 |
if not var_info.get("is_list", False) and var_info.get("type") != "vine":
|
| 2055 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' is not a list.", line)
|
| 2056 |
+
index += 1
|
| 2057 |
index_expr, index, idx_type = parse_equality(tokens, index)
|
| 2058 |
if idx_type is not None and idx_type != "seed":
|
| 2059 |
raise SemanticError(f"Semantic Error: List index must be of type 'seed', got '{idx_type}'.", line)
|
| 2060 |
if tokens[index].type != "]":
|
| 2061 |
raise SemanticError(f"Syntax Error: Expected ']' after list index.", line)
|
| 2062 |
+
index += 1
|
| 2063 |
|
|
|
|
| 2064 |
index_wrapper = ASTNode("Index", line=line)
|
| 2065 |
index_wrapper.add_child(index_expr)
|
| 2066 |
list_access_node = ListAccessNode(var_name, index_wrapper, line=line)
|
| 2067 |
|
|
|
|
| 2068 |
while tokens[index].type == "[":
|
| 2069 |
+
index += 1
|
| 2070 |
inner_expr, index, inner_type = parse_equality(tokens, index)
|
| 2071 |
if inner_type is not None and inner_type != "seed":
|
| 2072 |
raise SemanticError(f"Semantic Error: List index must be of type 'seed', got '{inner_type}'.", line)
|
| 2073 |
if tokens[index].type != "]":
|
| 2074 |
raise SemanticError(f"Syntax Error: Expected ']' after list index.", line)
|
| 2075 |
+
index += 1
|
| 2076 |
inner_wrapper = ASTNode("Index", line=line)
|
| 2077 |
inner_wrapper.add_child(inner_expr)
|
| 2078 |
list_access_node = ListAccessNode(list_access_node, inner_wrapper, line=line)
|
| 2079 |
|
| 2080 |
if tokens[index].type != ")":
|
| 2081 |
raise SemanticError(f"Semantic Error: Expected ')' after water(arr[i]).", line)
|
| 2082 |
+
index += 1
|
| 2083 |
if tokens[index].type != ";":
|
| 2084 |
raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
|
| 2085 |
+
index += 1
|
| 2086 |
input_node = ASTNode("Input", f"water({var_type})", line=line)
|
| 2087 |
assignment_node = AssignmentNode(list_access_node, input_node, line=line)
|
| 2088 |
return assignment_node, index
|
| 2089 |
|
| 2090 |
if tokens[index].type != ")":
|
| 2091 |
raise SemanticError(f"Semantic Error: water() accepts only a single variable name or type parameter.", line)
|
| 2092 |
+
index += 1
|
| 2093 |
if tokens[index].type != ";":
|
| 2094 |
raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
|
| 2095 |
+
index += 1
|
| 2096 |
|
|
|
|
| 2097 |
input_node = ASTNode("Input", f"water({var_type})", line=line)
|
| 2098 |
value_ident = ASTNode("Identifier", var_name, line=line)
|
| 2099 |
assignment_node = AssignmentNode(var_name, input_node, line=line)
|
|
|
|
| 2183 |
raise SemanticError(f"Semantic Error: Variable '{identif_name}' used before declaration.", line)
|
| 2184 |
|
| 2185 |
if tokens[index + 1].type == "." and arg_info["type"] in symbol_table.bundle_types:
|
|
|
|
| 2186 |
expr_node, index, _ = parse_expression(tokens, index)
|
| 2187 |
args.append(expr_node)
|
| 2188 |
|
|
|
|
| 2294 |
raise SemanticError(f"Semantic Error: List '{arg_name}' must be indexed with '[]' in expressions.", line)
|
| 2295 |
|
| 2296 |
if tokens[index + 1].type == "." and arg_info["type"] in symbol_table.bundle_types:
|
|
|
|
| 2297 |
arg_node, index, _ = parse_expression(tokens, index)
|
| 2298 |
actual_args.append(arg_node)
|
| 2299 |
|
|
|
|
| 2335 |
if placeholder_count > 15:
|
| 2336 |
raise SemanticError(f"Semantic Error: Exceeded maximum amount of 15 arguments in plant statement.", line)
|
| 2337 |
|
|
|
|
| 2338 |
if placeholder_count > 0 and placeholder_count != len(actual_args):
|
| 2339 |
raise SemanticError(f"Semantic Error: Found {len(actual_args)} argument(s). Expected {placeholder_count} argument(s).", line)
|
| 2340 |
|
|
|
|
| 2447 |
|
| 2448 |
return FertileDeclarationNode(var_type, var_name, value_node, line=line), index
|
| 2449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2450 |
def parse_if(tokens, index, func_type):
|
| 2451 |
line = tokens[index].line
|
| 2452 |
+
index += 1
|
| 2453 |
|
| 2454 |
if tokens[index].type != "(":
|
| 2455 |
raise SemanticError(f"Syntax Error: Expected '(' after 'spring'.", line)
|
| 2456 |
index += 1
|
| 2457 |
|
| 2458 |
+
condition_expr, index, cond_type = parse_expression_branch(tokens, index)
|
| 2459 |
|
| 2460 |
if cond_type != "branch":
|
| 2461 |
raise SemanticError(f"Semantic Error: spring condition must be branch, got {cond_type}.", line)
|
|
|
|
| 2492 |
raise SemanticError(f"Syntax Error: Expected '{{' after 'spring' condition.", line)
|
| 2493 |
|
| 2494 |
|
|
|
|
| 2495 |
while tokens[index].value == "bud":
|
| 2496 |
index += 1
|
| 2497 |
|
|
|
|
| 2536 |
raise SemanticError(f"Syntax Error: Expected '{{' after else-if condition.", line)
|
| 2537 |
|
| 2538 |
|
|
|
|
| 2539 |
if tokens[index].value == "wither":
|
| 2540 |
index += 1
|
| 2541 |
|
|
|
|
| 2573 |
elif tokens[index].type == "id":
|
| 2574 |
identifier = tokens[index].value
|
| 2575 |
|
| 2576 |
+
if tokens[index+1].type == "(":
|
| 2577 |
func_info = symbol_table.lookup_function(identifier)
|
| 2578 |
|
| 2579 |
+
if isinstance(func_info, str):
|
| 2580 |
raise SemanticError(f"Semantic Error: Function '{identifier}' is not defined.", line)
|
| 2581 |
|
| 2582 |
return_type = func_info["return_type"]
|
|
|
|
| 2585 |
|
| 2586 |
return_expr, index = parse_expression_type(tokens, index, func_type)
|
| 2587 |
|
| 2588 |
+
else:
|
| 2589 |
var_info = symbol_table.lookup_variable(identifier)
|
| 2590 |
if isinstance(var_info, str):
|
| 2591 |
raise SemanticError(f"Semantic Error: Variable '{identifier}' used before declaration.", line)
|
| 2592 |
|
|
|
|
|
|
|
| 2593 |
is_member_access = var_info["type"] in symbol_table.bundle_types and tokens[index+1].type == "."
|
| 2594 |
if not is_member_access:
|
| 2595 |
if var_info["type"] not in [func_type, "seed", "tree"] and var_info["type"] != "seed" and var_info["type"] != "tree":
|
|
|
|
| 2603 |
return ReturnNode(return_expr, line=line), index
|
| 2604 |
|
| 2605 |
|
|
|
|
| 2606 |
def parse_for(tokens, index, func_type):
|
| 2607 |
line = tokens[index].line
|
| 2608 |
index += 1
|
|
|
|
| 2723 |
assign_node = AssignmentNode(list_access_node, value_node, line=tokens[index].line)
|
| 2724 |
assignments_node.add_child(assign_node)
|
| 2725 |
elif tokens[index + 1].type in {"++", "--"}:
|
|
|
|
| 2726 |
if var_type not in {"seed", "tree"}:
|
| 2727 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_type} in expression.", line)
|
| 2728 |
operator = tokens[index + 1].value
|
| 2729 |
+
index += 2
|
| 2730 |
assignments_node.add_child(UnaryOpNode(operator, list_access_node, "post", line=line))
|
| 2731 |
else:
|
| 2732 |
raise SemanticError("Semantic Error: Expected '=' or '++'/'--' after list access.", tokens[index + 1].line)
|
|
|
|
| 2757 |
assignments_node.add_child(node)
|
| 2758 |
|
| 2759 |
elif tokens[index + 1].type in {"+=", "-=", "*=", "/=", "%=", "**="}:
|
|
|
|
| 2760 |
compound_op = tokens[index + 1].value
|
| 2761 |
base_op = compound_op[:-1]
|
| 2762 |
cur_var_name = tokens[index].value
|
|
|
|
| 2768 |
cur_var_type = cur_var_info["type"]
|
| 2769 |
if cur_var_type not in {"seed", "tree"}:
|
| 2770 |
raise SemanticError(f"Semantic Error: Cannot use compound assignment on '{cur_var_name}' of type '{cur_var_type}'.", line)
|
|
|
|
| 2771 |
if base_op == "%" and cur_var_type != "seed":
|
| 2772 |
raise SemanticError(
|
| 2773 |
f"Semantic Error: Modulo operator '%' requires 'seed' (integer) operands, "
|
| 2774 |
f"but '{cur_var_name}' is of type 'tree'.",
|
| 2775 |
line,
|
| 2776 |
)
|
| 2777 |
+
index += 2
|
|
|
|
| 2778 |
rhs_node, index, rhs_type = parse_expression(tokens, index)
|
| 2779 |
if rhs_type not in {"seed", "tree"}:
|
| 2780 |
raise SemanticError(
|
|
|
|
| 2799 |
if isinstance(var_info, str):
|
| 2800 |
raise SemanticError(f"Semantic Error: Variable '{var_name}' used before declaration.", line)
|
| 2801 |
|
|
|
|
| 2802 |
if tokens[index + 1].type == "[":
|
| 2803 |
if var_info["type"] not in {"seed", "tree"}:
|
| 2804 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_info['type']} in expression.", line)
|
| 2805 |
list_access_node, index = parse_list_access(tokens, index)
|
| 2806 |
+
index += 1
|
| 2807 |
assignments_node.add_child(UnaryOpNode(operator, list_access_node, "pre", line=line))
|
| 2808 |
|
|
|
|
| 2809 |
elif tokens[index + 1].type == ".":
|
| 2810 |
obj_name = tokens[index].value
|
| 2811 |
if var_info["type"] not in symbol_table.bundle_types:
|
|
|
|
| 2815 |
if member_name not in bundle_members:
|
| 2816 |
raise SemanticError(f"Semantic Error: Bundle type '{var_info['type']}' has no member '{member_name}'.", line)
|
| 2817 |
member_type = bundle_members[member_name]
|
| 2818 |
+
index += 3
|
| 2819 |
target = MemberAccessNode(obj_name, member_name, line=line)
|
|
|
|
| 2820 |
while tokens[index].type == "." and member_type in symbol_table.bundle_types:
|
| 2821 |
next_member = tokens[index + 1].value
|
| 2822 |
nested_members = symbol_table.bundle_types[member_type]
|
|
|
|
| 2829 |
raise SemanticError(f"Semantic Error: Cannot apply '{operator}' to member '{member_name}' of type '{member_type}'.", line)
|
| 2830 |
assignments_node.add_child(UnaryOpNode(operator, target, "pre", line=line))
|
| 2831 |
|
|
|
|
| 2832 |
else:
|
| 2833 |
if var_info["type"] not in {"seed", "tree"}:
|
| 2834 |
raise SemanticError(f"Semantic Error: Cannot use '{var_name}' of type {var_info['type']} in expression.", line)
|
|
|
|
| 2978 |
raise SemanticError(f"Syntax Error: Expected '(' after 'harvest'.", line)
|
| 2979 |
index += 1
|
| 2980 |
|
| 2981 |
+
switch_type = None
|
| 2982 |
|
| 2983 |
if tokens[index].type == "id":
|
| 2984 |
var_info = symbol_table.lookup_variable(tokens[index].value)
|
|
|
|
| 2992 |
|
| 2993 |
|
| 2994 |
elif tokens[index].type in {"intlit", "chrlit", "sunshine", "frost"} or tokens[index].type in {"--", "++", "-", "("}:
|
|
|
|
| 2995 |
if tokens[index].type == "intlit" or tokens[index].type in {"--", "++", "-"}:
|
| 2996 |
switch_type = "seed"
|
| 2997 |
elif tokens[index].type == "chrlit":
|
|
|
|
| 2999 |
elif tokens[index].type in {"sunshine", "frost"}:
|
| 3000 |
switch_type = "branch"
|
| 3001 |
elif tokens[index].type == "(":
|
| 3002 |
+
switch_type = "seed"
|
| 3003 |
switch_expr, index, _ = parse_expression(tokens, index)
|
| 3004 |
|
| 3005 |
elif tokens[index].type in {"stringlit"}:
|
|
|
|
| 3023 |
|
| 3024 |
case_nodes = []
|
| 3025 |
default_case = None
|
| 3026 |
+
seen_case_values = set()
|
| 3027 |
|
| 3028 |
while tokens[index].value in {"variety"}:
|
| 3029 |
case_line = tokens[index].line
|
|
|
|
| 3033 |
if tokens[index].type not in {"chrlit", "stringlit", "sunshine", "frost", "intlit", "dbllit"}:
|
| 3034 |
raise SemanticError(f"Semantic Error: Expected valid literal value after 'variety'.", line)
|
| 3035 |
|
|
|
|
| 3036 |
lit_type_map = {
|
| 3037 |
"intlit": "seed",
|
| 3038 |
"dbllit": "tree",
|
|
|
|
| 3048 |
tokens[index].line,
|
| 3049 |
)
|
| 3050 |
|
|
|
|
| 3051 |
case_val_key = tokens[index].value
|
| 3052 |
if case_val_key in seen_case_values:
|
| 3053 |
raise SemanticError(
|
|
|
|
| 3184 |
index += 1
|
| 3185 |
|
| 3186 |
expr_node, index, idx_type = parse_equality(tokens, index)
|
|
|
|
| 3187 |
if idx_type is not None and idx_type != "seed":
|
| 3188 |
raise SemanticError(
|
| 3189 |
f"Semantic Error: List index must be of type 'seed', got '{idx_type}'.",
|
|
|
|
| 3223 |
index += 1
|
| 3224 |
|
| 3225 |
expr_node, index, idx_type = parse_equality(tokens, index)
|
|
|
|
| 3226 |
if idx_type is not None and idx_type != "seed":
|
| 3227 |
raise SemanticError(
|
| 3228 |
f"Semantic Error: List index must be of type 'seed', got '{idx_type}'.",
|
|
|
|
| 3242 |
return any(ctx in {"WhileNode", "DoWhileNode", "SwitchNode", "ForNode"} for ctx in context_stack)
|
| 3243 |
|
| 3244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3245 |
def analyze_semantics(tokens):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3246 |
try:
|
| 3247 |
filtered = [t for t in tokens if t.type != '\n']
|
| 3248 |
ast = build_ast(filtered)
|
|
|
|
| 3291 |
}
|
| 3292 |
|
| 3293 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Backend/parser/parser.py
CHANGED
|
@@ -1,23 +1,8 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# LL(1) PARSER - Table-driven syntax analysis
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from Backend/Gal_Parser.py during the modular restructure.
|
| 5 |
-
# Pipeline position:
|
| 6 |
-
# lex -> THIS FILE (parse + delegate to builder) -> semantic -> ICG -> interpret
|
| 7 |
-
#
|
| 8 |
-
# The parser exposes two entry points:
|
| 9 |
-
# parse(tokens) : syntax-only check, returns (success, errors)
|
| 10 |
-
# parse_and_build(tokens) : syntax + AST construction (delegates to
|
| 11 |
-
# parser.builder.build_ast)
|
| 12 |
-
# ============================================================================
|
| 13 |
from __future__ import annotations
|
| 14 |
|
| 15 |
from dataclasses import dataclass
|
| 16 |
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Set, Tuple
|
| 17 |
|
| 18 |
-
# AST builder imports — the parser builds the AST after LL(1) validation.
|
| 19 |
-
# After the restructure, build_ast lives in parser/builder.py (a sibling),
|
| 20 |
-
# not in GALsemantic.py.
|
| 21 |
from .builder import (
|
| 22 |
build_ast as _build_ast,
|
| 23 |
symbol_table as _builder_st,
|
|
@@ -25,14 +10,8 @@ from .builder import (
|
|
| 25 |
from semantic.errors import SemanticError as _SemanticError
|
| 26 |
|
| 27 |
|
| 28 |
-
# ============================================================================
|
| 29 |
-
# TOKEN VIEW HELPERS - The parser accepts tokens as either Token objects
|
| 30 |
-
# or dicts (e.g. when reloaded from JSON). _TokView gives a uniform shape
|
| 31 |
-
# so the parsing loop doesn't have to handle both cases.
|
| 32 |
-
# ============================================================================
|
| 33 |
@dataclass(frozen=True)
|
| 34 |
class _TokView:
|
| 35 |
-
"""Lightweight view to normalize token access."""
|
| 36 |
type: str
|
| 37 |
value: str
|
| 38 |
line: int
|
|
@@ -40,16 +19,13 @@ class _TokView:
|
|
| 40 |
|
| 41 |
|
| 42 |
def _as_tok(token: Any) -> _TokView:
|
| 43 |
-
"""Normalize token objects/dicts to a common view."""
|
| 44 |
if isinstance(token, Mapping):
|
| 45 |
-
# Dict-shaped token (came from a JSON request body)
|
| 46 |
return _TokView(
|
| 47 |
type=str(token.get("type", "")),
|
| 48 |
value=str(token.get("value", "")),
|
| 49 |
line=int(token.get("line", 0) or 0),
|
| 50 |
col=int(token.get("col", 0) or 0),
|
| 51 |
)
|
| 52 |
-
# Token object (the lexer's normal output)
|
| 53 |
return _TokView(
|
| 54 |
type=str(getattr(token, "type", "")),
|
| 55 |
value=str(getattr(token, "value", "")),
|
|
@@ -58,9 +34,6 @@ def _as_tok(token: Any) -> _TokView:
|
|
| 58 |
)
|
| 59 |
|
| 60 |
|
| 61 |
-
# ============================================================================
|
| 62 |
-
# LL1Parser - The table-driven LL(1) parser
|
| 63 |
-
# ============================================================================
|
| 64 |
class LL1Parser:
|
| 65 |
def __init__(
|
| 66 |
self,
|
|
@@ -78,32 +51,19 @@ class LL1Parser:
|
|
| 78 |
self.predict_sets = predict_sets
|
| 79 |
self.first_sets = first_sets
|
| 80 |
|
| 81 |
-
# GAL uses λ to denote the empty production in its CFG/FIRST/FOLLOW/PREDICT sets.
|
| 82 |
self.epsilon_symbols: Set[str] = set(epsilon_symbols)
|
| 83 |
self.start_symbol = start_symbol
|
| 84 |
self.end_marker = end_marker
|
| 85 |
|
| 86 |
-
# If lexer emits newlines as tokens, treat them as skippable by default.
|
| 87 |
-
# The lexer uses '\n' as the token type for newlines
|
| 88 |
self.skip_token_types: Set[str] = set(skip_token_types or {"\n"})
|
| 89 |
self.token_type_alias = token_type_alias or {
|
| 90 |
-
# Helpful defaults based on inconsistencies seen in the document's token labels
|
| 91 |
-
# (e.g., 'idf' vs 'id', 'dbllit' vs 'dblit').
|
| 92 |
'idf': 'id',
|
| 93 |
'dbllit': 'dblit',
|
| 94 |
}
|
| 95 |
|
| 96 |
self.parsing_table: Dict[str, Dict[str, List[str]]] = self.construct_parsing_table()
|
| 97 |
|
| 98 |
-
# ========================================================================
|
| 99 |
-
# PARSING TABLE CONSTRUCTION
|
| 100 |
-
# Builds the LL(1) parsing table from the CFG + PREDICT sets.
|
| 101 |
-
# Layout: table[non_terminal][lookahead_token] = production_to_apply.
|
| 102 |
-
# Raises ValueError if the grammar isn't LL(1) (two productions claim
|
| 103 |
-
# the same lookahead) — this is the compile-time correctness check.
|
| 104 |
-
# ========================================================================
|
| 105 |
def construct_parsing_table(self) -> Dict[str, Dict[str, List[str]]]:
|
| 106 |
-
"""Build LL(1) parsing table using provided PREDICT sets."""
|
| 107 |
table: Dict[str, Dict[str, List[str]]] = {}
|
| 108 |
|
| 109 |
for non_terminal, productions in self.cfg.items():
|
|
@@ -112,8 +72,6 @@ class LL1Parser:
|
|
| 112 |
key = (non_terminal, tuple(production))
|
| 113 |
terms = self.predict_sets.get(key, set())
|
| 114 |
for terminal in terms:
|
| 115 |
-
# An LL(1) conflict means the grammar can't be parsed
|
| 116 |
-
# with one token of lookahead — fix the grammar first.
|
| 117 |
if terminal in row and row[terminal] != production:
|
| 118 |
raise ValueError(
|
| 119 |
f"LL(1) conflict at {non_terminal} with lookahead {terminal}: "
|
|
@@ -124,14 +82,7 @@ class LL1Parser:
|
|
| 124 |
return table
|
| 125 |
|
| 126 |
|
| 127 |
-
# ========================================================================
|
| 128 |
-
# TOKEN NORMALIZATION HELPERS - Bridge between lexer and grammar
|
| 129 |
-
# The lexer emits some token types that don't match the CFG terminal
|
| 130 |
-
# names (e.g. 'dbllit' from lexer vs 'dblit' in cfg.py). The alias map
|
| 131 |
-
# papers over those differences in one place.
|
| 132 |
-
# ========================================================================
|
| 133 |
def _normalize_token_type(self, token_type: str) -> str:
|
| 134 |
-
"""Map lexer token types into the terminal names used by the CFG."""
|
| 135 |
return self.token_type_alias.get(token_type, token_type)
|
| 136 |
|
| 137 |
def _ensure_eof(self, toks: List[_TokView]) -> List[_TokView]:
|
|
@@ -143,17 +94,13 @@ class LL1Parser:
|
|
| 143 |
toks = toks + [_TokView(self.end_marker, self.end_marker, last_line, last_col)]
|
| 144 |
return toks
|
| 145 |
|
| 146 |
-
# ── human-readable names for terminals ──────────────────────────
|
| 147 |
_TERMINAL_DISPLAY: Dict[str, str] = {
|
| 148 |
-
# literals / identifiers
|
| 149 |
'id': 'id', 'intlit': 'intlit', 'dblit': 'dblit',
|
| 150 |
'stringlit': 'stringlit', 'chrlit': 'chrlit',
|
| 151 |
'sunshine': "'sunshine'", 'frost': "'frost'",
|
| 152 |
-
# data types
|
| 153 |
'seed': "'seed'", 'tree': "'tree'",
|
| 154 |
'leaf': "'leaf'", 'branch': "'branch'",
|
| 155 |
'vine': "'vine'",
|
| 156 |
-
# keywords
|
| 157 |
'bundle': "'bundle'", 'fertile': "'fertile'",
|
| 158 |
'pollinate': "'pollinate'", 'root': "'root'",
|
| 159 |
'reclaim': "'reclaim'", 'spring': "'spring'",
|
|
@@ -164,52 +111,31 @@ class LL1Parser:
|
|
| 164 |
'prune': "'prune'", 'skip': "'skip'",
|
| 165 |
'water': "'water'", 'plant': "'plant'",
|
| 166 |
'empty': "'empty'",
|
| 167 |
-
# special
|
| 168 |
'EOF': 'end of file',
|
| 169 |
}
|
| 170 |
|
| 171 |
-
# ========================================================================
|
| 172 |
-
# ERROR-MESSAGE HELPERS
|
| 173 |
-
# When the parser hits a token that doesn't match any production, we
|
| 174 |
-
# build a friendly "Expected: X, Y, Z" string from the parsing-table
|
| 175 |
-
# row. _generate_helpful_error adds context-aware hints (e.g. "did you
|
| 176 |
-
# mean '==' instead of '='?").
|
| 177 |
-
# ========================================================================
|
| 178 |
def _format_expected(self, expected: Set[str], non_terminal: Optional[str] = None) -> str:
|
| 179 |
-
"""Return a human-readable string listing the expected terminals.
|
| 180 |
-
|
| 181 |
-
The *expected* set comes directly from the parsing-table row for
|
| 182 |
-
*non_terminal*, which is itself built from the PREDICT sets.
|
| 183 |
-
"""
|
| 184 |
symbols = {'(', ')', '{', '}', ';', ',', '=', '+', '-', '*', '/', '%',
|
| 185 |
'++', '--', '==', '!=', '<', '>', '<=', '>=', '&&', '||',
|
| 186 |
'!', '~', '+=', '-=', '*=', '/=', '%=', '.', '[', ']', ':', '`'}
|
| 187 |
-
# Only hide 'reclaim' from suggestions when the source already contains one
|
| 188 |
has_reclaim = any(tk.type == 'reclaim' for tk in getattr(self, '_current_tokens', []))
|
| 189 |
|
| 190 |
parts: List[str] = []
|
| 191 |
for t in sorted(expected):
|
| 192 |
if t == 'reclaim' and has_reclaim:
|
| 193 |
-
continue
|
| 194 |
if t in self._TERMINAL_DISPLAY:
|
| 195 |
parts.append(self._TERMINAL_DISPLAY[t])
|
| 196 |
elif t in symbols:
|
| 197 |
parts.append(f"'{t}'")
|
| 198 |
elif t.startswith('<') and t.endswith('>'):
|
| 199 |
-
continue
|
| 200 |
else:
|
| 201 |
parts.append(f"'{t}'")
|
| 202 |
if not parts:
|
| 203 |
return 'nothing'
|
| 204 |
return f"Expected: {', '.join(parts)}"
|
| 205 |
|
| 206 |
-
# ========================================================================
|
| 207 |
-
# _GENERATE_HELPFUL_ERROR - The big one
|
| 208 |
-
# When a token doesn't match, this method inspects context (previous
|
| 209 |
-
# tokens, the non-terminal we're inside, common typos like '==='/' &&&')
|
| 210 |
-
# and produces a SPECIFIC error rather than a generic "Expected: ..."
|
| 211 |
-
# message. This is what makes GAL's error messages friendly.
|
| 212 |
-
# ========================================================================
|
| 213 |
def _generate_helpful_error(
|
| 214 |
self,
|
| 215 |
non_terminal: str,
|
|
@@ -221,18 +147,14 @@ class LL1Parser:
|
|
| 221 |
index: int,
|
| 222 |
toks: List[_TokView]
|
| 223 |
) -> str:
|
| 224 |
-
"""Generate contextual error messages for common syntax mistakes."""
|
| 225 |
|
| 226 |
-
# Parameter type keywords (used in multiple checks below)
|
| 227 |
param_type_tokens = {'seed', 'tree', 'leaf', 'vine', 'branch'}
|
| 228 |
|
| 229 |
-
# PRIORITY CHECK: Unexpected end of file (missing closing brace)
|
| 230 |
if token_type == self.end_marker or token_value == '':
|
| 231 |
if '}' in expected:
|
| 232 |
return f"SYNTAX error line {line} col {col} Unexpected end of file. Missing closing '}}'. {self._format_expected(expected, non_terminal)}"
|
| 233 |
return f"SYNTAX error line {line} col {col} Unexpected end of file. {self._format_expected(expected, non_terminal)}"
|
| 234 |
|
| 235 |
-
# PRIORITY CHECK: Detect === operator (tokenized as == followed by =)
|
| 236 |
if token_type == '=' and index > 0:
|
| 237 |
prev_index = index - 1
|
| 238 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -241,7 +163,6 @@ class LL1Parser:
|
|
| 241 |
if prev_index >= 0 and toks[prev_index].type == '==':
|
| 242 |
return f"SYNTAX error line {line} col {col} Invalid operator '==='. Use '==' for equality comparison. {self._format_expected(expected, non_terminal)}"
|
| 243 |
|
| 244 |
-
# PRIORITY CHECK: Detect &&& operator (tokenized as && followed by &)
|
| 245 |
if token_type == '&' and index > 0:
|
| 246 |
prev_index = index - 1
|
| 247 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -249,10 +170,8 @@ class LL1Parser:
|
|
| 249 |
|
| 250 |
if prev_index >= 0 and toks[prev_index].type == '&&':
|
| 251 |
return f"SYNTAX error line {line} col {col} Invalid operator '&&&'. Use '&&' for logical AND. {self._format_expected(expected, non_terminal)}"
|
| 252 |
-
# Single & without preceding &&
|
| 253 |
return f"SYNTAX error line {line} col {col} Invalid operator '&'. Use '&&' for logical AND. {self._format_expected(expected, non_terminal)}"
|
| 254 |
|
| 255 |
-
# PRIORITY CHECK: Detect ||| operator (tokenized as || followed by |)
|
| 256 |
if token_type == '|' and index > 0:
|
| 257 |
prev_index = index - 1
|
| 258 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -260,23 +179,18 @@ class LL1Parser:
|
|
| 260 |
|
| 261 |
if prev_index >= 0 and toks[prev_index].type == '||':
|
| 262 |
return f"SYNTAX error line {line} col {col} Invalid operator '|||'. Use '||' for logical OR. {self._format_expected(expected, non_terminal)}"
|
| 263 |
-
# Single | without preceding ||
|
| 264 |
return f"SYNTAX error line {line} col {col} Invalid operator '|'. Use '||' for logical OR. {self._format_expected(expected, non_terminal)}"
|
| 265 |
|
| 266 |
-
# PRIORITY CHECK: Detect malformed literals in current token
|
| 267 |
if token_type == 'chrlit' and token_value and not token_value.endswith("'"):
|
| 268 |
return f"SYNTAX error line {line} col {col} Missing closing single quote in character literal. {self._format_expected(expected, non_terminal)}"
|
| 269 |
|
| 270 |
if token_type == 'stringlit' and token_value and not token_value.endswith('"'):
|
| 271 |
return f"SYNTAX error line {line} col {col} Missing closing double quote in string literal. {self._format_expected(expected, non_terminal)}"
|
| 272 |
|
| 273 |
-
# PRIORITY CHECK: Missing semicolon after 'reclaim'
|
| 274 |
if non_terminal == '<reclaim_value>' and token_type == '}':
|
| 275 |
return f"SYNTAX error line {line} col {col} Missing ';' after 'reclaim'. Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 276 |
|
| 277 |
-
# Check for unmatched closing parenthesis
|
| 278 |
if token_type == ')' and ')' not in expected:
|
| 279 |
-
# Check if previous token was a binary operator - if so, it's a missing operand issue
|
| 280 |
if index > 0:
|
| 281 |
prev_index = index - 1
|
| 282 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -288,11 +202,9 @@ class LL1Parser:
|
|
| 288 |
if prev_tok.type in binary_operators:
|
| 289 |
return f"SYNTAX error line {line} col {col} Unexpected token ')' after binary operator '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 290 |
|
| 291 |
-
# Check for trailing comma in parameter list
|
| 292 |
if prev_tok.type == ',' and param_type_tokens & expected:
|
| 293 |
return f"SYNTAX error line {line} col {col}: Unexpected token ')'. Expected parameter type (seed, tree, leaf, vine, branch) after ','"
|
| 294 |
|
| 295 |
-
# Check if keyword( ) — user tried to use a reserved keyword as a function name
|
| 296 |
if prev_tok.type == '(':
|
| 297 |
kw_index = prev_index - 1
|
| 298 |
while kw_index >= 0 and toks[kw_index].type in self.skip_token_types:
|
|
@@ -316,15 +228,12 @@ class LL1Parser:
|
|
| 316 |
desc = keyword_descriptions[kw_tok.type]
|
| 317 |
return f"SYNTAX error line {kw_tok.line} col {kw_tok.col} '{kw_tok.value}' is a reserved keyword ({desc}) and cannot be used as a function name."
|
| 318 |
|
| 319 |
-
# Otherwise, it's an unmatched closing parenthesis
|
| 320 |
return f"SYNTAX error line {line} col {col} Unexpected token ')' - no matching '(' found in expression. {self._format_expected(expected, non_terminal)}"
|
| 321 |
|
| 322 |
-
# Explain assignment operators that occur where no valid assignment can start.
|
| 323 |
assignment_operators = {'+=', '-=', '*=', '/=', '%='}
|
| 324 |
if token_type in assignment_operators:
|
| 325 |
return f"SYNTAX error line {line} col {col} Assignment operator '{token_value}' must follow a modifiable assignment target. {self._format_expected(expected, non_terminal)}"
|
| 326 |
|
| 327 |
-
# Check for '=' after binary operator (likely space in compound assignment like 'a + = 2')
|
| 328 |
if token_type == '=':
|
| 329 |
if index > 0:
|
| 330 |
prev_index = index - 1
|
|
@@ -335,7 +244,6 @@ class LL1Parser:
|
|
| 335 |
prev_tok = toks[prev_index]
|
| 336 |
compound_op_bases = {'+', '-', '*', '/', '%'}
|
| 337 |
if prev_tok.type in compound_op_bases:
|
| 338 |
-
# Check if there's an identifier before the operator
|
| 339 |
prev_prev_index = prev_index - 1
|
| 340 |
while prev_prev_index >= 0 and toks[prev_prev_index].type in self.skip_token_types:
|
| 341 |
prev_prev_index -= 1
|
|
@@ -345,12 +253,10 @@ class LL1Parser:
|
|
| 345 |
compound_op = f"{prev_tok.value}="
|
| 346 |
return f"SYNTAX error line {line} col {col} Unexpected token '=' after operator '{prev_tok.value}'. Did you mean '{id_tok.value} {compound_op}' (compound assignment with no space)? {self._format_expected(expected, non_terminal)}"
|
| 347 |
|
| 348 |
-
# Check for '{' at start of program - likely caused by lexical error preventing 'root' token
|
| 349 |
if token_type == '{' and non_terminal in {'<program>', '<global_declaration>'}:
|
| 350 |
if index == 0 or (index <= 2 and all(toks[i].type in self.skip_token_types for i in range(index))):
|
| 351 |
return f"SYNTAX error line {line} col {col} 'root' function declaration is missing opening '('. {self._format_expected(expected, non_terminal)}"
|
| 352 |
|
| 353 |
-
# Check next token if it exists (look-ahead for malformed literals)
|
| 354 |
if index + 1 < len(toks):
|
| 355 |
next_tok = toks[index + 1]
|
| 356 |
if next_tok.type == 'chrlit' and next_tok.value and not next_tok.value.endswith("'"):
|
|
@@ -359,21 +265,17 @@ class LL1Parser:
|
|
| 359 |
if next_tok.type == 'stringlit' and next_tok.value and not next_tok.value.endswith('"'):
|
| 360 |
return f"SYNTAX error line {next_tok.line} col {next_tok.col} Missing closing double quote in string literal. {self._format_expected(expected, non_terminal)}"
|
| 361 |
|
| 362 |
-
# Check for semicolon after bundle closing brace (common mistake)
|
| 363 |
if token_type == ';' and non_terminal == '<global_declaration>':
|
| 364 |
-
# Look back to see if previous token was '}' from a bundle definition
|
| 365 |
if index > 0:
|
| 366 |
prev_index = index - 1
|
| 367 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
| 368 |
prev_index -= 1
|
| 369 |
|
| 370 |
if prev_index >= 0 and toks[prev_index].type == '}':
|
| 371 |
-
# Look further back to see if this was a bundle definition
|
| 372 |
bundle_index = prev_index - 1
|
| 373 |
while bundle_index >= 0 and toks[bundle_index].type in self.skip_token_types:
|
| 374 |
bundle_index -= 1
|
| 375 |
|
| 376 |
-
# Check if we can find 'bundle' keyword before this
|
| 377 |
found_bundle = False
|
| 378 |
for i in range(bundle_index, max(0, bundle_index - 20) - 1, -1):
|
| 379 |
if toks[i].type == 'bundle':
|
|
@@ -384,7 +286,6 @@ class LL1Parser:
|
|
| 384 |
expected_str = self._format_expected(expected, non_terminal)
|
| 385 |
return f"SYNTAX error line {line} col {col} Unexpected token ';' after bundle definition closing '}}'. ';' is not in {expected_str}. Remove the trailing ';'"
|
| 386 |
|
| 387 |
-
# Check for common invalid keywords from other languages (check early)
|
| 388 |
common_keyword_mistakes = {
|
| 389 |
'function': 'pollinate',
|
| 390 |
'int': 'seed',
|
|
@@ -418,22 +319,16 @@ class LL1Parser:
|
|
| 418 |
correct_keyword = common_keyword_mistakes[token_value]
|
| 419 |
return f"SYNTAX error line {line} col {col} '{token_value}' is not a GAL keyword. Use '{correct_keyword}' instead."
|
| 420 |
|
| 421 |
-
# Check for bundle definition inside a function (common mistake)
|
| 422 |
if token_type == '{' and (non_terminal == '<bundle_or_var>' or non_terminal == '<bundle_mem_dec>'):
|
| 423 |
-
# This means we saw "bundle id {" but inside a function body
|
| 424 |
-
# In global scope, this would be a bundle definition, but it's not allowed inside functions
|
| 425 |
return f"SYNTAX error line {line} col {col} Bundle definitions must be at global scope (outside all functions). Move this bundle definition before 'root()'. {self._format_expected(expected, non_terminal)}"
|
| 426 |
|
| 427 |
-
# Check if semicolon is expected and we're seeing a keyword/statement starter
|
| 428 |
statement_starters = {
|
| 429 |
'reclaim', 'spring', 'wither', 'bud', 'grow', 'cultivate', 'tend',
|
| 430 |
'harvest', 'prune', 'skip', 'water', 'plant', 'seed', 'leaf',
|
| 431 |
'branch', 'tree', 'vine', 'bundle', 'fertile', 'pollinate', 'root', 'id'
|
| 432 |
}
|
| 433 |
|
| 434 |
-
# Check for missing ':' after variety/soil expression (priority over ';' check)
|
| 435 |
if ':' in expected and token_type in statement_starters:
|
| 436 |
-
# Walk back to find 'variety' or 'soil'
|
| 437 |
context_keyword = None
|
| 438 |
if index > 0:
|
| 439 |
scan = index - 1
|
|
@@ -447,7 +342,6 @@ class LL1Parser:
|
|
| 447 |
if toks[scan].type == 'soil':
|
| 448 |
context_keyword = 'soil'
|
| 449 |
break
|
| 450 |
-
# Skip expression tokens between variety and here
|
| 451 |
if toks[scan].type in {'id', 'intlit', 'dblit', 'stringlit', 'chrlit',
|
| 452 |
'sunshine', 'frost', '+', '-', '*', '/', '%',
|
| 453 |
'==', '!=', '<', '>', '<=', '>=', '&&', '||',
|
|
@@ -460,8 +354,6 @@ class LL1Parser:
|
|
| 460 |
|
| 461 |
if ';' in expected:
|
| 462 |
if token_type in statement_starters or token_type == 'id':
|
| 463 |
-
# Look back at previous token to show what was incomplete
|
| 464 |
-
# Skip past newlines to find the actual meaningful previous token
|
| 465 |
if index > 0:
|
| 466 |
prev_index = index - 1
|
| 467 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -470,19 +362,16 @@ class LL1Parser:
|
|
| 470 |
if prev_index >= 0:
|
| 471 |
prev_tok = toks[prev_index]
|
| 472 |
|
| 473 |
-
# Check if previous token is '=' (missing value in assignment)
|
| 474 |
if prev_tok.type == '=':
|
| 475 |
prev_line = prev_tok.line
|
| 476 |
prev_col = prev_tok.col
|
| 477 |
return f"SYNTAX error line {prev_line} col {prev_col} Missing value after '=' operator. {self._format_expected(expected, non_terminal)}"
|
| 478 |
|
| 479 |
-
# Check if previous token is a malformed character literal (missing closing quote)
|
| 480 |
if prev_tok.type == 'chrlit' and prev_tok.value and not prev_tok.value.endswith("'"):
|
| 481 |
prev_line = prev_tok.line
|
| 482 |
prev_col = prev_tok.col
|
| 483 |
return f"SYNTAX error line {prev_line} col {prev_col} Missing closing single quote in character literal. {self._format_expected(expected, non_terminal)}"
|
| 484 |
|
| 485 |
-
# Check if previous token is a malformed string literal (missing closing quote)
|
| 486 |
if prev_tok.type == 'stringlit' and prev_tok.value and not prev_tok.value.endswith('"'):
|
| 487 |
prev_line = prev_tok.line
|
| 488 |
prev_col = prev_tok.col
|
|
@@ -491,12 +380,11 @@ class LL1Parser:
|
|
| 491 |
prev_line = prev_tok.line
|
| 492 |
prev_col = prev_tok.col + len(str(prev_tok.value))
|
| 493 |
expected_str = self._format_expected(expected, non_terminal)
|
| 494 |
-
if prev_line != line:
|
| 495 |
return (f"SYNTAX error line {prev_line} col {prev_col} Unexpected token '{token_value}' after '{prev_tok.value}'. {expected_str}")
|
| 496 |
else:
|
| 497 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {expected_str}"
|
| 498 |
elif token_type == '}':
|
| 499 |
-
# Missing semicolon before closing brace
|
| 500 |
if index > 0:
|
| 501 |
prev_index = index - 1
|
| 502 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -505,17 +393,14 @@ class LL1Parser:
|
|
| 505 |
if prev_index >= 0:
|
| 506 |
prev_tok = toks[prev_index]
|
| 507 |
|
| 508 |
-
# Check for missing ';' after 'reclaim'
|
| 509 |
if prev_tok.type == 'reclaim':
|
| 510 |
return f"SYNTAX error line {prev_tok.line} col {prev_tok.col + len('reclaim')} Missing ';' after 'reclaim'. {self._format_expected(expected, non_terminal)}"
|
| 511 |
|
| 512 |
-
# Check if previous token is a malformed character literal (missing closing quote)
|
| 513 |
if prev_tok.type == 'chrlit' and prev_tok.value and not prev_tok.value.endswith("'"):
|
| 514 |
prev_line = prev_tok.line
|
| 515 |
prev_col = prev_tok.col
|
| 516 |
return f"SYNTAX error line {prev_line} col {prev_col} Missing closing single quote in character literal. {self._format_expected(expected, non_terminal)}"
|
| 517 |
|
| 518 |
-
# Check if previous token is a malformed string literal (missing closing quote)
|
| 519 |
if prev_tok.type == 'stringlit' and prev_tok.value and not prev_tok.value.endswith('"'):
|
| 520 |
prev_line = prev_tok.line
|
| 521 |
prev_col = prev_tok.col
|
|
@@ -529,22 +414,18 @@ class LL1Parser:
|
|
| 529 |
else:
|
| 530 |
return f"SYNTAX error line {line} col {col} Unexpected token '}}'. {expected_str}"
|
| 531 |
|
| 532 |
-
# Check for missing reclaim statement
|
| 533 |
if 'reclaim' in expected and token_type == '}':
|
| 534 |
return f"SYNTAX error line {line} col {col} expected 'reclaim;' before '}}'. All functions, including root(), must end with 'reclaim;'. {self._format_expected(expected, non_terminal)}"
|
| 535 |
|
| 536 |
-
# Check for missing prune (break) in variety (case) statements
|
| 537 |
if 'prune' in expected and token_type in {'variety', 'soil', '}'}:
|
| 538 |
if token_type == 'variety':
|
| 539 |
return f"SYNTAX error line {line} col {col} expected 'prune;' before next 'variety'. Each case in 'harvest' must end with 'prune;'. {self._format_expected(expected, non_terminal)}"
|
| 540 |
elif token_type == 'soil':
|
| 541 |
return f"SYNTAX error line {line} col {col} expected 'prune;' before 'soil'. Each case must end with 'prune;'. {self._format_expected(expected, non_terminal)}"
|
| 542 |
-
else:
|
| 543 |
return f"SYNTAX error line {line} col {col} expected 'prune;' before closing '}}'. Each case must end with 'prune;'. {self._format_expected(expected, non_terminal)}"
|
| 544 |
|
| 545 |
-
# Check for unary minus/plus usage (not supported in grammar) - must check BEFORE missing value error
|
| 546 |
if token_type in {'-', '+'} and non_terminal in {'<expression>', '<factor>', '<term>', '<arithmetic>', '<logic_or>', '<logic_and>', '<relational>', '<init_val>'}:
|
| 547 |
-
# Look back to see what preceded this operator
|
| 548 |
if index > 0:
|
| 549 |
prev_index = index - 1
|
| 550 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -552,14 +433,12 @@ class LL1Parser:
|
|
| 552 |
|
| 553 |
if prev_index >= 0:
|
| 554 |
prev_tok = toks[prev_index]
|
| 555 |
-
# If preceded by assignment or operators, likely trying to use unary operator
|
| 556 |
if prev_tok.type in {'=', '+=', '-=', '*=', '/=', '%=', '(', ','}:
|
| 557 |
if token_value == '-':
|
| 558 |
return f"SYNTAX error line {line} col {col} Unary '-' not supported. Use '~' for negative numbers (e.g., '~5') or '(0 - value)' for negation. {self._format_expected(expected, non_terminal)}"
|
| 559 |
else:
|
| 560 |
return f"SYNTAX error line {line} col {col} Unary '+' operator not supported. Use parentheses for expressions like '(0 + value)'. {self._format_expected(expected, non_terminal)}"
|
| 561 |
|
| 562 |
-
# Check for binary operator after opening parenthesis (prefix notation attempt)
|
| 563 |
if token_type in {'*', '/', '%', '+', '-', '`', '&&', '||', '==', '!=', '<', '>', '<=', '>='}:
|
| 564 |
if index > 0:
|
| 565 |
prev_index = index - 1
|
|
@@ -569,10 +448,7 @@ class LL1Parser:
|
|
| 569 |
if prev_index >= 0 and toks[prev_index].type == '(':
|
| 570 |
return f"SYNTAX error line {line} col {col} Unexpected binary operator '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 571 |
|
| 572 |
-
# Check for missing opening parenthesis
|
| 573 |
if '(' in expected and token_type != '(':
|
| 574 |
-
# Check if previous token was an arithmetic/logical operator - missing operand case
|
| 575 |
-
# BUT skip if current token is +/- (handled above as unary operator case)
|
| 576 |
if index > 0 and token_type not in {'-', '+'}:
|
| 577 |
prev_index = index - 1
|
| 578 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -580,19 +456,14 @@ class LL1Parser:
|
|
| 580 |
|
| 581 |
if prev_index >= 0:
|
| 582 |
prev_tok = toks[prev_index]
|
| 583 |
-
# If an operator precedes the current token, user likely forgot an operand
|
| 584 |
if prev_tok.type in {'+', '-', '*', '/', '%', '`', '=', '+=', '-=', '*=', '/=', '%=', '&&', '||', '==', '!=', '<', '>', '<=', '>='}:
|
| 585 |
-
# Check if current token is a binary operator
|
| 586 |
if token_type in {'*', '/', '%', '+', '-', '`', '&&', '||', '==', '!=', '<', '>', '<=', '>='}:
|
| 587 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}' operator - binary operators cannot start an expression. {self._format_expected(expected, non_terminal)}"
|
| 588 |
return f"SYNTAX error line {line} col {col} Missing value after '{prev_tok.value}' operator. {self._format_expected(expected, non_terminal)}"
|
| 589 |
|
| 590 |
-
# Don't report a misleading "missing parenthesis" message for a
|
| 591 |
-
# misplaced assignment operator.
|
| 592 |
if token_type in {'=', '+=', '-=', '*=', '/=', '%='}:
|
| 593 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 594 |
|
| 595 |
-
# Look back to find the keyword/statement that needs parentheses
|
| 596 |
if index > 0:
|
| 597 |
prev_index = index - 1
|
| 598 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -601,7 +472,6 @@ class LL1Parser:
|
|
| 601 |
if prev_index >= 0:
|
| 602 |
prev_tok = toks[prev_index]
|
| 603 |
|
| 604 |
-
# Check if previous token was an invalid keyword from other languages
|
| 605 |
common_keyword_mistakes = {
|
| 606 |
'function': 'pollinate',
|
| 607 |
'int': 'seed',
|
|
@@ -635,22 +505,15 @@ class LL1Parser:
|
|
| 635 |
correct_keyword = common_keyword_mistakes[prev_tok.value]
|
| 636 |
return f"SYNTAX error line {prev_tok.line} col {prev_tok.col} '{prev_tok.value}' is not a GAL keyword. Use '{correct_keyword}' instead."
|
| 637 |
|
| 638 |
-
# Keywords that require parentheses
|
| 639 |
keywords_needing_parens = {'spring', 'grow', 'cultivate', 'harvest', 'water', 'plant', 'tend', 'bud'}
|
| 640 |
if prev_tok.type in keywords_needing_parens:
|
| 641 |
return f"SYNTAX error line {line} col {col} Missing '(' after '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 642 |
|
| 643 |
-
# If previous token was an identifier, check context
|
| 644 |
if prev_tok.type == 'id':
|
| 645 |
# NOTE: GAL does support '**' (exponentiation) and '**=' (exponent-assign).
|
| 646 |
-
# The old heuristic here used to reject both — removed when those operators
|
| 647 |
-
# were enabled. A spaced '** =' is still invalid (lexer treats them as
|
| 648 |
-
# two tokens) — fall through to the generic compound-assign hint below.
|
| 649 |
|
| 650 |
-
# Check if current token is a binary operator followed by '=' (spaced compound assignment)
|
| 651 |
compound_op_bases = {'+', '-', '*', '/', '%'}
|
| 652 |
if token_type in compound_op_bases:
|
| 653 |
-
# Look ahead to see if next token is '='
|
| 654 |
next_index = index + 1
|
| 655 |
while next_index < len(toks) and toks[next_index].type in self.skip_token_types:
|
| 656 |
next_index += 1
|
|
@@ -659,16 +522,11 @@ class LL1Parser:
|
|
| 659 |
compound_op = f"{token_value}="
|
| 660 |
return f"SYNTAX error line {line} col {col} Unexpected operator '{token_value}' followed by '='. Expected: '{compound_op}' (compound assignment must be written without spaces). {self._format_expected(expected, non_terminal)}"
|
| 661 |
|
| 662 |
-
# Check if we're in an expression context by looking at expected tokens
|
| 663 |
-
# If operators are expected, we're in expression context
|
| 664 |
binary_operators = {'+', '-', '*', '/', '%', '==', '!=', '<', '>', '<=', '>=', '&&', '||'}
|
| 665 |
has_operators_expected = bool(binary_operators & expected)
|
| 666 |
|
| 667 |
if has_operators_expected:
|
| 668 |
-
# In expression context: likely missing operator between identifier and next value
|
| 669 |
-
# Special case: increment/decrement operators cannot be used in expressions
|
| 670 |
if token_type in {'++', '--'}:
|
| 671 |
-
# Look ahead to see if there's another + or - (e.g., +++ or ---)
|
| 672 |
next_index = index + 1
|
| 673 |
while next_index < len(toks) and toks[next_index].type in self.skip_token_types:
|
| 674 |
next_index += 1
|
|
@@ -680,27 +538,20 @@ class LL1Parser:
|
|
| 680 |
return f"SYNTAX error line {line} col {col} Unexpected token '{operator_seq}' operator sequence. {self._format_expected(expected, non_terminal)}"
|
| 681 |
|
| 682 |
return f"SYNTAX error line {line} col {col} Unexpected {token_type} operator. {self._format_expected(expected, non_terminal)}"
|
| 683 |
-
# Look ahead to see what the current token is
|
| 684 |
if token_type in {'intlit', 'dblit', 'stringlit', 'chrlit', 'id'}:
|
| 685 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_type}' after identifier '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 686 |
else:
|
| 687 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}' after identifier '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 688 |
else:
|
| 689 |
-
# In statement context: identifier used incorrectly as standalone statement
|
| 690 |
return f"SYNTAX error line {prev_tok.line} col {prev_tok.col} invalid statement: identifier '{prev_tok.value}' must be followed by assignment operator, unary operator (++/--), or function call syntax '()'. {self._format_expected(expected, non_terminal)}"
|
| 691 |
|
| 692 |
-
# Fallback message
|
| 693 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 694 |
|
| 695 |
-
# A declaration token reaching the executable list means that this
|
| 696 |
-
# block already contains executable code.
|
| 697 |
declaration_keywords = {'seed', 'tree', 'leaf', 'vine', 'branch', 'bundle', 'fertile'}
|
| 698 |
if token_type in declaration_keywords and non_terminal in {'<body_statement>', '<statement>', '<case_statements>'}:
|
| 699 |
return f"SYNTAX error line {line} col {col} Unexpected local declaration '{token_value}' after an executable statement. Local declarations must appear first in the block. {self._format_expected(expected, non_terminal)}"
|
| 700 |
|
| 701 |
-
# Check for missing closing braces
|
| 702 |
if '}' in expected and token_type in statement_starters:
|
| 703 |
-
# Special case: bud/wither appearing without a preceding spring
|
| 704 |
if token_type == 'bud':
|
| 705 |
return f"SYNTAX error line {line} col {col} 'bud' can only appear after a 'spring' statement. {self._format_expected(expected, non_terminal)}"
|
| 706 |
elif token_type == 'wither':
|
|
@@ -709,28 +560,21 @@ class LL1Parser:
|
|
| 709 |
return f"SYNTAX error line {line} col {col} Missing closing brace before '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 710 |
return f"SYNTAX error line {line} col {col} Missing closing brace before '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 711 |
|
| 712 |
-
# Check for ++ or -- operators used incorrectly in expressions
|
| 713 |
if token_type in {'++', '--'} and ')' in expected:
|
| 714 |
op_name = "increment" if token_value == "++" else "decrement"
|
| 715 |
return f"SYNTAX error line {line} col {col} Postfix {op_name} operator '{token_value}' not allowed in expression context. {self._format_expected(expected, non_terminal)}"
|
| 716 |
|
| 717 |
-
# Check for missing parameter type in function declaration
|
| 718 |
if param_type_tokens & expected and ')' in expected and token_type == 'id':
|
| 719 |
return f"SYNTAX error line {line} col {col}: Unexpected token '{token_value}'. Expected parameter type (seed, tree, leaf, vine, branch) or ')'"
|
| 720 |
|
| 721 |
-
# Check for missing closing parenthesis
|
| 722 |
if ')' in expected and token_type not in {')'}:
|
| 723 |
-
# Check for missing comma between function call arguments
|
| 724 |
if ',' in expected and token_type in {'intlit', 'dblit', 'stringlit', 'chrlit', 'id', 'sunshine', 'frost', '~', '!'}:
|
| 725 |
return f"SYNTAX error line {line} col {col}: Unexpected token '{token_value}'. Expected ',' between arguments or ')' to close function call"
|
| 726 |
-
# Check if '~' or '!' (unary operators) appear where a binary operator is expected
|
| 727 |
if token_type in {'~', '!'}:
|
| 728 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 729 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 730 |
|
| 731 |
-
# Check for literal/identifier after identifier in expression context (missing operator)
|
| 732 |
if token_type in {'intlit', 'dblit', 'stringlit', 'chrlit', 'id'}:
|
| 733 |
-
# Check if previous non-skipped token was also an identifier or literal
|
| 734 |
if index > 0:
|
| 735 |
prev_index = index - 1
|
| 736 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -738,9 +582,7 @@ class LL1Parser:
|
|
| 738 |
|
| 739 |
if prev_index >= 0:
|
| 740 |
prev_tok = toks[prev_index]
|
| 741 |
-
# If previous was identifier or literal, we're missing an operator
|
| 742 |
if prev_tok.type in {'id', 'intlit', 'dblit', 'stringlit', 'chrlit'}:
|
| 743 |
-
# Check if we're in expression context
|
| 744 |
expression_contexts = {'<expression>', '<factor>', '<term>', '<arithmetic>', '<logic_or>',
|
| 745 |
'<logic_and>', '<relational>', '<init_val>', '<param_list>', '<arg_list>',
|
| 746 |
'<term_tail>', '<arithmetic_tail>', '<relational_tail>', '<logic_and_tail>', '<logic_or_tail>'}
|
|
@@ -764,44 +606,24 @@ class LL1Parser:
|
|
| 764 |
|
| 765 |
return f"SYNTAX error line {line} col {col} Unexpected {curr_type_friendly} '{token_value}' after {prev_type_friendly} '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 766 |
|
| 767 |
-
# Check for variable used as array size — only constant integer literals allowed
|
| 768 |
if non_terminal == '<array_dim_opt>' and token_type == 'id':
|
| 769 |
return f"SYNTAX error line {line} col {col} Array size must be a constant integer literal, not a variable '{token_value}'. Expected: ']', dblit, intlit"
|
| 770 |
|
| 771 |
-
# Default message with helpful context
|
| 772 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 773 |
|
| 774 |
-
# ========================================================================
|
| 775 |
-
# parse() - LL(1) SYNTAX VALIDATION (no AST built)
|
| 776 |
-
# The classic table-driven LL(1) algorithm: maintain a parse stack
|
| 777 |
-
# initialized with the start symbol, then for each input token either
|
| 778 |
-
# 'match' a terminal (pop stack + advance input) or 'expand' a
|
| 779 |
-
# non-terminal (look up table[NT][lookahead], push the production
|
| 780 |
-
# backwards onto the stack). Returns (success, error_messages_list).
|
| 781 |
-
# Used by /api/parse for syntax-only checking.
|
| 782 |
-
# ========================================================================
|
| 783 |
def parse(self, tokens: Sequence[Any]) -> Tuple[bool, List[str]]:
|
| 784 |
-
"""Parse tokens according to the supplied CFG/PREDICT sets.
|
| 785 |
-
|
| 786 |
-
Returns:
|
| 787 |
-
(success, errors) - stops at the first error encountered.
|
| 788 |
-
"""
|
| 789 |
toks = [_as_tok(t) for t in tokens]
|
| 790 |
toks = [_TokView(self._normalize_token_type(t.type), t.value, t.line, t.col) for t in toks]
|
| 791 |
toks = self._ensure_eof(toks)
|
| 792 |
|
| 793 |
-
# Store tokens so _format_expected can inspect the source
|
| 794 |
self._current_tokens = toks
|
| 795 |
|
| 796 |
stack: List[str] = [self.end_marker, self.start_symbol]
|
| 797 |
index = 0
|
| 798 |
|
| 799 |
-
# Track variable declaration context for type checking
|
| 800 |
current_var_type: Optional[str] = None
|
| 801 |
expecting_value_for_type: Optional[str] = None
|
| 802 |
|
| 803 |
-
# Track whether 'reclaim' has been seen in the current block.
|
| 804 |
-
# Stack of booleans, one per nested { } block depth.
|
| 805 |
reclaim_seen_stack: List[bool] = []
|
| 806 |
|
| 807 |
def current_token() -> _TokView:
|
|
@@ -819,41 +641,31 @@ class LL1Parser:
|
|
| 819 |
token_value = tok.value
|
| 820 |
line = tok.line or 1
|
| 821 |
|
| 822 |
-
# Skip configured whitespace-like tokens unless grammar explicitly expects them.
|
| 823 |
if token_type in self.skip_token_types and top != token_type:
|
| 824 |
index += 1
|
| 825 |
continue
|
| 826 |
|
| 827 |
-
# Expand non-terminal
|
| 828 |
if top in self.parsing_table:
|
| 829 |
row = self.parsing_table[top]
|
| 830 |
if token_type in row:
|
| 831 |
production = row[token_type]
|
| 832 |
|
| 833 |
-
# Check for code after 'reclaim' in the current block
|
| 834 |
if top == '<statement>' and token_type != '}' and reclaim_seen_stack and reclaim_seen_stack[-1]:
|
| 835 |
return False, [f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}' after 'reclaim'. Expected: '}}'."]
|
| 836 |
|
| 837 |
-
# Check for empty blocks in conditionals/loops
|
| 838 |
-
# When <statement> expands to epsilon and next token is '}', check if inside conditional/loop
|
| 839 |
if top == '<statement>' and token_type == '}':
|
| 840 |
is_epsilon = (len(production) == 0 or (len(production) == 1 and production[0] in self.epsilon_symbols))
|
| 841 |
if is_epsilon:
|
| 842 |
-
# Look back to find the opening brace
|
| 843 |
lookback = index - 1
|
| 844 |
while lookback >= 0 and toks[lookback].type in self.skip_token_types:
|
| 845 |
lookback -= 1
|
| 846 |
|
| 847 |
-
# If we immediately see '{' after skipping whitespace, it's an empty block
|
| 848 |
if lookback >= 0 and toks[lookback].type == '{':
|
| 849 |
-
# Find what's before the brace
|
| 850 |
before_brace = lookback - 1
|
| 851 |
while before_brace >= 0 and toks[before_brace].type in self.skip_token_types:
|
| 852 |
before_brace -= 1
|
| 853 |
|
| 854 |
-
# Check if there's a ')' before the brace (conditional/loop pattern)
|
| 855 |
if before_brace >= 0 and toks[before_brace].type == ')':
|
| 856 |
-
# Find the matching '(' and the keyword before it
|
| 857 |
paren_depth = 1
|
| 858 |
paren_pos = before_brace - 1
|
| 859 |
while paren_pos >= 0 and paren_depth > 0:
|
|
@@ -863,7 +675,6 @@ class LL1Parser:
|
|
| 863 |
paren_depth -= 1
|
| 864 |
paren_pos -= 1
|
| 865 |
|
| 866 |
-
# Find keyword before the '('
|
| 867 |
if paren_pos >= 0:
|
| 868 |
kw_pos = paren_pos
|
| 869 |
while kw_pos >= 0 and toks[kw_pos].type in self.skip_token_types:
|
|
@@ -871,19 +682,16 @@ class LL1Parser:
|
|
| 871 |
|
| 872 |
if kw_pos >= 0:
|
| 873 |
kw = toks[kw_pos]
|
| 874 |
-
# These keywords require at least one statement in their blocks
|
| 875 |
conditional_keywords = {'spring', 'bud', 'wither', 'grow', 'cultivate', 'tend', 'harvest'}
|
| 876 |
if kw.type in conditional_keywords:
|
| 877 |
return False, [f"SYNTAX error line {line} col {tok.col} Empty block after '{kw.value}' statement - at least one statement required"]
|
| 878 |
|
| 879 |
-
# Check for wither/tend directly before '{' (no parentheses)
|
| 880 |
elif before_brace >= 0 and toks[before_brace].type in {'wither', 'tend'}:
|
| 881 |
kw = toks[before_brace]
|
| 882 |
return False, [f"SYNTAX error line {line} col {tok.col} Empty block after '{kw.value}' statement - at least one statement required"]
|
| 883 |
|
| 884 |
stack.pop()
|
| 885 |
|
| 886 |
-
# Push RHS (unless epsilon)
|
| 887 |
if not (
|
| 888 |
len(production) == 0
|
| 889 |
or (len(production) == 1 and production[0] in self.epsilon_symbols)
|
|
@@ -893,48 +701,36 @@ class LL1Parser:
|
|
| 893 |
|
| 894 |
expected = set(row.keys())
|
| 895 |
|
| 896 |
-
# Skip variety/soil outside harvest — let semantic analyzer handle the error
|
| 897 |
if token_type in {'variety', 'soil'} and token_type not in expected:
|
| 898 |
-
# Skip past the entire variety/soil block
|
| 899 |
while index < len(toks) and toks[index].type != ';':
|
| 900 |
if toks[index].type == 'prune':
|
| 901 |
-
index += 1
|
| 902 |
break
|
| 903 |
index += 1
|
| 904 |
if index < len(toks) and toks[index].type == ';':
|
| 905 |
-
index += 1
|
| 906 |
continue
|
| 907 |
|
| 908 |
-
# Enhanced error messages for common mistakes
|
| 909 |
error_msg = self._generate_helpful_error(top, token_type, token_value, line, tok.col, expected, index, toks)
|
| 910 |
return False, [error_msg]
|
| 911 |
|
| 912 |
-
# Match terminal
|
| 913 |
if top == token_type:
|
| 914 |
-
# Track variable type declarations
|
| 915 |
if token_type in {'seed', 'tree', 'leaf', 'branch', 'vine'}:
|
| 916 |
current_var_type = token_type
|
| 917 |
expecting_value_for_type = None
|
| 918 |
|
| 919 |
-
# When we see '=', prepare to check the value
|
| 920 |
elif token_type == '=' and current_var_type is not None:
|
| 921 |
expecting_value_for_type = current_var_type
|
| 922 |
|
| 923 |
-
# Check value type matches declared variable type
|
| 924 |
elif expecting_value_for_type is not None and token_type in {'intlit', 'dblit', 'stringlit', 'chrlit', 'sunshine', 'frost', 'id'}:
|
| 925 |
-
# Type checking mapping
|
| 926 |
-
# tree and branch reject identifiers; seed, leaf, vine allow them
|
| 927 |
-
# seed also accepts dbllit (implicit truncation like C: int x = 4.5 → 4)
|
| 928 |
-
# tree also accepts intlit (implicit promotion like C: double x = 5 → 5.0)
|
| 929 |
type_value_map = {
|
| 930 |
-
'seed': {'intlit', 'dblit'},
|
| 931 |
-
'tree': {'dblit', 'intlit'},
|
| 932 |
-
'leaf': {'chrlit'},
|
| 933 |
-
'branch': {'sunshine', 'frost'},
|
| 934 |
-
'vine': {'stringlit'}
|
| 935 |
}
|
| 936 |
|
| 937 |
-
# Allow identifiers for all types (semantic layer handles type checking)
|
| 938 |
if token_type == 'id':
|
| 939 |
expecting_value_for_type = None
|
| 940 |
stack.pop()
|
|
@@ -944,7 +740,6 @@ class LL1Parser:
|
|
| 944 |
expected_value_types = type_value_map.get(expecting_value_for_type, set())
|
| 945 |
|
| 946 |
if token_type not in expected_value_types:
|
| 947 |
-
# Generate helpful error message
|
| 948 |
type_names = {
|
| 949 |
'seed': 'integer (seed)',
|
| 950 |
'tree': 'double (tree)',
|
|
@@ -968,41 +763,30 @@ class LL1Parser:
|
|
| 968 |
error_msg = f"SEMANTIC error line {line} col {tok.col} Type mismatch: cannot assign {actual_type} value '{token_value}' to {declared_type} variable"
|
| 969 |
return False, [error_msg]
|
| 970 |
|
| 971 |
-
# Validate character literal length for leaf (char) type
|
| 972 |
if token_type == 'chrlit' and expecting_value_for_type == 'leaf':
|
| 973 |
-
# Extract the actual character content (remove surrounding quotes)
|
| 974 |
-
# Use slicing instead of strip to handle edge cases like '''
|
| 975 |
char_content = token_value[1:-1] if len(token_value) >= 2 else token_value
|
| 976 |
if len(char_content) == 0:
|
| 977 |
error_msg = f"SYNTAX error line {line} col {tok.col} Character literal cannot be empty. Expected a single character for leaf variable"
|
| 978 |
return False, [error_msg]
|
| 979 |
elif char_content.startswith('\\') and len(char_content) == 2:
|
| 980 |
-
# Valid escape sequence like \n, \t, \\, \'
|
| 981 |
pass
|
| 982 |
elif len(char_content) > 1:
|
| 983 |
error_msg = f"SYNTAX error line {line} col {tok.col} Character literal '{token_value}' contains {len(char_content)} characters. leaf variables can only hold a single character"
|
| 984 |
return False, [error_msg]
|
| 985 |
|
| 986 |
-
# tree and branch now allow expressions — semantic layer handles type checking
|
| 987 |
|
| 988 |
-
# Reset after checking
|
| 989 |
expecting_value_for_type = None
|
| 990 |
|
| 991 |
-
# Reset on semicolon (end of statement)
|
| 992 |
elif token_type == ';':
|
| 993 |
current_var_type = None
|
| 994 |
expecting_value_for_type = None
|
| 995 |
|
| 996 |
-
# Check for numeric literal in control flow condition
|
| 997 |
if top == 'intlit' or top == 'dblit':
|
| 998 |
-
# Look back to see if we're inside a control flow condition
|
| 999 |
-
# Check if there's a '(' before this and a control flow keyword before that
|
| 1000 |
lookback = index - 1
|
| 1001 |
while lookback >= 0 and toks[lookback].type in self.skip_token_types:
|
| 1002 |
lookback -= 1
|
| 1003 |
|
| 1004 |
if lookback >= 0 and toks[lookback].type == '(':
|
| 1005 |
-
# Found '(', now check for control flow keyword before it
|
| 1006 |
kw_pos = lookback - 1
|
| 1007 |
while kw_pos >= 0 and toks[kw_pos].type in self.skip_token_types:
|
| 1008 |
kw_pos -= 1
|
|
@@ -1011,7 +795,6 @@ class LL1Parser:
|
|
| 1011 |
kw = toks[kw_pos]
|
| 1012 |
condition_keywords = {'spring', 'grow', 'cultivate', 'tend', 'bud'}
|
| 1013 |
if kw.type in condition_keywords:
|
| 1014 |
-
# Check if next token after the literal is ')' (meaning no comparison operator)
|
| 1015 |
next_idx = index + 1
|
| 1016 |
while next_idx < len(toks) and toks[next_idx].type in self.skip_token_types:
|
| 1017 |
next_idx += 1
|
|
@@ -1019,9 +802,7 @@ class LL1Parser:
|
|
| 1019 |
if next_idx < len(toks) and toks[next_idx].type == ')':
|
| 1020 |
return False, [f"SYNTAX error line {line} col {tok.col} '{kw.value}' requires a boolean condition, not a numeric literal"]
|
| 1021 |
|
| 1022 |
-
# Check for logical operators used with non-branch operands
|
| 1023 |
if token_type in {'&&', '||'}:
|
| 1024 |
-
# Look back to find the previous operand
|
| 1025 |
prev_idx = index - 1
|
| 1026 |
while prev_idx >= 0 and toks[prev_idx].type in self.skip_token_types:
|
| 1027 |
prev_idx -= 1
|
|
@@ -1029,14 +810,12 @@ class LL1Parser:
|
|
| 1029 |
prev_tok = toks[prev_idx]
|
| 1030 |
non_branch_literals = {'intlit', 'dblit', 'stringlit', 'chrlit'}
|
| 1031 |
if prev_tok.type in non_branch_literals:
|
| 1032 |
-
# Check if this literal is the RHS of a comparison operator
|
| 1033 |
-
# e.g. age < 1 || ... -> '1' is part of 'age < 1' (boolean)
|
| 1034 |
cmp_idx = prev_idx - 1
|
| 1035 |
while cmp_idx >= 0 and toks[cmp_idx].type in self.skip_token_types:
|
| 1036 |
cmp_idx -= 1
|
| 1037 |
comparison_ops = {'<', '>', '<=', '>=', '==', '!='}
|
| 1038 |
if cmp_idx >= 0 and toks[cmp_idx].type in comparison_ops:
|
| 1039 |
-
pass
|
| 1040 |
else:
|
| 1041 |
type_names = {
|
| 1042 |
'intlit': 'integer literal',
|
|
@@ -1047,21 +826,17 @@ class LL1Parser:
|
|
| 1047 |
op_name = 'AND' if token_type == '&&' else 'OR'
|
| 1048 |
return False, [f"SYNTAX error line {line} col {tok.col} Logical operator '{token_type}' ({op_name}) requires branch operands, not {type_names[prev_tok.type]} '{prev_tok.value}'"]
|
| 1049 |
|
| 1050 |
-
# Check for non-branch literal after logical operators
|
| 1051 |
if token_type in {'intlit', 'dblit', 'stringlit', 'chrlit'}:
|
| 1052 |
prev_idx = index - 1
|
| 1053 |
while prev_idx >= 0 and toks[prev_idx].type in self.skip_token_types:
|
| 1054 |
prev_idx -= 1
|
| 1055 |
if prev_idx >= 0 and toks[prev_idx].type in {'&&', '||'}:
|
| 1056 |
-
# Check if next token after this literal is a comparison operator
|
| 1057 |
-
# e.g. ... || age > 120 -> 'age' after || is id not literal, but also
|
| 1058 |
-
# check if this literal is the LHS of a comparison (unlikely but safe)
|
| 1059 |
next_check = index + 1
|
| 1060 |
while next_check < len(toks) and toks[next_check].type in self.skip_token_types:
|
| 1061 |
next_check += 1
|
| 1062 |
comparison_ops = {'<', '>', '<=', '>=', '==', '!='}
|
| 1063 |
if next_check < len(toks) and toks[next_check].type in comparison_ops:
|
| 1064 |
-
pass
|
| 1065 |
else:
|
| 1066 |
type_names = {
|
| 1067 |
'intlit': 'integer literal',
|
|
@@ -1072,7 +847,6 @@ class LL1Parser:
|
|
| 1072 |
op_name = 'AND' if toks[prev_idx].type == '&&' else 'OR'
|
| 1073 |
return False, [f"SYNTAX error line {line} col {tok.col} Logical operator '{toks[prev_idx].type}' ({op_name}) requires branch operands, not {type_names[token_type]} '{token_value}'"]
|
| 1074 |
|
| 1075 |
-
# Track reclaim and block depth
|
| 1076 |
if token_type == '{':
|
| 1077 |
reclaim_seen_stack.append(False)
|
| 1078 |
elif token_type == '}':
|
|
@@ -1086,7 +860,6 @@ class LL1Parser:
|
|
| 1086 |
index += 1
|
| 1087 |
continue
|
| 1088 |
|
| 1089 |
-
# If the stack expects EOF but we still have skippable tokens, skip them.
|
| 1090 |
if top == self.end_marker and token_type in self.skip_token_types:
|
| 1091 |
index += 1
|
| 1092 |
continue
|
|
@@ -1094,15 +867,11 @@ class LL1Parser:
|
|
| 1094 |
expected = {top}
|
| 1095 |
shown_value = "end of file" if token_type == self.end_marker else token_value
|
| 1096 |
|
| 1097 |
-
# PRIORITY: Handle EOF hitting any expected terminal
|
| 1098 |
if token_type == self.end_marker:
|
| 1099 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected end of file. Expected: '{top}'. Missing closing '}}' or incomplete statement."
|
| 1100 |
return False, [error_msg]
|
| 1101 |
|
| 1102 |
-
# Enhanced error messages for specific missing tokens
|
| 1103 |
-
# Check for missing 'grow' after tend { ... }
|
| 1104 |
if top == 'grow' and token_type == '(':
|
| 1105 |
-
# Look back to see if we're inside a tend statement
|
| 1106 |
scan_idx = index - 1
|
| 1107 |
while scan_idx >= 0 and toks[scan_idx].type in self.skip_token_types:
|
| 1108 |
scan_idx -= 1
|
|
@@ -1123,7 +892,6 @@ class LL1Parser:
|
|
| 1123 |
return False, [error_msg]
|
| 1124 |
|
| 1125 |
if top == 'reclaim' and token_type == '}':
|
| 1126 |
-
# Check if we're in root() function vs regular function
|
| 1127 |
is_root = False
|
| 1128 |
for i in range(index - 1, -1, -1):
|
| 1129 |
if toks[i].type == 'root':
|
|
@@ -1147,8 +915,6 @@ class LL1Parser:
|
|
| 1147 |
error_msg = f"SYNTAX error line {line} col {tok.col} expected 'prune;' before closing '}}'. Each case must end with 'prune;'. {self._format_expected(expected)}"
|
| 1148 |
return False, [error_msg]
|
| 1149 |
elif top == '(' and token_type != '(':
|
| 1150 |
-
# Don't give a misleading "missing opening parenthesis" message
|
| 1151 |
-
# when an assignment operator is misplaced.
|
| 1152 |
if token_type in {'=', '+=', '-=', '*=', '/=', '%='}:
|
| 1153 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. {self._format_expected(expected, top)}"
|
| 1154 |
elif index > 0:
|
|
@@ -1159,9 +925,7 @@ class LL1Parser:
|
|
| 1159 |
if prev_index >= 0:
|
| 1160 |
prev_tok = toks[prev_index]
|
| 1161 |
|
| 1162 |
-
# Special case: tend requires condition after closing brace
|
| 1163 |
if prev_tok.type == '}':
|
| 1164 |
-
# Look back to find 'tend' keyword
|
| 1165 |
brace_depth = 1
|
| 1166 |
scan_idx = prev_index - 1
|
| 1167 |
while scan_idx >= 0 and brace_depth > 0:
|
|
@@ -1171,7 +935,6 @@ class LL1Parser:
|
|
| 1171 |
brace_depth -= 1
|
| 1172 |
scan_idx -= 1
|
| 1173 |
|
| 1174 |
-
# Now check if 'tend' is before the opening brace
|
| 1175 |
if scan_idx >= 0:
|
| 1176 |
kw_idx = scan_idx
|
| 1177 |
while kw_idx >= 0 and toks[kw_idx].type in self.skip_token_types:
|
|
@@ -1195,7 +958,6 @@ class LL1Parser:
|
|
| 1195 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. {self._format_expected(expected)}"
|
| 1196 |
return False, [error_msg]
|
| 1197 |
elif top == '{' and token_type != '{':
|
| 1198 |
-
# Check for extra closing parenthesis - common mistake like root())
|
| 1199 |
error_msg = None
|
| 1200 |
if token_type == ')' and index > 0:
|
| 1201 |
prev_index = index - 1
|
|
@@ -1203,8 +965,6 @@ class LL1Parser:
|
|
| 1203 |
prev_index -= 1
|
| 1204 |
|
| 1205 |
if prev_index >= 0 and toks[prev_index].type == ')':
|
| 1206 |
-
# Two consecutive `)` - likely extra closing paren
|
| 1207 |
-
# Find the keyword before the first `)`
|
| 1208 |
paren_index = prev_index - 1
|
| 1209 |
paren_count = 1
|
| 1210 |
while paren_index >= 0 and paren_count > 0:
|
|
@@ -1224,7 +984,6 @@ class LL1Parser:
|
|
| 1224 |
if kw_tok.type in {'root', 'pollinate'}:
|
| 1225 |
error_msg = f"SYNTAX error line {line} col {tok.col} Extra closing ')' after '{kw_tok.value}()'. Correct syntax: {kw_tok.value}(){{ ... }}. {self._format_expected(expected)}"
|
| 1226 |
|
| 1227 |
-
# Missing opening brace - look back to find the keyword
|
| 1228 |
if error_msg is None and index > 0:
|
| 1229 |
prev_index = index - 1
|
| 1230 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -1232,11 +991,9 @@ class LL1Parser:
|
|
| 1232 |
|
| 1233 |
if prev_index >= 0:
|
| 1234 |
prev_tok = toks[prev_index]
|
| 1235 |
-
# Keywords/tokens that require braces
|
| 1236 |
keywords_needing_braces = {'spring', 'wither', 'grow', 'cultivate', 'tend', 'harvest', 'bud', ')', 'pollinate', 'root'}
|
| 1237 |
if prev_tok.type in keywords_needing_braces:
|
| 1238 |
if prev_tok.type == ')':
|
| 1239 |
-
# Look further back to find the keyword before the closing paren
|
| 1240 |
paren_index = prev_index - 1
|
| 1241 |
paren_count = 1
|
| 1242 |
while paren_index >= 0 and paren_count > 0:
|
|
@@ -1246,7 +1003,6 @@ class LL1Parser:
|
|
| 1246 |
paren_count -= 1
|
| 1247 |
paren_index -= 1
|
| 1248 |
|
| 1249 |
-
# Now find the keyword before the opening paren
|
| 1250 |
if paren_index >= 0:
|
| 1251 |
kw_index = paren_index
|
| 1252 |
while kw_index >= 0 and toks[kw_index].type in self.skip_token_types:
|
|
@@ -1276,7 +1032,6 @@ class LL1Parser:
|
|
| 1276 |
elif top == ')' and token_type != ')':
|
| 1277 |
return False, [f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. {self._format_expected(expected)}"]
|
| 1278 |
elif top == ':' and token_type != ':':
|
| 1279 |
-
# Missing colon — look back to find 'variety' or 'soil' keyword for context
|
| 1280 |
context_keyword = None
|
| 1281 |
if index > 0:
|
| 1282 |
prev_index = index - 1
|
|
@@ -1284,11 +1039,9 @@ class LL1Parser:
|
|
| 1284 |
prev_index -= 1
|
| 1285 |
if prev_index >= 0:
|
| 1286 |
prev_tok = toks[prev_index]
|
| 1287 |
-
# For 'soil' the colon comes immediately after: soil :
|
| 1288 |
if prev_tok.type == 'soil':
|
| 1289 |
context_keyword = 'soil'
|
| 1290 |
else:
|
| 1291 |
-
# For 'variety expr :' — walk back past expression to find 'variety'
|
| 1292 |
scan = prev_index
|
| 1293 |
while scan >= 0:
|
| 1294 |
if toks[scan].type in self.skip_token_types:
|
|
@@ -1297,7 +1050,6 @@ class LL1Parser:
|
|
| 1297 |
if toks[scan].type == 'variety':
|
| 1298 |
context_keyword = 'variety'
|
| 1299 |
break
|
| 1300 |
-
# Skip over expression tokens (literals, ids, operators, parens)
|
| 1301 |
if toks[scan].type in {'id', 'intlit', 'dblit', 'stringlit', 'chrlit',
|
| 1302 |
'sunshine', 'frost', '+', '-', '*', '/', '%',
|
| 1303 |
'==', '!=', '<', '>', '<=', '>=', '&&', '||',
|
|
@@ -1310,8 +1062,6 @@ class LL1Parser:
|
|
| 1310 |
else:
|
| 1311 |
return False, [f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. {self._format_expected({':'})}"]
|
| 1312 |
elif top == ';' and token_type != ';':
|
| 1313 |
-
# Missing semicolon - but check if this might be an invalid keyword used as a function
|
| 1314 |
-
# Pattern: "if (condition) {" where parser saw "if()" as a function call
|
| 1315 |
common_keyword_mistakes = {
|
| 1316 |
'function': 'pollinate', 'int': 'seed', 'float': 'tree', 'double': 'tree',
|
| 1317 |
'char': 'leaf', 'bool': 'branch', 'boolean': 'branch', 'if': 'spring',
|
|
@@ -1323,15 +1073,12 @@ class LL1Parser:
|
|
| 1323 |
}
|
| 1324 |
|
| 1325 |
error_msg = None
|
| 1326 |
-
# Check if current token is '{' after what looks like a function call with invalid keyword
|
| 1327 |
if token_type == '{' and index > 0:
|
| 1328 |
-
# Look back for a closing parenthesis
|
| 1329 |
paren_idx = index - 1
|
| 1330 |
while paren_idx >= 0 and toks[paren_idx].type in self.skip_token_types:
|
| 1331 |
paren_idx -= 1
|
| 1332 |
|
| 1333 |
if paren_idx >= 0 and toks[paren_idx].type == ')':
|
| 1334 |
-
# Find the matching opening parenthesis
|
| 1335 |
paren_depth = 1
|
| 1336 |
paren_idx -= 1
|
| 1337 |
while paren_idx >= 0 and paren_depth > 0:
|
|
@@ -1341,7 +1088,6 @@ class LL1Parser:
|
|
| 1341 |
paren_depth -= 1
|
| 1342 |
paren_idx -= 1
|
| 1343 |
|
| 1344 |
-
# Check token before the opening parenthesis
|
| 1345 |
if paren_idx >= 0:
|
| 1346 |
id_idx = paren_idx
|
| 1347 |
while id_idx >= 0 and toks[id_idx].type in self.skip_token_types:
|
|
@@ -1352,12 +1098,10 @@ class LL1Parser:
|
|
| 1352 |
correct_keyword = common_keyword_mistakes[keyword_tok.value]
|
| 1353 |
error_msg = f"SYNTAX error line {keyword_tok.line} col {keyword_tok.col} '{keyword_tok.value}' is not a GAL keyword. Use '{correct_keyword}' instead."
|
| 1354 |
|
| 1355 |
-
# Check if CURRENT token (the one we're looking at) is an invalid keyword
|
| 1356 |
if error_msg is None and token_type == 'id' and token_value in common_keyword_mistakes:
|
| 1357 |
correct_keyword = common_keyword_mistakes[token_value]
|
| 1358 |
error_msg = f"SYNTAX error line {line} col {tok.col} '{token_value}' is not a GAL keyword. Use '{correct_keyword}' instead."
|
| 1359 |
|
| 1360 |
-
# Check for chained increment/decrement operators (e.g., a++++)
|
| 1361 |
if error_msg is None and token_type in {'++', '--'} and index > 0:
|
| 1362 |
prev_index = index - 1
|
| 1363 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -1368,7 +1112,6 @@ class LL1Parser:
|
|
| 1368 |
if prev_tok.type in {'++', '--'}:
|
| 1369 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}' after '{prev_tok.value}'. Increment/decrement operators cannot be chained. {self._format_expected(expected)}"
|
| 1370 |
|
| 1371 |
-
# Check for binary operator after increment/decrement (e.g., a--+2)
|
| 1372 |
binary_operators = {'+', '-', '*', '/', '%', '==', '!=', '<', '>', '<=', '>=', '&&', '||'}
|
| 1373 |
if error_msg is None and token_type in binary_operators and index > 0:
|
| 1374 |
prev_index = index - 1
|
|
@@ -1380,7 +1123,6 @@ class LL1Parser:
|
|
| 1380 |
if prev_tok.type in {'++', '--'}:
|
| 1381 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected binary operator '{token_value}' after unary operator '{prev_tok.value}'. Increment/decrement must be standalone statements. {self._format_expected(expected)}"
|
| 1382 |
|
| 1383 |
-
# Otherwise, it's a missing semicolon - report on the line where it should be added
|
| 1384 |
if error_msg is None:
|
| 1385 |
if index > 0:
|
| 1386 |
prev_index = index - 1
|
|
@@ -1389,9 +1131,8 @@ class LL1Parser:
|
|
| 1389 |
|
| 1390 |
if prev_index >= 0:
|
| 1391 |
prev_tok = toks[prev_index]
|
| 1392 |
-
# Report error on the line of the previous token (where semicolon should be)
|
| 1393 |
error_msg = f"SYNTAX error line {prev_tok.line} col {prev_tok.col + len(str(prev_tok.value))} Unexpected token '{token_value}'. Expected ';'. {self._format_expected(expected)}"
|
| 1394 |
-
line = prev_tok.line
|
| 1395 |
else:
|
| 1396 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. Expected ';'. {self._format_expected(expected)}"
|
| 1397 |
else:
|
|
@@ -1399,8 +1140,6 @@ class LL1Parser:
|
|
| 1399 |
|
| 1400 |
return False, [error_msg]
|
| 1401 |
else:
|
| 1402 |
-
# Check if missing return type in function declaration
|
| 1403 |
-
# e.g. pollinate add(...) — 'add' consumed as return type, parser expects id for function name, sees '('
|
| 1404 |
if top == 'id' and token_type == '(' and index > 0:
|
| 1405 |
prev_index = index - 1
|
| 1406 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
@@ -1413,13 +1152,10 @@ class LL1Parser:
|
|
| 1413 |
func_name = toks[prev_index].value
|
| 1414 |
return False, [f"SYNTAX error line {toks[kw_index].line} col {toks[kw_index].col} Missing return type after 'pollinate'. '{func_name}' was parsed as the return type, not the function name. Expected: 'branch', 'empty', 'leaf', 'seed', 'tree', 'vine'"]
|
| 1415 |
|
| 1416 |
-
# Check if we're expecting a parameter name after an identifier was consumed as a bundle type
|
| 1417 |
-
# e.g. pollinate seed add(seed a, b) — 'b' consumed as bundle type, parser expects 'id' for param name, but sees ')'
|
| 1418 |
if top == 'id' and token_type == ')' and index > 0:
|
| 1419 |
prev_index = index - 1
|
| 1420 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
| 1421 |
prev_index -= 1
|
| 1422 |
-
# If prev token is an id that follows a ',' it's a missing param type
|
| 1423 |
if prev_index >= 0 and toks[prev_index].type == 'id':
|
| 1424 |
comma_index = prev_index - 1
|
| 1425 |
while comma_index >= 0 and toks[comma_index].type in self.skip_token_types:
|
|
@@ -1429,13 +1165,11 @@ class LL1Parser:
|
|
| 1429 |
param_expected = {'seed', 'tree', 'leaf', 'vine', 'branch'}
|
| 1430 |
return False, [f"SYNTAX error line {toks[prev_index].line} col {toks[prev_index].col} Missing type for parameter '{param_name}'. Each parameter requires a type. {self._format_expected(param_expected)}"]
|
| 1431 |
|
| 1432 |
-
# Use the helper method for better error messages
|
| 1433 |
error_msg = self._generate_helpful_error(
|
| 1434 |
top, token_type, token_value, line, tok.col, expected, index, toks
|
| 1435 |
)
|
| 1436 |
return False, [error_msg]
|
| 1437 |
|
| 1438 |
-
# Consume trailing skippables then require EOF
|
| 1439 |
while index < len(toks) and toks[index].type in self.skip_token_types:
|
| 1440 |
index += 1
|
| 1441 |
if index < len(toks) and toks[index].type != self.end_marker:
|
|
@@ -1446,25 +1180,7 @@ class LL1Parser:
|
|
| 1446 |
|
| 1447 |
return True, []
|
| 1448 |
|
| 1449 |
-
# ========================================================================
|
| 1450 |
-
# parse_and_build() - SYNTAX + AST CONSTRUCTION (full parser entry)
|
| 1451 |
-
# First runs the LL(1) check; if it passes, delegates to
|
| 1452 |
-
# GALsemantic.build_ast which walks the same token stream and
|
| 1453 |
-
# constructs the AST. Some semantic errors (undeclared variables,
|
| 1454 |
-
# duplicate functions) are caught during AST construction and reported
|
| 1455 |
-
# with error_stage='semantic' so the IDE can label them correctly.
|
| 1456 |
-
# Used by /api/semantic, /api/icg, /api/run, and Socket.IO 'run_code'.
|
| 1457 |
-
# ========================================================================
|
| 1458 |
def parse_and_build(self, tokens: Sequence[Any]):
|
| 1459 |
-
"""Validate syntax with LL(1) grammar, then build AST.
|
| 1460 |
-
|
| 1461 |
-
This is the main parser entry point for the full pipeline:
|
| 1462 |
-
tokens → LL(1) syntax check → AST construction
|
| 1463 |
-
|
| 1464 |
-
Returns:
|
| 1465 |
-
dict with keys: success, errors, ast, symbol_table
|
| 1466 |
-
"""
|
| 1467 |
-
# Step 1: LL(1) syntax validation
|
| 1468 |
syntax_ok, syntax_errors = self.parse(tokens)
|
| 1469 |
if not syntax_ok:
|
| 1470 |
return {
|
|
@@ -1474,12 +1190,10 @@ class LL1Parser:
|
|
| 1474 |
"symbol_table": {},
|
| 1475 |
}
|
| 1476 |
|
| 1477 |
-
# Step 2: Build AST from the validated token stream
|
| 1478 |
try:
|
| 1479 |
filtered = [t for t in tokens if getattr(t, 'type', '') != '\n']
|
| 1480 |
ast = _build_ast(filtered)
|
| 1481 |
|
| 1482 |
-
# Serialize the symbol table produced during AST construction
|
| 1483 |
st = {
|
| 1484 |
"variables": [
|
| 1485 |
{
|
|
@@ -1523,4 +1237,3 @@ class LL1Parser:
|
|
| 1523 |
}
|
| 1524 |
|
| 1525 |
|
| 1526 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Set, Tuple
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
from .builder import (
|
| 7 |
build_ast as _build_ast,
|
| 8 |
symbol_table as _builder_st,
|
|
|
|
| 10 |
from semantic.errors import SemanticError as _SemanticError
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
@dataclass(frozen=True)
|
| 14 |
class _TokView:
|
|
|
|
| 15 |
type: str
|
| 16 |
value: str
|
| 17 |
line: int
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
def _as_tok(token: Any) -> _TokView:
|
|
|
|
| 22 |
if isinstance(token, Mapping):
|
|
|
|
| 23 |
return _TokView(
|
| 24 |
type=str(token.get("type", "")),
|
| 25 |
value=str(token.get("value", "")),
|
| 26 |
line=int(token.get("line", 0) or 0),
|
| 27 |
col=int(token.get("col", 0) or 0),
|
| 28 |
)
|
|
|
|
| 29 |
return _TokView(
|
| 30 |
type=str(getattr(token, "type", "")),
|
| 31 |
value=str(getattr(token, "value", "")),
|
|
|
|
| 34 |
)
|
| 35 |
|
| 36 |
|
|
|
|
|
|
|
|
|
|
| 37 |
class LL1Parser:
|
| 38 |
def __init__(
|
| 39 |
self,
|
|
|
|
| 51 |
self.predict_sets = predict_sets
|
| 52 |
self.first_sets = first_sets
|
| 53 |
|
|
|
|
| 54 |
self.epsilon_symbols: Set[str] = set(epsilon_symbols)
|
| 55 |
self.start_symbol = start_symbol
|
| 56 |
self.end_marker = end_marker
|
| 57 |
|
|
|
|
|
|
|
| 58 |
self.skip_token_types: Set[str] = set(skip_token_types or {"\n"})
|
| 59 |
self.token_type_alias = token_type_alias or {
|
|
|
|
|
|
|
| 60 |
'idf': 'id',
|
| 61 |
'dbllit': 'dblit',
|
| 62 |
}
|
| 63 |
|
| 64 |
self.parsing_table: Dict[str, Dict[str, List[str]]] = self.construct_parsing_table()
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
def construct_parsing_table(self) -> Dict[str, Dict[str, List[str]]]:
|
|
|
|
| 67 |
table: Dict[str, Dict[str, List[str]]] = {}
|
| 68 |
|
| 69 |
for non_terminal, productions in self.cfg.items():
|
|
|
|
| 72 |
key = (non_terminal, tuple(production))
|
| 73 |
terms = self.predict_sets.get(key, set())
|
| 74 |
for terminal in terms:
|
|
|
|
|
|
|
| 75 |
if terminal in row and row[terminal] != production:
|
| 76 |
raise ValueError(
|
| 77 |
f"LL(1) conflict at {non_terminal} with lookahead {terminal}: "
|
|
|
|
| 82 |
return table
|
| 83 |
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def _normalize_token_type(self, token_type: str) -> str:
|
|
|
|
| 86 |
return self.token_type_alias.get(token_type, token_type)
|
| 87 |
|
| 88 |
def _ensure_eof(self, toks: List[_TokView]) -> List[_TokView]:
|
|
|
|
| 94 |
toks = toks + [_TokView(self.end_marker, self.end_marker, last_line, last_col)]
|
| 95 |
return toks
|
| 96 |
|
|
|
|
| 97 |
_TERMINAL_DISPLAY: Dict[str, str] = {
|
|
|
|
| 98 |
'id': 'id', 'intlit': 'intlit', 'dblit': 'dblit',
|
| 99 |
'stringlit': 'stringlit', 'chrlit': 'chrlit',
|
| 100 |
'sunshine': "'sunshine'", 'frost': "'frost'",
|
|
|
|
| 101 |
'seed': "'seed'", 'tree': "'tree'",
|
| 102 |
'leaf': "'leaf'", 'branch': "'branch'",
|
| 103 |
'vine': "'vine'",
|
|
|
|
| 104 |
'bundle': "'bundle'", 'fertile': "'fertile'",
|
| 105 |
'pollinate': "'pollinate'", 'root': "'root'",
|
| 106 |
'reclaim': "'reclaim'", 'spring': "'spring'",
|
|
|
|
| 111 |
'prune': "'prune'", 'skip': "'skip'",
|
| 112 |
'water': "'water'", 'plant': "'plant'",
|
| 113 |
'empty': "'empty'",
|
|
|
|
| 114 |
'EOF': 'end of file',
|
| 115 |
}
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
def _format_expected(self, expected: Set[str], non_terminal: Optional[str] = None) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
symbols = {'(', ')', '{', '}', ';', ',', '=', '+', '-', '*', '/', '%',
|
| 119 |
'++', '--', '==', '!=', '<', '>', '<=', '>=', '&&', '||',
|
| 120 |
'!', '~', '+=', '-=', '*=', '/=', '%=', '.', '[', ']', ':', '`'}
|
|
|
|
| 121 |
has_reclaim = any(tk.type == 'reclaim' for tk in getattr(self, '_current_tokens', []))
|
| 122 |
|
| 123 |
parts: List[str] = []
|
| 124 |
for t in sorted(expected):
|
| 125 |
if t == 'reclaim' and has_reclaim:
|
| 126 |
+
continue
|
| 127 |
if t in self._TERMINAL_DISPLAY:
|
| 128 |
parts.append(self._TERMINAL_DISPLAY[t])
|
| 129 |
elif t in symbols:
|
| 130 |
parts.append(f"'{t}'")
|
| 131 |
elif t.startswith('<') and t.endswith('>'):
|
| 132 |
+
continue
|
| 133 |
else:
|
| 134 |
parts.append(f"'{t}'")
|
| 135 |
if not parts:
|
| 136 |
return 'nothing'
|
| 137 |
return f"Expected: {', '.join(parts)}"
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
def _generate_helpful_error(
|
| 140 |
self,
|
| 141 |
non_terminal: str,
|
|
|
|
| 147 |
index: int,
|
| 148 |
toks: List[_TokView]
|
| 149 |
) -> str:
|
|
|
|
| 150 |
|
|
|
|
| 151 |
param_type_tokens = {'seed', 'tree', 'leaf', 'vine', 'branch'}
|
| 152 |
|
|
|
|
| 153 |
if token_type == self.end_marker or token_value == '':
|
| 154 |
if '}' in expected:
|
| 155 |
return f"SYNTAX error line {line} col {col} Unexpected end of file. Missing closing '}}'. {self._format_expected(expected, non_terminal)}"
|
| 156 |
return f"SYNTAX error line {line} col {col} Unexpected end of file. {self._format_expected(expected, non_terminal)}"
|
| 157 |
|
|
|
|
| 158 |
if token_type == '=' and index > 0:
|
| 159 |
prev_index = index - 1
|
| 160 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 163 |
if prev_index >= 0 and toks[prev_index].type == '==':
|
| 164 |
return f"SYNTAX error line {line} col {col} Invalid operator '==='. Use '==' for equality comparison. {self._format_expected(expected, non_terminal)}"
|
| 165 |
|
|
|
|
| 166 |
if token_type == '&' and index > 0:
|
| 167 |
prev_index = index - 1
|
| 168 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 170 |
|
| 171 |
if prev_index >= 0 and toks[prev_index].type == '&&':
|
| 172 |
return f"SYNTAX error line {line} col {col} Invalid operator '&&&'. Use '&&' for logical AND. {self._format_expected(expected, non_terminal)}"
|
|
|
|
| 173 |
return f"SYNTAX error line {line} col {col} Invalid operator '&'. Use '&&' for logical AND. {self._format_expected(expected, non_terminal)}"
|
| 174 |
|
|
|
|
| 175 |
if token_type == '|' and index > 0:
|
| 176 |
prev_index = index - 1
|
| 177 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 179 |
|
| 180 |
if prev_index >= 0 and toks[prev_index].type == '||':
|
| 181 |
return f"SYNTAX error line {line} col {col} Invalid operator '|||'. Use '||' for logical OR. {self._format_expected(expected, non_terminal)}"
|
|
|
|
| 182 |
return f"SYNTAX error line {line} col {col} Invalid operator '|'. Use '||' for logical OR. {self._format_expected(expected, non_terminal)}"
|
| 183 |
|
|
|
|
| 184 |
if token_type == 'chrlit' and token_value and not token_value.endswith("'"):
|
| 185 |
return f"SYNTAX error line {line} col {col} Missing closing single quote in character literal. {self._format_expected(expected, non_terminal)}"
|
| 186 |
|
| 187 |
if token_type == 'stringlit' and token_value and not token_value.endswith('"'):
|
| 188 |
return f"SYNTAX error line {line} col {col} Missing closing double quote in string literal. {self._format_expected(expected, non_terminal)}"
|
| 189 |
|
|
|
|
| 190 |
if non_terminal == '<reclaim_value>' and token_type == '}':
|
| 191 |
return f"SYNTAX error line {line} col {col} Missing ';' after 'reclaim'. Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 192 |
|
|
|
|
| 193 |
if token_type == ')' and ')' not in expected:
|
|
|
|
| 194 |
if index > 0:
|
| 195 |
prev_index = index - 1
|
| 196 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 202 |
if prev_tok.type in binary_operators:
|
| 203 |
return f"SYNTAX error line {line} col {col} Unexpected token ')' after binary operator '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 204 |
|
|
|
|
| 205 |
if prev_tok.type == ',' and param_type_tokens & expected:
|
| 206 |
return f"SYNTAX error line {line} col {col}: Unexpected token ')'. Expected parameter type (seed, tree, leaf, vine, branch) after ','"
|
| 207 |
|
|
|
|
| 208 |
if prev_tok.type == '(':
|
| 209 |
kw_index = prev_index - 1
|
| 210 |
while kw_index >= 0 and toks[kw_index].type in self.skip_token_types:
|
|
|
|
| 228 |
desc = keyword_descriptions[kw_tok.type]
|
| 229 |
return f"SYNTAX error line {kw_tok.line} col {kw_tok.col} '{kw_tok.value}' is a reserved keyword ({desc}) and cannot be used as a function name."
|
| 230 |
|
|
|
|
| 231 |
return f"SYNTAX error line {line} col {col} Unexpected token ')' - no matching '(' found in expression. {self._format_expected(expected, non_terminal)}"
|
| 232 |
|
|
|
|
| 233 |
assignment_operators = {'+=', '-=', '*=', '/=', '%='}
|
| 234 |
if token_type in assignment_operators:
|
| 235 |
return f"SYNTAX error line {line} col {col} Assignment operator '{token_value}' must follow a modifiable assignment target. {self._format_expected(expected, non_terminal)}"
|
| 236 |
|
|
|
|
| 237 |
if token_type == '=':
|
| 238 |
if index > 0:
|
| 239 |
prev_index = index - 1
|
|
|
|
| 244 |
prev_tok = toks[prev_index]
|
| 245 |
compound_op_bases = {'+', '-', '*', '/', '%'}
|
| 246 |
if prev_tok.type in compound_op_bases:
|
|
|
|
| 247 |
prev_prev_index = prev_index - 1
|
| 248 |
while prev_prev_index >= 0 and toks[prev_prev_index].type in self.skip_token_types:
|
| 249 |
prev_prev_index -= 1
|
|
|
|
| 253 |
compound_op = f"{prev_tok.value}="
|
| 254 |
return f"SYNTAX error line {line} col {col} Unexpected token '=' after operator '{prev_tok.value}'. Did you mean '{id_tok.value} {compound_op}' (compound assignment with no space)? {self._format_expected(expected, non_terminal)}"
|
| 255 |
|
|
|
|
| 256 |
if token_type == '{' and non_terminal in {'<program>', '<global_declaration>'}:
|
| 257 |
if index == 0 or (index <= 2 and all(toks[i].type in self.skip_token_types for i in range(index))):
|
| 258 |
return f"SYNTAX error line {line} col {col} 'root' function declaration is missing opening '('. {self._format_expected(expected, non_terminal)}"
|
| 259 |
|
|
|
|
| 260 |
if index + 1 < len(toks):
|
| 261 |
next_tok = toks[index + 1]
|
| 262 |
if next_tok.type == 'chrlit' and next_tok.value and not next_tok.value.endswith("'"):
|
|
|
|
| 265 |
if next_tok.type == 'stringlit' and next_tok.value and not next_tok.value.endswith('"'):
|
| 266 |
return f"SYNTAX error line {next_tok.line} col {next_tok.col} Missing closing double quote in string literal. {self._format_expected(expected, non_terminal)}"
|
| 267 |
|
|
|
|
| 268 |
if token_type == ';' and non_terminal == '<global_declaration>':
|
|
|
|
| 269 |
if index > 0:
|
| 270 |
prev_index = index - 1
|
| 271 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
| 272 |
prev_index -= 1
|
| 273 |
|
| 274 |
if prev_index >= 0 and toks[prev_index].type == '}':
|
|
|
|
| 275 |
bundle_index = prev_index - 1
|
| 276 |
while bundle_index >= 0 and toks[bundle_index].type in self.skip_token_types:
|
| 277 |
bundle_index -= 1
|
| 278 |
|
|
|
|
| 279 |
found_bundle = False
|
| 280 |
for i in range(bundle_index, max(0, bundle_index - 20) - 1, -1):
|
| 281 |
if toks[i].type == 'bundle':
|
|
|
|
| 286 |
expected_str = self._format_expected(expected, non_terminal)
|
| 287 |
return f"SYNTAX error line {line} col {col} Unexpected token ';' after bundle definition closing '}}'. ';' is not in {expected_str}. Remove the trailing ';'"
|
| 288 |
|
|
|
|
| 289 |
common_keyword_mistakes = {
|
| 290 |
'function': 'pollinate',
|
| 291 |
'int': 'seed',
|
|
|
|
| 319 |
correct_keyword = common_keyword_mistakes[token_value]
|
| 320 |
return f"SYNTAX error line {line} col {col} '{token_value}' is not a GAL keyword. Use '{correct_keyword}' instead."
|
| 321 |
|
|
|
|
| 322 |
if token_type == '{' and (non_terminal == '<bundle_or_var>' or non_terminal == '<bundle_mem_dec>'):
|
|
|
|
|
|
|
| 323 |
return f"SYNTAX error line {line} col {col} Bundle definitions must be at global scope (outside all functions). Move this bundle definition before 'root()'. {self._format_expected(expected, non_terminal)}"
|
| 324 |
|
|
|
|
| 325 |
statement_starters = {
|
| 326 |
'reclaim', 'spring', 'wither', 'bud', 'grow', 'cultivate', 'tend',
|
| 327 |
'harvest', 'prune', 'skip', 'water', 'plant', 'seed', 'leaf',
|
| 328 |
'branch', 'tree', 'vine', 'bundle', 'fertile', 'pollinate', 'root', 'id'
|
| 329 |
}
|
| 330 |
|
|
|
|
| 331 |
if ':' in expected and token_type in statement_starters:
|
|
|
|
| 332 |
context_keyword = None
|
| 333 |
if index > 0:
|
| 334 |
scan = index - 1
|
|
|
|
| 342 |
if toks[scan].type == 'soil':
|
| 343 |
context_keyword = 'soil'
|
| 344 |
break
|
|
|
|
| 345 |
if toks[scan].type in {'id', 'intlit', 'dblit', 'stringlit', 'chrlit',
|
| 346 |
'sunshine', 'frost', '+', '-', '*', '/', '%',
|
| 347 |
'==', '!=', '<', '>', '<=', '>=', '&&', '||',
|
|
|
|
| 354 |
|
| 355 |
if ';' in expected:
|
| 356 |
if token_type in statement_starters or token_type == 'id':
|
|
|
|
|
|
|
| 357 |
if index > 0:
|
| 358 |
prev_index = index - 1
|
| 359 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 362 |
if prev_index >= 0:
|
| 363 |
prev_tok = toks[prev_index]
|
| 364 |
|
|
|
|
| 365 |
if prev_tok.type == '=':
|
| 366 |
prev_line = prev_tok.line
|
| 367 |
prev_col = prev_tok.col
|
| 368 |
return f"SYNTAX error line {prev_line} col {prev_col} Missing value after '=' operator. {self._format_expected(expected, non_terminal)}"
|
| 369 |
|
|
|
|
| 370 |
if prev_tok.type == 'chrlit' and prev_tok.value and not prev_tok.value.endswith("'"):
|
| 371 |
prev_line = prev_tok.line
|
| 372 |
prev_col = prev_tok.col
|
| 373 |
return f"SYNTAX error line {prev_line} col {prev_col} Missing closing single quote in character literal. {self._format_expected(expected, non_terminal)}"
|
| 374 |
|
|
|
|
| 375 |
if prev_tok.type == 'stringlit' and prev_tok.value and not prev_tok.value.endswith('"'):
|
| 376 |
prev_line = prev_tok.line
|
| 377 |
prev_col = prev_tok.col
|
|
|
|
| 380 |
prev_line = prev_tok.line
|
| 381 |
prev_col = prev_tok.col + len(str(prev_tok.value))
|
| 382 |
expected_str = self._format_expected(expected, non_terminal)
|
| 383 |
+
if prev_line != line:
|
| 384 |
return (f"SYNTAX error line {prev_line} col {prev_col} Unexpected token '{token_value}' after '{prev_tok.value}'. {expected_str}")
|
| 385 |
else:
|
| 386 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {expected_str}"
|
| 387 |
elif token_type == '}':
|
|
|
|
| 388 |
if index > 0:
|
| 389 |
prev_index = index - 1
|
| 390 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 393 |
if prev_index >= 0:
|
| 394 |
prev_tok = toks[prev_index]
|
| 395 |
|
|
|
|
| 396 |
if prev_tok.type == 'reclaim':
|
| 397 |
return f"SYNTAX error line {prev_tok.line} col {prev_tok.col + len('reclaim')} Missing ';' after 'reclaim'. {self._format_expected(expected, non_terminal)}"
|
| 398 |
|
|
|
|
| 399 |
if prev_tok.type == 'chrlit' and prev_tok.value and not prev_tok.value.endswith("'"):
|
| 400 |
prev_line = prev_tok.line
|
| 401 |
prev_col = prev_tok.col
|
| 402 |
return f"SYNTAX error line {prev_line} col {prev_col} Missing closing single quote in character literal. {self._format_expected(expected, non_terminal)}"
|
| 403 |
|
|
|
|
| 404 |
if prev_tok.type == 'stringlit' and prev_tok.value and not prev_tok.value.endswith('"'):
|
| 405 |
prev_line = prev_tok.line
|
| 406 |
prev_col = prev_tok.col
|
|
|
|
| 414 |
else:
|
| 415 |
return f"SYNTAX error line {line} col {col} Unexpected token '}}'. {expected_str}"
|
| 416 |
|
|
|
|
| 417 |
if 'reclaim' in expected and token_type == '}':
|
| 418 |
return f"SYNTAX error line {line} col {col} expected 'reclaim;' before '}}'. All functions, including root(), must end with 'reclaim;'. {self._format_expected(expected, non_terminal)}"
|
| 419 |
|
|
|
|
| 420 |
if 'prune' in expected and token_type in {'variety', 'soil', '}'}:
|
| 421 |
if token_type == 'variety':
|
| 422 |
return f"SYNTAX error line {line} col {col} expected 'prune;' before next 'variety'. Each case in 'harvest' must end with 'prune;'. {self._format_expected(expected, non_terminal)}"
|
| 423 |
elif token_type == 'soil':
|
| 424 |
return f"SYNTAX error line {line} col {col} expected 'prune;' before 'soil'. Each case must end with 'prune;'. {self._format_expected(expected, non_terminal)}"
|
| 425 |
+
else:
|
| 426 |
return f"SYNTAX error line {line} col {col} expected 'prune;' before closing '}}'. Each case must end with 'prune;'. {self._format_expected(expected, non_terminal)}"
|
| 427 |
|
|
|
|
| 428 |
if token_type in {'-', '+'} and non_terminal in {'<expression>', '<factor>', '<term>', '<arithmetic>', '<logic_or>', '<logic_and>', '<relational>', '<init_val>'}:
|
|
|
|
| 429 |
if index > 0:
|
| 430 |
prev_index = index - 1
|
| 431 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 433 |
|
| 434 |
if prev_index >= 0:
|
| 435 |
prev_tok = toks[prev_index]
|
|
|
|
| 436 |
if prev_tok.type in {'=', '+=', '-=', '*=', '/=', '%=', '(', ','}:
|
| 437 |
if token_value == '-':
|
| 438 |
return f"SYNTAX error line {line} col {col} Unary '-' not supported. Use '~' for negative numbers (e.g., '~5') or '(0 - value)' for negation. {self._format_expected(expected, non_terminal)}"
|
| 439 |
else:
|
| 440 |
return f"SYNTAX error line {line} col {col} Unary '+' operator not supported. Use parentheses for expressions like '(0 + value)'. {self._format_expected(expected, non_terminal)}"
|
| 441 |
|
|
|
|
| 442 |
if token_type in {'*', '/', '%', '+', '-', '`', '&&', '||', '==', '!=', '<', '>', '<=', '>='}:
|
| 443 |
if index > 0:
|
| 444 |
prev_index = index - 1
|
|
|
|
| 448 |
if prev_index >= 0 and toks[prev_index].type == '(':
|
| 449 |
return f"SYNTAX error line {line} col {col} Unexpected binary operator '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 450 |
|
|
|
|
| 451 |
if '(' in expected and token_type != '(':
|
|
|
|
|
|
|
| 452 |
if index > 0 and token_type not in {'-', '+'}:
|
| 453 |
prev_index = index - 1
|
| 454 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 456 |
|
| 457 |
if prev_index >= 0:
|
| 458 |
prev_tok = toks[prev_index]
|
|
|
|
| 459 |
if prev_tok.type in {'+', '-', '*', '/', '%', '`', '=', '+=', '-=', '*=', '/=', '%=', '&&', '||', '==', '!=', '<', '>', '<=', '>='}:
|
|
|
|
| 460 |
if token_type in {'*', '/', '%', '+', '-', '`', '&&', '||', '==', '!=', '<', '>', '<=', '>='}:
|
| 461 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}' operator - binary operators cannot start an expression. {self._format_expected(expected, non_terminal)}"
|
| 462 |
return f"SYNTAX error line {line} col {col} Missing value after '{prev_tok.value}' operator. {self._format_expected(expected, non_terminal)}"
|
| 463 |
|
|
|
|
|
|
|
| 464 |
if token_type in {'=', '+=', '-=', '*=', '/=', '%='}:
|
| 465 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 466 |
|
|
|
|
| 467 |
if index > 0:
|
| 468 |
prev_index = index - 1
|
| 469 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 472 |
if prev_index >= 0:
|
| 473 |
prev_tok = toks[prev_index]
|
| 474 |
|
|
|
|
| 475 |
common_keyword_mistakes = {
|
| 476 |
'function': 'pollinate',
|
| 477 |
'int': 'seed',
|
|
|
|
| 505 |
correct_keyword = common_keyword_mistakes[prev_tok.value]
|
| 506 |
return f"SYNTAX error line {prev_tok.line} col {prev_tok.col} '{prev_tok.value}' is not a GAL keyword. Use '{correct_keyword}' instead."
|
| 507 |
|
|
|
|
| 508 |
keywords_needing_parens = {'spring', 'grow', 'cultivate', 'harvest', 'water', 'plant', 'tend', 'bud'}
|
| 509 |
if prev_tok.type in keywords_needing_parens:
|
| 510 |
return f"SYNTAX error line {line} col {col} Missing '(' after '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 511 |
|
|
|
|
| 512 |
if prev_tok.type == 'id':
|
| 513 |
# NOTE: GAL does support '**' (exponentiation) and '**=' (exponent-assign).
|
|
|
|
|
|
|
|
|
|
| 514 |
|
|
|
|
| 515 |
compound_op_bases = {'+', '-', '*', '/', '%'}
|
| 516 |
if token_type in compound_op_bases:
|
|
|
|
| 517 |
next_index = index + 1
|
| 518 |
while next_index < len(toks) and toks[next_index].type in self.skip_token_types:
|
| 519 |
next_index += 1
|
|
|
|
| 522 |
compound_op = f"{token_value}="
|
| 523 |
return f"SYNTAX error line {line} col {col} Unexpected operator '{token_value}' followed by '='. Expected: '{compound_op}' (compound assignment must be written without spaces). {self._format_expected(expected, non_terminal)}"
|
| 524 |
|
|
|
|
|
|
|
| 525 |
binary_operators = {'+', '-', '*', '/', '%', '==', '!=', '<', '>', '<=', '>=', '&&', '||'}
|
| 526 |
has_operators_expected = bool(binary_operators & expected)
|
| 527 |
|
| 528 |
if has_operators_expected:
|
|
|
|
|
|
|
| 529 |
if token_type in {'++', '--'}:
|
|
|
|
| 530 |
next_index = index + 1
|
| 531 |
while next_index < len(toks) and toks[next_index].type in self.skip_token_types:
|
| 532 |
next_index += 1
|
|
|
|
| 538 |
return f"SYNTAX error line {line} col {col} Unexpected token '{operator_seq}' operator sequence. {self._format_expected(expected, non_terminal)}"
|
| 539 |
|
| 540 |
return f"SYNTAX error line {line} col {col} Unexpected {token_type} operator. {self._format_expected(expected, non_terminal)}"
|
|
|
|
| 541 |
if token_type in {'intlit', 'dblit', 'stringlit', 'chrlit', 'id'}:
|
| 542 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_type}' after identifier '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 543 |
else:
|
| 544 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}' after identifier '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 545 |
else:
|
|
|
|
| 546 |
return f"SYNTAX error line {prev_tok.line} col {prev_tok.col} invalid statement: identifier '{prev_tok.value}' must be followed by assignment operator, unary operator (++/--), or function call syntax '()'. {self._format_expected(expected, non_terminal)}"
|
| 547 |
|
|
|
|
| 548 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 549 |
|
|
|
|
|
|
|
| 550 |
declaration_keywords = {'seed', 'tree', 'leaf', 'vine', 'branch', 'bundle', 'fertile'}
|
| 551 |
if token_type in declaration_keywords and non_terminal in {'<body_statement>', '<statement>', '<case_statements>'}:
|
| 552 |
return f"SYNTAX error line {line} col {col} Unexpected local declaration '{token_value}' after an executable statement. Local declarations must appear first in the block. {self._format_expected(expected, non_terminal)}"
|
| 553 |
|
|
|
|
| 554 |
if '}' in expected and token_type in statement_starters:
|
|
|
|
| 555 |
if token_type == 'bud':
|
| 556 |
return f"SYNTAX error line {line} col {col} 'bud' can only appear after a 'spring' statement. {self._format_expected(expected, non_terminal)}"
|
| 557 |
elif token_type == 'wither':
|
|
|
|
| 560 |
return f"SYNTAX error line {line} col {col} Missing closing brace before '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 561 |
return f"SYNTAX error line {line} col {col} Missing closing brace before '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 562 |
|
|
|
|
| 563 |
if token_type in {'++', '--'} and ')' in expected:
|
| 564 |
op_name = "increment" if token_value == "++" else "decrement"
|
| 565 |
return f"SYNTAX error line {line} col {col} Postfix {op_name} operator '{token_value}' not allowed in expression context. {self._format_expected(expected, non_terminal)}"
|
| 566 |
|
|
|
|
| 567 |
if param_type_tokens & expected and ')' in expected and token_type == 'id':
|
| 568 |
return f"SYNTAX error line {line} col {col}: Unexpected token '{token_value}'. Expected parameter type (seed, tree, leaf, vine, branch) or ')'"
|
| 569 |
|
|
|
|
| 570 |
if ')' in expected and token_type not in {')'}:
|
|
|
|
| 571 |
if ',' in expected and token_type in {'intlit', 'dblit', 'stringlit', 'chrlit', 'id', 'sunshine', 'frost', '~', '!'}:
|
| 572 |
return f"SYNTAX error line {line} col {col}: Unexpected token '{token_value}'. Expected ',' between arguments or ')' to close function call"
|
|
|
|
| 573 |
if token_type in {'~', '!'}:
|
| 574 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 575 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 576 |
|
|
|
|
| 577 |
if token_type in {'intlit', 'dblit', 'stringlit', 'chrlit', 'id'}:
|
|
|
|
| 578 |
if index > 0:
|
| 579 |
prev_index = index - 1
|
| 580 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 582 |
|
| 583 |
if prev_index >= 0:
|
| 584 |
prev_tok = toks[prev_index]
|
|
|
|
| 585 |
if prev_tok.type in {'id', 'intlit', 'dblit', 'stringlit', 'chrlit'}:
|
|
|
|
| 586 |
expression_contexts = {'<expression>', '<factor>', '<term>', '<arithmetic>', '<logic_or>',
|
| 587 |
'<logic_and>', '<relational>', '<init_val>', '<param_list>', '<arg_list>',
|
| 588 |
'<term_tail>', '<arithmetic_tail>', '<relational_tail>', '<logic_and_tail>', '<logic_or_tail>'}
|
|
|
|
| 606 |
|
| 607 |
return f"SYNTAX error line {line} col {col} Unexpected {curr_type_friendly} '{token_value}' after {prev_type_friendly} '{prev_tok.value}'. {self._format_expected(expected, non_terminal)}"
|
| 608 |
|
|
|
|
| 609 |
if non_terminal == '<array_dim_opt>' and token_type == 'id':
|
| 610 |
return f"SYNTAX error line {line} col {col} Array size must be a constant integer literal, not a variable '{token_value}'. Expected: ']', dblit, intlit"
|
| 611 |
|
|
|
|
| 612 |
return f"SYNTAX error line {line} col {col} Unexpected token '{token_value}'. {self._format_expected(expected, non_terminal)}"
|
| 613 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
def parse(self, tokens: Sequence[Any]) -> Tuple[bool, List[str]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
toks = [_as_tok(t) for t in tokens]
|
| 616 |
toks = [_TokView(self._normalize_token_type(t.type), t.value, t.line, t.col) for t in toks]
|
| 617 |
toks = self._ensure_eof(toks)
|
| 618 |
|
|
|
|
| 619 |
self._current_tokens = toks
|
| 620 |
|
| 621 |
stack: List[str] = [self.end_marker, self.start_symbol]
|
| 622 |
index = 0
|
| 623 |
|
|
|
|
| 624 |
current_var_type: Optional[str] = None
|
| 625 |
expecting_value_for_type: Optional[str] = None
|
| 626 |
|
|
|
|
|
|
|
| 627 |
reclaim_seen_stack: List[bool] = []
|
| 628 |
|
| 629 |
def current_token() -> _TokView:
|
|
|
|
| 641 |
token_value = tok.value
|
| 642 |
line = tok.line or 1
|
| 643 |
|
|
|
|
| 644 |
if token_type in self.skip_token_types and top != token_type:
|
| 645 |
index += 1
|
| 646 |
continue
|
| 647 |
|
|
|
|
| 648 |
if top in self.parsing_table:
|
| 649 |
row = self.parsing_table[top]
|
| 650 |
if token_type in row:
|
| 651 |
production = row[token_type]
|
| 652 |
|
|
|
|
| 653 |
if top == '<statement>' and token_type != '}' and reclaim_seen_stack and reclaim_seen_stack[-1]:
|
| 654 |
return False, [f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}' after 'reclaim'. Expected: '}}'."]
|
| 655 |
|
|
|
|
|
|
|
| 656 |
if top == '<statement>' and token_type == '}':
|
| 657 |
is_epsilon = (len(production) == 0 or (len(production) == 1 and production[0] in self.epsilon_symbols))
|
| 658 |
if is_epsilon:
|
|
|
|
| 659 |
lookback = index - 1
|
| 660 |
while lookback >= 0 and toks[lookback].type in self.skip_token_types:
|
| 661 |
lookback -= 1
|
| 662 |
|
|
|
|
| 663 |
if lookback >= 0 and toks[lookback].type == '{':
|
|
|
|
| 664 |
before_brace = lookback - 1
|
| 665 |
while before_brace >= 0 and toks[before_brace].type in self.skip_token_types:
|
| 666 |
before_brace -= 1
|
| 667 |
|
|
|
|
| 668 |
if before_brace >= 0 and toks[before_brace].type == ')':
|
|
|
|
| 669 |
paren_depth = 1
|
| 670 |
paren_pos = before_brace - 1
|
| 671 |
while paren_pos >= 0 and paren_depth > 0:
|
|
|
|
| 675 |
paren_depth -= 1
|
| 676 |
paren_pos -= 1
|
| 677 |
|
|
|
|
| 678 |
if paren_pos >= 0:
|
| 679 |
kw_pos = paren_pos
|
| 680 |
while kw_pos >= 0 and toks[kw_pos].type in self.skip_token_types:
|
|
|
|
| 682 |
|
| 683 |
if kw_pos >= 0:
|
| 684 |
kw = toks[kw_pos]
|
|
|
|
| 685 |
conditional_keywords = {'spring', 'bud', 'wither', 'grow', 'cultivate', 'tend', 'harvest'}
|
| 686 |
if kw.type in conditional_keywords:
|
| 687 |
return False, [f"SYNTAX error line {line} col {tok.col} Empty block after '{kw.value}' statement - at least one statement required"]
|
| 688 |
|
|
|
|
| 689 |
elif before_brace >= 0 and toks[before_brace].type in {'wither', 'tend'}:
|
| 690 |
kw = toks[before_brace]
|
| 691 |
return False, [f"SYNTAX error line {line} col {tok.col} Empty block after '{kw.value}' statement - at least one statement required"]
|
| 692 |
|
| 693 |
stack.pop()
|
| 694 |
|
|
|
|
| 695 |
if not (
|
| 696 |
len(production) == 0
|
| 697 |
or (len(production) == 1 and production[0] in self.epsilon_symbols)
|
|
|
|
| 701 |
|
| 702 |
expected = set(row.keys())
|
| 703 |
|
|
|
|
| 704 |
if token_type in {'variety', 'soil'} and token_type not in expected:
|
|
|
|
| 705 |
while index < len(toks) and toks[index].type != ';':
|
| 706 |
if toks[index].type == 'prune':
|
| 707 |
+
index += 1
|
| 708 |
break
|
| 709 |
index += 1
|
| 710 |
if index < len(toks) and toks[index].type == ';':
|
| 711 |
+
index += 1
|
| 712 |
continue
|
| 713 |
|
|
|
|
| 714 |
error_msg = self._generate_helpful_error(top, token_type, token_value, line, tok.col, expected, index, toks)
|
| 715 |
return False, [error_msg]
|
| 716 |
|
|
|
|
| 717 |
if top == token_type:
|
|
|
|
| 718 |
if token_type in {'seed', 'tree', 'leaf', 'branch', 'vine'}:
|
| 719 |
current_var_type = token_type
|
| 720 |
expecting_value_for_type = None
|
| 721 |
|
|
|
|
| 722 |
elif token_type == '=' and current_var_type is not None:
|
| 723 |
expecting_value_for_type = current_var_type
|
| 724 |
|
|
|
|
| 725 |
elif expecting_value_for_type is not None and token_type in {'intlit', 'dblit', 'stringlit', 'chrlit', 'sunshine', 'frost', 'id'}:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
type_value_map = {
|
| 727 |
+
'seed': {'intlit', 'dblit'},
|
| 728 |
+
'tree': {'dblit', 'intlit'},
|
| 729 |
+
'leaf': {'chrlit'},
|
| 730 |
+
'branch': {'sunshine', 'frost'},
|
| 731 |
+
'vine': {'stringlit'}
|
| 732 |
}
|
| 733 |
|
|
|
|
| 734 |
if token_type == 'id':
|
| 735 |
expecting_value_for_type = None
|
| 736 |
stack.pop()
|
|
|
|
| 740 |
expected_value_types = type_value_map.get(expecting_value_for_type, set())
|
| 741 |
|
| 742 |
if token_type not in expected_value_types:
|
|
|
|
| 743 |
type_names = {
|
| 744 |
'seed': 'integer (seed)',
|
| 745 |
'tree': 'double (tree)',
|
|
|
|
| 763 |
error_msg = f"SEMANTIC error line {line} col {tok.col} Type mismatch: cannot assign {actual_type} value '{token_value}' to {declared_type} variable"
|
| 764 |
return False, [error_msg]
|
| 765 |
|
|
|
|
| 766 |
if token_type == 'chrlit' and expecting_value_for_type == 'leaf':
|
|
|
|
|
|
|
| 767 |
char_content = token_value[1:-1] if len(token_value) >= 2 else token_value
|
| 768 |
if len(char_content) == 0:
|
| 769 |
error_msg = f"SYNTAX error line {line} col {tok.col} Character literal cannot be empty. Expected a single character for leaf variable"
|
| 770 |
return False, [error_msg]
|
| 771 |
elif char_content.startswith('\\') and len(char_content) == 2:
|
|
|
|
| 772 |
pass
|
| 773 |
elif len(char_content) > 1:
|
| 774 |
error_msg = f"SYNTAX error line {line} col {tok.col} Character literal '{token_value}' contains {len(char_content)} characters. leaf variables can only hold a single character"
|
| 775 |
return False, [error_msg]
|
| 776 |
|
|
|
|
| 777 |
|
|
|
|
| 778 |
expecting_value_for_type = None
|
| 779 |
|
|
|
|
| 780 |
elif token_type == ';':
|
| 781 |
current_var_type = None
|
| 782 |
expecting_value_for_type = None
|
| 783 |
|
|
|
|
| 784 |
if top == 'intlit' or top == 'dblit':
|
|
|
|
|
|
|
| 785 |
lookback = index - 1
|
| 786 |
while lookback >= 0 and toks[lookback].type in self.skip_token_types:
|
| 787 |
lookback -= 1
|
| 788 |
|
| 789 |
if lookback >= 0 and toks[lookback].type == '(':
|
|
|
|
| 790 |
kw_pos = lookback - 1
|
| 791 |
while kw_pos >= 0 and toks[kw_pos].type in self.skip_token_types:
|
| 792 |
kw_pos -= 1
|
|
|
|
| 795 |
kw = toks[kw_pos]
|
| 796 |
condition_keywords = {'spring', 'grow', 'cultivate', 'tend', 'bud'}
|
| 797 |
if kw.type in condition_keywords:
|
|
|
|
| 798 |
next_idx = index + 1
|
| 799 |
while next_idx < len(toks) and toks[next_idx].type in self.skip_token_types:
|
| 800 |
next_idx += 1
|
|
|
|
| 802 |
if next_idx < len(toks) and toks[next_idx].type == ')':
|
| 803 |
return False, [f"SYNTAX error line {line} col {tok.col} '{kw.value}' requires a boolean condition, not a numeric literal"]
|
| 804 |
|
|
|
|
| 805 |
if token_type in {'&&', '||'}:
|
|
|
|
| 806 |
prev_idx = index - 1
|
| 807 |
while prev_idx >= 0 and toks[prev_idx].type in self.skip_token_types:
|
| 808 |
prev_idx -= 1
|
|
|
|
| 810 |
prev_tok = toks[prev_idx]
|
| 811 |
non_branch_literals = {'intlit', 'dblit', 'stringlit', 'chrlit'}
|
| 812 |
if prev_tok.type in non_branch_literals:
|
|
|
|
|
|
|
| 813 |
cmp_idx = prev_idx - 1
|
| 814 |
while cmp_idx >= 0 and toks[cmp_idx].type in self.skip_token_types:
|
| 815 |
cmp_idx -= 1
|
| 816 |
comparison_ops = {'<', '>', '<=', '>=', '==', '!='}
|
| 817 |
if cmp_idx >= 0 and toks[cmp_idx].type in comparison_ops:
|
| 818 |
+
pass
|
| 819 |
else:
|
| 820 |
type_names = {
|
| 821 |
'intlit': 'integer literal',
|
|
|
|
| 826 |
op_name = 'AND' if token_type == '&&' else 'OR'
|
| 827 |
return False, [f"SYNTAX error line {line} col {tok.col} Logical operator '{token_type}' ({op_name}) requires branch operands, not {type_names[prev_tok.type]} '{prev_tok.value}'"]
|
| 828 |
|
|
|
|
| 829 |
if token_type in {'intlit', 'dblit', 'stringlit', 'chrlit'}:
|
| 830 |
prev_idx = index - 1
|
| 831 |
while prev_idx >= 0 and toks[prev_idx].type in self.skip_token_types:
|
| 832 |
prev_idx -= 1
|
| 833 |
if prev_idx >= 0 and toks[prev_idx].type in {'&&', '||'}:
|
|
|
|
|
|
|
|
|
|
| 834 |
next_check = index + 1
|
| 835 |
while next_check < len(toks) and toks[next_check].type in self.skip_token_types:
|
| 836 |
next_check += 1
|
| 837 |
comparison_ops = {'<', '>', '<=', '>=', '==', '!='}
|
| 838 |
if next_check < len(toks) and toks[next_check].type in comparison_ops:
|
| 839 |
+
pass
|
| 840 |
else:
|
| 841 |
type_names = {
|
| 842 |
'intlit': 'integer literal',
|
|
|
|
| 847 |
op_name = 'AND' if toks[prev_idx].type == '&&' else 'OR'
|
| 848 |
return False, [f"SYNTAX error line {line} col {tok.col} Logical operator '{toks[prev_idx].type}' ({op_name}) requires branch operands, not {type_names[token_type]} '{token_value}'"]
|
| 849 |
|
|
|
|
| 850 |
if token_type == '{':
|
| 851 |
reclaim_seen_stack.append(False)
|
| 852 |
elif token_type == '}':
|
|
|
|
| 860 |
index += 1
|
| 861 |
continue
|
| 862 |
|
|
|
|
| 863 |
if top == self.end_marker and token_type in self.skip_token_types:
|
| 864 |
index += 1
|
| 865 |
continue
|
|
|
|
| 867 |
expected = {top}
|
| 868 |
shown_value = "end of file" if token_type == self.end_marker else token_value
|
| 869 |
|
|
|
|
| 870 |
if token_type == self.end_marker:
|
| 871 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected end of file. Expected: '{top}'. Missing closing '}}' or incomplete statement."
|
| 872 |
return False, [error_msg]
|
| 873 |
|
|
|
|
|
|
|
| 874 |
if top == 'grow' and token_type == '(':
|
|
|
|
| 875 |
scan_idx = index - 1
|
| 876 |
while scan_idx >= 0 and toks[scan_idx].type in self.skip_token_types:
|
| 877 |
scan_idx -= 1
|
|
|
|
| 892 |
return False, [error_msg]
|
| 893 |
|
| 894 |
if top == 'reclaim' and token_type == '}':
|
|
|
|
| 895 |
is_root = False
|
| 896 |
for i in range(index - 1, -1, -1):
|
| 897 |
if toks[i].type == 'root':
|
|
|
|
| 915 |
error_msg = f"SYNTAX error line {line} col {tok.col} expected 'prune;' before closing '}}'. Each case must end with 'prune;'. {self._format_expected(expected)}"
|
| 916 |
return False, [error_msg]
|
| 917 |
elif top == '(' and token_type != '(':
|
|
|
|
|
|
|
| 918 |
if token_type in {'=', '+=', '-=', '*=', '/=', '%='}:
|
| 919 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. {self._format_expected(expected, top)}"
|
| 920 |
elif index > 0:
|
|
|
|
| 925 |
if prev_index >= 0:
|
| 926 |
prev_tok = toks[prev_index]
|
| 927 |
|
|
|
|
| 928 |
if prev_tok.type == '}':
|
|
|
|
| 929 |
brace_depth = 1
|
| 930 |
scan_idx = prev_index - 1
|
| 931 |
while scan_idx >= 0 and brace_depth > 0:
|
|
|
|
| 935 |
brace_depth -= 1
|
| 936 |
scan_idx -= 1
|
| 937 |
|
|
|
|
| 938 |
if scan_idx >= 0:
|
| 939 |
kw_idx = scan_idx
|
| 940 |
while kw_idx >= 0 and toks[kw_idx].type in self.skip_token_types:
|
|
|
|
| 958 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. {self._format_expected(expected)}"
|
| 959 |
return False, [error_msg]
|
| 960 |
elif top == '{' and token_type != '{':
|
|
|
|
| 961 |
error_msg = None
|
| 962 |
if token_type == ')' and index > 0:
|
| 963 |
prev_index = index - 1
|
|
|
|
| 965 |
prev_index -= 1
|
| 966 |
|
| 967 |
if prev_index >= 0 and toks[prev_index].type == ')':
|
|
|
|
|
|
|
| 968 |
paren_index = prev_index - 1
|
| 969 |
paren_count = 1
|
| 970 |
while paren_index >= 0 and paren_count > 0:
|
|
|
|
| 984 |
if kw_tok.type in {'root', 'pollinate'}:
|
| 985 |
error_msg = f"SYNTAX error line {line} col {tok.col} Extra closing ')' after '{kw_tok.value}()'. Correct syntax: {kw_tok.value}(){{ ... }}. {self._format_expected(expected)}"
|
| 986 |
|
|
|
|
| 987 |
if error_msg is None and index > 0:
|
| 988 |
prev_index = index - 1
|
| 989 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 991 |
|
| 992 |
if prev_index >= 0:
|
| 993 |
prev_tok = toks[prev_index]
|
|
|
|
| 994 |
keywords_needing_braces = {'spring', 'wither', 'grow', 'cultivate', 'tend', 'harvest', 'bud', ')', 'pollinate', 'root'}
|
| 995 |
if prev_tok.type in keywords_needing_braces:
|
| 996 |
if prev_tok.type == ')':
|
|
|
|
| 997 |
paren_index = prev_index - 1
|
| 998 |
paren_count = 1
|
| 999 |
while paren_index >= 0 and paren_count > 0:
|
|
|
|
| 1003 |
paren_count -= 1
|
| 1004 |
paren_index -= 1
|
| 1005 |
|
|
|
|
| 1006 |
if paren_index >= 0:
|
| 1007 |
kw_index = paren_index
|
| 1008 |
while kw_index >= 0 and toks[kw_index].type in self.skip_token_types:
|
|
|
|
| 1032 |
elif top == ')' and token_type != ')':
|
| 1033 |
return False, [f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. {self._format_expected(expected)}"]
|
| 1034 |
elif top == ':' and token_type != ':':
|
|
|
|
| 1035 |
context_keyword = None
|
| 1036 |
if index > 0:
|
| 1037 |
prev_index = index - 1
|
|
|
|
| 1039 |
prev_index -= 1
|
| 1040 |
if prev_index >= 0:
|
| 1041 |
prev_tok = toks[prev_index]
|
|
|
|
| 1042 |
if prev_tok.type == 'soil':
|
| 1043 |
context_keyword = 'soil'
|
| 1044 |
else:
|
|
|
|
| 1045 |
scan = prev_index
|
| 1046 |
while scan >= 0:
|
| 1047 |
if toks[scan].type in self.skip_token_types:
|
|
|
|
| 1050 |
if toks[scan].type == 'variety':
|
| 1051 |
context_keyword = 'variety'
|
| 1052 |
break
|
|
|
|
| 1053 |
if toks[scan].type in {'id', 'intlit', 'dblit', 'stringlit', 'chrlit',
|
| 1054 |
'sunshine', 'frost', '+', '-', '*', '/', '%',
|
| 1055 |
'==', '!=', '<', '>', '<=', '>=', '&&', '||',
|
|
|
|
| 1062 |
else:
|
| 1063 |
return False, [f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. {self._format_expected({':'})}"]
|
| 1064 |
elif top == ';' and token_type != ';':
|
|
|
|
|
|
|
| 1065 |
common_keyword_mistakes = {
|
| 1066 |
'function': 'pollinate', 'int': 'seed', 'float': 'tree', 'double': 'tree',
|
| 1067 |
'char': 'leaf', 'bool': 'branch', 'boolean': 'branch', 'if': 'spring',
|
|
|
|
| 1073 |
}
|
| 1074 |
|
| 1075 |
error_msg = None
|
|
|
|
| 1076 |
if token_type == '{' and index > 0:
|
|
|
|
| 1077 |
paren_idx = index - 1
|
| 1078 |
while paren_idx >= 0 and toks[paren_idx].type in self.skip_token_types:
|
| 1079 |
paren_idx -= 1
|
| 1080 |
|
| 1081 |
if paren_idx >= 0 and toks[paren_idx].type == ')':
|
|
|
|
| 1082 |
paren_depth = 1
|
| 1083 |
paren_idx -= 1
|
| 1084 |
while paren_idx >= 0 and paren_depth > 0:
|
|
|
|
| 1088 |
paren_depth -= 1
|
| 1089 |
paren_idx -= 1
|
| 1090 |
|
|
|
|
| 1091 |
if paren_idx >= 0:
|
| 1092 |
id_idx = paren_idx
|
| 1093 |
while id_idx >= 0 and toks[id_idx].type in self.skip_token_types:
|
|
|
|
| 1098 |
correct_keyword = common_keyword_mistakes[keyword_tok.value]
|
| 1099 |
error_msg = f"SYNTAX error line {keyword_tok.line} col {keyword_tok.col} '{keyword_tok.value}' is not a GAL keyword. Use '{correct_keyword}' instead."
|
| 1100 |
|
|
|
|
| 1101 |
if error_msg is None and token_type == 'id' and token_value in common_keyword_mistakes:
|
| 1102 |
correct_keyword = common_keyword_mistakes[token_value]
|
| 1103 |
error_msg = f"SYNTAX error line {line} col {tok.col} '{token_value}' is not a GAL keyword. Use '{correct_keyword}' instead."
|
| 1104 |
|
|
|
|
| 1105 |
if error_msg is None and token_type in {'++', '--'} and index > 0:
|
| 1106 |
prev_index = index - 1
|
| 1107 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 1112 |
if prev_tok.type in {'++', '--'}:
|
| 1113 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}' after '{prev_tok.value}'. Increment/decrement operators cannot be chained. {self._format_expected(expected)}"
|
| 1114 |
|
|
|
|
| 1115 |
binary_operators = {'+', '-', '*', '/', '%', '==', '!=', '<', '>', '<=', '>=', '&&', '||'}
|
| 1116 |
if error_msg is None and token_type in binary_operators and index > 0:
|
| 1117 |
prev_index = index - 1
|
|
|
|
| 1123 |
if prev_tok.type in {'++', '--'}:
|
| 1124 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected binary operator '{token_value}' after unary operator '{prev_tok.value}'. Increment/decrement must be standalone statements. {self._format_expected(expected)}"
|
| 1125 |
|
|
|
|
| 1126 |
if error_msg is None:
|
| 1127 |
if index > 0:
|
| 1128 |
prev_index = index - 1
|
|
|
|
| 1131 |
|
| 1132 |
if prev_index >= 0:
|
| 1133 |
prev_tok = toks[prev_index]
|
|
|
|
| 1134 |
error_msg = f"SYNTAX error line {prev_tok.line} col {prev_tok.col + len(str(prev_tok.value))} Unexpected token '{token_value}'. Expected ';'. {self._format_expected(expected)}"
|
| 1135 |
+
line = prev_tok.line
|
| 1136 |
else:
|
| 1137 |
error_msg = f"SYNTAX error line {line} col {tok.col} Unexpected token '{token_value}'. Expected ';'. {self._format_expected(expected)}"
|
| 1138 |
else:
|
|
|
|
| 1140 |
|
| 1141 |
return False, [error_msg]
|
| 1142 |
else:
|
|
|
|
|
|
|
| 1143 |
if top == 'id' and token_type == '(' and index > 0:
|
| 1144 |
prev_index = index - 1
|
| 1145 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
|
|
|
| 1152 |
func_name = toks[prev_index].value
|
| 1153 |
return False, [f"SYNTAX error line {toks[kw_index].line} col {toks[kw_index].col} Missing return type after 'pollinate'. '{func_name}' was parsed as the return type, not the function name. Expected: 'branch', 'empty', 'leaf', 'seed', 'tree', 'vine'"]
|
| 1154 |
|
|
|
|
|
|
|
| 1155 |
if top == 'id' and token_type == ')' and index > 0:
|
| 1156 |
prev_index = index - 1
|
| 1157 |
while prev_index >= 0 and toks[prev_index].type in self.skip_token_types:
|
| 1158 |
prev_index -= 1
|
|
|
|
| 1159 |
if prev_index >= 0 and toks[prev_index].type == 'id':
|
| 1160 |
comma_index = prev_index - 1
|
| 1161 |
while comma_index >= 0 and toks[comma_index].type in self.skip_token_types:
|
|
|
|
| 1165 |
param_expected = {'seed', 'tree', 'leaf', 'vine', 'branch'}
|
| 1166 |
return False, [f"SYNTAX error line {toks[prev_index].line} col {toks[prev_index].col} Missing type for parameter '{param_name}'. Each parameter requires a type. {self._format_expected(param_expected)}"]
|
| 1167 |
|
|
|
|
| 1168 |
error_msg = self._generate_helpful_error(
|
| 1169 |
top, token_type, token_value, line, tok.col, expected, index, toks
|
| 1170 |
)
|
| 1171 |
return False, [error_msg]
|
| 1172 |
|
|
|
|
| 1173 |
while index < len(toks) and toks[index].type in self.skip_token_types:
|
| 1174 |
index += 1
|
| 1175 |
if index < len(toks) and toks[index].type != self.end_marker:
|
|
|
|
| 1180 |
|
| 1181 |
return True, []
|
| 1182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
def parse_and_build(self, tokens: Sequence[Any]):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1184 |
syntax_ok, syntax_errors = self.parse(tokens)
|
| 1185 |
if not syntax_ok:
|
| 1186 |
return {
|
|
|
|
| 1190 |
"symbol_table": {},
|
| 1191 |
}
|
| 1192 |
|
|
|
|
| 1193 |
try:
|
| 1194 |
filtered = [t for t in tokens if getattr(t, 'type', '') != '\n']
|
| 1195 |
ast = _build_ast(filtered)
|
| 1196 |
|
|
|
|
| 1197 |
st = {
|
| 1198 |
"variables": [
|
| 1199 |
{
|
|
|
|
| 1237 |
}
|
| 1238 |
|
| 1239 |
|
|
|
Backend/semantic/__init__.py
CHANGED
|
@@ -1,9 +1,3 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# SEMANTIC PACKAGE - Symbol table + tree-walking AST validator
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Public API:
|
| 5 |
-
# from semantic import SymbolTable, validate_ast, ASTValidator
|
| 6 |
-
# ============================================================================
|
| 7 |
|
| 8 |
from .symbol_table import SymbolTable # noqa: F401
|
| 9 |
from .analyzer import ASTValidator, validate_ast # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
from .symbol_table import SymbolTable # noqa: F401
|
| 3 |
from .analyzer import ASTValidator, validate_ast # noqa: F401
|
Backend/semantic/analyzer.py
CHANGED
|
@@ -1,24 +1,7 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# SEMANTIC ANALYZER - Tree-walking validator for the completed AST
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from GALsemantic.py during the modular restructure.
|
| 5 |
-
# The parser already does primary checks during AST construction. This
|
| 6 |
-
# validator does the SECOND pass over the completed tree to catch checks
|
| 7 |
-
# that need a global view (function arity, return-path coverage, etc.).
|
| 8 |
-
# ============================================================================
|
| 9 |
from semantic.errors import SemanticError
|
| 10 |
|
| 11 |
|
| 12 |
class ASTValidator:
|
| 13 |
-
"""Tree-walking semantic validator.
|
| 14 |
-
|
| 15 |
-
Receives an already-built AST and walks every node to verify:
|
| 16 |
-
• Variable declarations are well-formed
|
| 17 |
-
• Function declarations have names and return types
|
| 18 |
-
• Break/continue appear only inside loops or switches
|
| 19 |
-
• Loop and conditional bodies are present
|
| 20 |
-
• Expressions have valid structure
|
| 21 |
-
"""
|
| 22 |
|
| 23 |
def __init__(self):
|
| 24 |
self.errors = []
|
|
@@ -28,10 +11,8 @@ class ASTValidator:
|
|
| 28 |
self._in_function = False
|
| 29 |
self._current_func_type = None
|
| 30 |
|
| 31 |
-
# ── public entry point ──────────────────────────────────────────
|
| 32 |
|
| 33 |
def validate(self, ast, symbol_table_data):
|
| 34 |
-
"""Validate *ast* and return a result dict."""
|
| 35 |
self._walk(ast)
|
| 36 |
return {
|
| 37 |
"success": len(self.errors) == 0,
|
|
@@ -41,7 +22,6 @@ class ASTValidator:
|
|
| 41 |
"ast": ast,
|
| 42 |
}
|
| 43 |
|
| 44 |
-
# ── recursive walker ────────────────────────────────────────────
|
| 45 |
|
| 46 |
def _walk(self, node):
|
| 47 |
if node is None:
|
|
@@ -50,11 +30,9 @@ class ASTValidator:
|
|
| 50 |
if handler:
|
| 51 |
handler(node)
|
| 52 |
else:
|
| 53 |
-
# Default: visit all children
|
| 54 |
for child in getattr(node, 'children', []):
|
| 55 |
self._walk(child)
|
| 56 |
|
| 57 |
-
# ── node-specific validators ────────────────────────────────────
|
| 58 |
|
| 59 |
def _check_Program(self, node):
|
| 60 |
for child in node.children:
|
|
@@ -72,7 +50,6 @@ class ASTValidator:
|
|
| 72 |
self._walk(child)
|
| 73 |
|
| 74 |
def _check_SturdyDeclaration(self, node):
|
| 75 |
-
# fertile (constant) declaration
|
| 76 |
if len(node.children) < 3:
|
| 77 |
self.errors.append(
|
| 78 |
f"Ln {node.line} Semantic Error: Fertile declaration must have type, name, and value.")
|
|
@@ -86,7 +63,6 @@ class ASTValidator:
|
|
| 86 |
prev_in_func = self._in_function
|
| 87 |
prev_func_type = self._current_func_type
|
| 88 |
self._in_function = True
|
| 89 |
-
# Return type is in the first child
|
| 90 |
if node.children:
|
| 91 |
self._current_func_type = node.children[0].value
|
| 92 |
for child in node.children:
|
|
@@ -222,25 +198,6 @@ class ASTValidator:
|
|
| 222 |
self._walk(child)
|
| 223 |
|
| 224 |
|
| 225 |
-
# ============================================================================
|
| 226 |
-
# validate_ast() - PUBLIC SEMANTIC-ANALYSIS ENTRY POINT
|
| 227 |
-
# Used by server.py after parse_and_build succeeds. Wraps ASTValidator.
|
| 228 |
-
# Returns a dict with success/errors/warnings/symbol_table/ast.
|
| 229 |
-
# ============================================================================
|
| 230 |
def validate_ast(ast, symbol_table_data):
|
| 231 |
-
"""Public API: validate an already-built AST.
|
| 232 |
-
|
| 233 |
-
This is the semantic analysis phase of the compiler pipeline.
|
| 234 |
-
The parser has already built the AST (with primary checks during
|
| 235 |
-
construction). This function performs a tree-walking validation
|
| 236 |
-
pass over the completed AST.
|
| 237 |
-
|
| 238 |
-
Args:
|
| 239 |
-
ast: The root AST node (ProgramNode) produced by the parser.
|
| 240 |
-
symbol_table_data: Serialized symbol table dict from the parser.
|
| 241 |
-
|
| 242 |
-
Returns:
|
| 243 |
-
dict with keys: success, errors, warnings, symbol_table, ast
|
| 244 |
-
"""
|
| 245 |
validator = ASTValidator()
|
| 246 |
return validator.validate(ast, symbol_table_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from semantic.errors import SemanticError
|
| 2 |
|
| 3 |
|
| 4 |
class ASTValidator:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
def __init__(self):
|
| 7 |
self.errors = []
|
|
|
|
| 11 |
self._in_function = False
|
| 12 |
self._current_func_type = None
|
| 13 |
|
|
|
|
| 14 |
|
| 15 |
def validate(self, ast, symbol_table_data):
|
|
|
|
| 16 |
self._walk(ast)
|
| 17 |
return {
|
| 18 |
"success": len(self.errors) == 0,
|
|
|
|
| 22 |
"ast": ast,
|
| 23 |
}
|
| 24 |
|
|
|
|
| 25 |
|
| 26 |
def _walk(self, node):
|
| 27 |
if node is None:
|
|
|
|
| 30 |
if handler:
|
| 31 |
handler(node)
|
| 32 |
else:
|
|
|
|
| 33 |
for child in getattr(node, 'children', []):
|
| 34 |
self._walk(child)
|
| 35 |
|
|
|
|
| 36 |
|
| 37 |
def _check_Program(self, node):
|
| 38 |
for child in node.children:
|
|
|
|
| 50 |
self._walk(child)
|
| 51 |
|
| 52 |
def _check_SturdyDeclaration(self, node):
|
|
|
|
| 53 |
if len(node.children) < 3:
|
| 54 |
self.errors.append(
|
| 55 |
f"Ln {node.line} Semantic Error: Fertile declaration must have type, name, and value.")
|
|
|
|
| 63 |
prev_in_func = self._in_function
|
| 64 |
prev_func_type = self._current_func_type
|
| 65 |
self._in_function = True
|
|
|
|
| 66 |
if node.children:
|
| 67 |
self._current_func_type = node.children[0].value
|
| 68 |
for child in node.children:
|
|
|
|
| 198 |
self._walk(child)
|
| 199 |
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
def validate_ast(ast, symbol_table_data):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
validator = ASTValidator()
|
| 203 |
return validator.validate(ast, symbol_table_data)
|
Backend/semantic/errors.py
CHANGED
|
@@ -1,10 +1,3 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# SEMANTIC ERROR - Raised during AST construction / validation
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Raised by parser/builder.py (during AST construction) and by
|
| 5 |
-
# semantic/analyzer.py (during the final tree-walking validation pass).
|
| 6 |
-
# Includes the source line so the IDE can highlight the right location.
|
| 7 |
-
# ============================================================================
|
| 8 |
|
| 9 |
|
| 10 |
class SemanticError(Exception):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
|
| 3 |
class SemanticError(Exception):
|
Backend/semantic/symbol_table.py
CHANGED
|
@@ -1,31 +1,19 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# SYMBOL TABLE - Declaration tracker for the AST builder
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from GALsemantic.py during the modular restructure.
|
| 5 |
-
# Used by parser/builder.py during AST construction to track variables,
|
| 6 |
-
# functions, bundles, and scopes; the populated table is then serialized
|
| 7 |
-
# (in parser/parser.py) and passed to semantic/analyzer.py for the second
|
| 8 |
-
# semantic pass.
|
| 9 |
-
# ============================================================================
|
| 10 |
|
| 11 |
|
| 12 |
class SymbolTable:
|
| 13 |
def __init__(self):
|
| 14 |
-
self.variables = {}
|
| 15 |
-
self.global_variables = {}
|
| 16 |
-
self.functions = {}
|
| 17 |
-
self.scopes = [{}]
|
| 18 |
self.current_func_name = None
|
| 19 |
self.function_variables = {}
|
| 20 |
-
self.bundle_types = {}
|
| 21 |
|
| 22 |
-
###### VARIABLE ######
|
| 23 |
def declare_variable(self, name, type_, value=None, is_list=False, is_fertile=False):
|
| 24 |
scope = self.scopes[-1]
|
| 25 |
current_func = self.current_func_name
|
| 26 |
|
| 27 |
-
#for i, s in enumerate(self.scopes):
|
| 28 |
-
#print(f"[SCOPE {i}] {s}")
|
| 29 |
|
| 30 |
if name in self.functions:
|
| 31 |
return f"Semantic Error: Variable '{name}' already declared as a function."
|
|
@@ -59,7 +47,6 @@ class SymbolTable:
|
|
| 59 |
}
|
| 60 |
|
| 61 |
|
| 62 |
-
|
| 63 |
def lookup_variable(self, name):
|
| 64 |
for i, scope in enumerate(reversed(self.scopes)):
|
| 65 |
if name in scope:
|
|
@@ -78,7 +65,6 @@ class SymbolTable:
|
|
| 78 |
else:
|
| 79 |
return f"Semantic Error: Variable '{name}' not declared in the current scope."
|
| 80 |
|
| 81 |
-
###### FUNCTION ######
|
| 82 |
def declare_function(self, name, return_type, params, node=None):
|
| 83 |
if name in self.functions:
|
| 84 |
return f"Semantic Error: Function '{name}' already declared."
|
|
@@ -90,7 +76,6 @@ class SymbolTable:
|
|
| 90 |
return f"Semantic Error: Function '{name}' is not defined."
|
| 91 |
|
| 92 |
|
| 93 |
-
###### SCOPE ######
|
| 94 |
def enter_scope(self):
|
| 95 |
self.scopes.append({})
|
| 96 |
|
|
@@ -114,15 +99,3 @@ class SymbolTable:
|
|
| 114 |
print("================================\n")
|
| 115 |
|
| 116 |
|
| 117 |
-
|
| 118 |
-
# ============================================================================
|
| 119 |
-
# BUILD AST + parse_* HELPERS - Moved to parser/builder.py (Phase 5b).
|
| 120 |
-
# No re-export here because that would create a circular import:
|
| 121 |
-
# parser.builder imports SymbolTable from here, so we can't also import
|
| 122 |
-
# back from parser.builder.
|
| 123 |
-
# External callers should import directly from the new location:
|
| 124 |
-
# from parser.builder import build_ast, symbol_table, analyze_semantics
|
| 125 |
-
# ============================================================================
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
|
| 3 |
class SymbolTable:
|
| 4 |
def __init__(self):
|
| 5 |
+
self.variables = {}
|
| 6 |
+
self.global_variables = {}
|
| 7 |
+
self.functions = {}
|
| 8 |
+
self.scopes = [{}]
|
| 9 |
self.current_func_name = None
|
| 10 |
self.function_variables = {}
|
| 11 |
+
self.bundle_types = {}
|
| 12 |
|
|
|
|
| 13 |
def declare_variable(self, name, type_, value=None, is_list=False, is_fertile=False):
|
| 14 |
scope = self.scopes[-1]
|
| 15 |
current_func = self.current_func_name
|
| 16 |
|
|
|
|
|
|
|
| 17 |
|
| 18 |
if name in self.functions:
|
| 19 |
return f"Semantic Error: Variable '{name}' already declared as a function."
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
|
|
|
|
| 50 |
def lookup_variable(self, name):
|
| 51 |
for i, scope in enumerate(reversed(self.scopes)):
|
| 52 |
if name in scope:
|
|
|
|
| 65 |
else:
|
| 66 |
return f"Semantic Error: Variable '{name}' not declared in the current scope."
|
| 67 |
|
|
|
|
| 68 |
def declare_function(self, name, return_type, params, node=None):
|
| 69 |
if name in self.functions:
|
| 70 |
return f"Semantic Error: Function '{name}' already declared."
|
|
|
|
| 76 |
return f"Semantic Error: Function '{name}' is not defined."
|
| 77 |
|
| 78 |
|
|
|
|
| 79 |
def enter_scope(self):
|
| 80 |
self.scopes.append({})
|
| 81 |
|
|
|
|
| 99 |
print("================================\n")
|
| 100 |
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Backend/server.py
CHANGED
|
@@ -1,52 +1,27 @@
|
|
| 1 |
-
|
| 2 |
-
# GAL COMPILER SERVER - HTTP + WebSocket entry point
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# This is the orchestrator of the entire compiler. It exposes Flask routes
|
| 5 |
-
# for each compiler stage (lex / parse / semantic / icg / run) and Socket.IO
|
| 6 |
-
# events for interactive program execution. Every request flows through:
|
| 7 |
-
#
|
| 8 |
-
# source code -> lexer -> parser+AST -> semantic -> ICG -> interpreter
|
| 9 |
-
#
|
| 10 |
-
# Each stage short-circuits on failure so the user sees the FIRST stage that
|
| 11 |
-
# rejected their code, not a flood of cascading errors.
|
| 12 |
-
# ============================================================================
|
| 13 |
-
|
| 14 |
-
# ============================================================================
|
| 15 |
# WARNING SUPPRESSION + EVENTLET BOOTSTRAP
|
| 16 |
-
# Eventlet provides cooperative concurrency so Socket.IO can park a request
|
| 17 |
-
# (e.g. waiting on water() input) without blocking the whole server.
|
| 18 |
-
# monkey_patch() must run BEFORE flask/socketio are imported.
|
| 19 |
-
# ============================================================================
|
| 20 |
import warnings
|
| 21 |
warnings.filterwarnings("ignore", message=".*RLock.*were not greened.*")
|
| 22 |
|
| 23 |
import eventlet
|
| 24 |
eventlet.monkey_patch()
|
| 25 |
|
| 26 |
-
# ============================================================================
|
| 27 |
-
# IMPORTS - Flask web framework + every layer of the GAL compiler
|
| 28 |
-
# ============================================================================
|
| 29 |
from flask import Flask, request, jsonify, send_from_directory
|
| 30 |
from flask_cors import CORS
|
| 31 |
from flask_socketio import SocketIO, emit
|
| 32 |
import os
|
| 33 |
-
from google import genai
|
| 34 |
-
from lexer import lex, get_token_description
|
| 35 |
-
from parser import LL1Parser
|
| 36 |
-
from cfg import cfg, first_sets, predict_sets
|
| 37 |
-
from parser.builder import analyze_semantics
|
| 38 |
-
from semantic import validate_ast
|
| 39 |
-
from icg import generate_icg
|
| 40 |
-
from interpreter import Interpreter, InterpreterError, _CancelledError
|
| 41 |
-
from ai import fallback_reply
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# ============================================================================
|
| 45 |
-
# DISPLAY HELPER - Escapes control characters so token values are safe
|
| 46 |
-
# to embed in JSON responses for the IDE's lexeme table.
|
| 47 |
-
# ============================================================================
|
| 48 |
def _display_value(val):
|
| 49 |
-
"""Escape special chars in token values for safe display (like C's repr)."""
|
| 50 |
if val is None:
|
| 51 |
return ''
|
| 52 |
s = str(val)
|
|
@@ -56,54 +31,33 @@ def _display_value(val):
|
|
| 56 |
return s
|
| 57 |
|
| 58 |
|
| 59 |
-
# ============================================================================
|
| 60 |
-
# FLASK APP INITIALIZATION
|
| 61 |
-
# ============================================================================
|
| 62 |
app = Flask(__name__, static_folder='../UI', static_url_path='')
|
| 63 |
-
CORS(app)
|
| 64 |
-
socketio = SocketIO(app, cors_allowed_origins="*")
|
| 65 |
|
| 66 |
-
# Per-session interpreter registry (sid -> Interpreter instance).
|
| 67 |
-
# One user can only have one program running at a time; new runs cancel the old.
|
| 68 |
interpreters = {}
|
| 69 |
|
| 70 |
|
| 71 |
-
# ============================================================================
|
| 72 |
-
# SESSION EMITTER - Routes interpreter output to the originating browser
|
| 73 |
-
# session instead of broadcasting to every connected client.
|
| 74 |
-
# ============================================================================
|
| 75 |
class SessionEmitter:
|
| 76 |
-
"""Wrapper around SocketIO that always emits to a specific client session."""
|
| 77 |
def __init__(self, sio, sid):
|
| 78 |
-
self._sio = sio
|
| 79 |
-
self._sid = sid
|
| 80 |
|
| 81 |
def emit(self, event, data=None, **kwargs):
|
| 82 |
-
# Always 'to=sid' so output goes to one user, never broadcast
|
| 83 |
self._sio.emit(event, data, to=self._sid, **kwargs)
|
| 84 |
|
| 85 |
|
| 86 |
-
# ============================================================================
|
| 87 |
-
# PARSER SINGLETON - Built once at startup; the LL(1) parser is stateless
|
| 88 |
-
# across calls so this instance is shared by every request.
|
| 89 |
-
# ============================================================================
|
| 90 |
parser = LL1Parser(
|
| 91 |
-
cfg=cfg,
|
| 92 |
-
predict_sets=predict_sets,
|
| 93 |
-
first_sets=first_sets,
|
| 94 |
-
start_symbol="<program>",
|
| 95 |
-
end_marker="EOF",
|
| 96 |
-
skip_token_types={'\n'}
|
| 97 |
)
|
| 98 |
|
| 99 |
-
# ============================================================================
|
| 100 |
-
# STATIC FILE SERVING - Serves the IDE's HTML/CSS/JS/images so the whole app
|
| 101 |
-
# works as a single deployable. The no-cache header prevents stale UI during
|
| 102 |
-
# development.
|
| 103 |
-
# ============================================================================
|
| 104 |
@app.after_request
|
| 105 |
def add_no_cache(response):
|
| 106 |
-
"""Prevent browser from caching static files during development."""
|
| 107 |
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
| 108 |
response.headers['Pragma'] = 'no-cache'
|
| 109 |
response.headers['Expires'] = '0'
|
|
@@ -111,32 +65,19 @@ def add_no_cache(response):
|
|
| 111 |
|
| 112 |
@app.route('/')
|
| 113 |
def index():
|
| 114 |
-
"""Serve the main HTML file"""
|
| 115 |
return send_from_directory('../UI', 'index.html')
|
| 116 |
|
| 117 |
@app.route('/images/<path:filename>')
|
| 118 |
def serve_images(filename):
|
| 119 |
-
"""Serve image files from root images folder"""
|
| 120 |
return send_from_directory('../images', filename)
|
| 121 |
|
| 122 |
@app.route('/<path:path>')
|
| 123 |
def serve_static(path):
|
| 124 |
-
"""Serve static files (CSS, JS, etc.) from UI folder"""
|
| 125 |
return send_from_directory('../UI', path)
|
| 126 |
|
| 127 |
|
| 128 |
-
# ============================================================================
|
| 129 |
-
# API ENDPOINT: /api/lex - LEXICAL ANALYSIS (stage 1 only)
|
| 130 |
-
# Returns the token stream and any lexical errors. Used by the IDE's
|
| 131 |
-
# "Lexemes" tab to display the token table.
|
| 132 |
-
# ============================================================================
|
| 133 |
@app.route('/api/lex', methods=['POST'])
|
| 134 |
def lexer_endpoint():
|
| 135 |
-
"""
|
| 136 |
-
API endpoint for lexical analysis
|
| 137 |
-
Expects JSON: {"source_code": "your GAL code here"}
|
| 138 |
-
Returns JSON: {"tokens": [...], "errors": [...]}
|
| 139 |
-
"""
|
| 140 |
try:
|
| 141 |
data = request.get_json()
|
| 142 |
|
|
@@ -147,10 +88,8 @@ def lexer_endpoint():
|
|
| 147 |
|
| 148 |
source_code = data['source_code']
|
| 149 |
|
| 150 |
-
# Run the lexer
|
| 151 |
tokens, errors = lex(source_code)
|
| 152 |
|
| 153 |
-
# Convert tokens to serializable format
|
| 154 |
token_list = []
|
| 155 |
for token in tokens:
|
| 156 |
token_list.append({
|
|
@@ -171,19 +110,8 @@ def lexer_endpoint():
|
|
| 171 |
'error': f'Server error: {str(e)}'
|
| 172 |
}), 500
|
| 173 |
|
| 174 |
-
# ============================================================================
|
| 175 |
-
# API ENDPOINT: /api/parse - LEX + SYNTAX ANALYSIS
|
| 176 |
-
# Runs the lexer and (only if no lex errors) the LL(1) parser.
|
| 177 |
-
# Short-circuits at the first failing stage and labels the response with
|
| 178 |
-
# the failing stage so the IDE can highlight the right phase indicator.
|
| 179 |
-
# ============================================================================
|
| 180 |
@app.route('/api/parse', methods=['POST'])
|
| 181 |
def parser_endpoint():
|
| 182 |
-
"""
|
| 183 |
-
API endpoint for syntax analysis (parsing)
|
| 184 |
-
Expects JSON: {"source_code": "your GAL code here"}
|
| 185 |
-
Returns JSON: {"success": true/false, "tokens": [...], "errors": [...]}
|
| 186 |
-
"""
|
| 187 |
try:
|
| 188 |
data = request.get_json()
|
| 189 |
|
|
@@ -194,10 +122,8 @@ def parser_endpoint():
|
|
| 194 |
|
| 195 |
source_code = data['source_code']
|
| 196 |
|
| 197 |
-
# First, run the lexer to get tokens
|
| 198 |
tokens, lex_errors = lex(source_code)
|
| 199 |
|
| 200 |
-
# Convert tokens to serializable format
|
| 201 |
token_list = []
|
| 202 |
for token in tokens:
|
| 203 |
token_list.append({
|
|
@@ -208,7 +134,6 @@ def parser_endpoint():
|
|
| 208 |
'description': get_token_description(token.type, token.value)
|
| 209 |
})
|
| 210 |
|
| 211 |
-
# Only run the parser if there are no lexical errors
|
| 212 |
if lex_errors:
|
| 213 |
return jsonify({
|
| 214 |
'success': False,
|
|
@@ -221,7 +146,6 @@ def parser_endpoint():
|
|
| 221 |
|
| 222 |
parse_success, parse_errors = parser.parse(tokens)
|
| 223 |
|
| 224 |
-
# Determine which stages have errors
|
| 225 |
stages = []
|
| 226 |
if parse_errors:
|
| 227 |
stages.append('syntax')
|
|
@@ -240,31 +164,16 @@ def parser_endpoint():
|
|
| 240 |
'error': f'Server error: {str(e)}'
|
| 241 |
}), 500
|
| 242 |
|
| 243 |
-
# ============================================================================
|
| 244 |
-
# API ENDPOINT: /api/health - liveness check (used by deployment tools)
|
| 245 |
-
# ============================================================================
|
| 246 |
@app.route('/api/health', methods=['GET'])
|
| 247 |
def health_check():
|
| 248 |
-
"""Health check endpoint"""
|
| 249 |
return jsonify({
|
| 250 |
'status': 'healthy',
|
| 251 |
'message': 'GAL Compiler Server is running'
|
| 252 |
})
|
| 253 |
|
| 254 |
|
| 255 |
-
# ============================================================================
|
| 256 |
-
# API ENDPOINT: /api/semantic - LEX + PARSE + AST + SEMANTIC ANALYSIS
|
| 257 |
-
# Uses the parser's two-step API: parse_and_build (syntax + AST construction)
|
| 258 |
-
# then validate_ast (tree-walking semantic checks). Distinguishes between
|
| 259 |
-
# 'syntax' and 'semantic' error stages even though both come from the parser.
|
| 260 |
-
# ============================================================================
|
| 261 |
@app.route('/api/semantic', methods=['POST'])
|
| 262 |
def semantic_endpoint():
|
| 263 |
-
"""
|
| 264 |
-
API endpoint for semantic analysis
|
| 265 |
-
Expects JSON: {"source_code": "your GAL code here"}
|
| 266 |
-
Returns JSON: {"success": true/false, "errors": [...], "warnings": [...], "symbol_table": {...}}
|
| 267 |
-
"""
|
| 268 |
try:
|
| 269 |
data = request.get_json()
|
| 270 |
|
|
@@ -275,10 +184,8 @@ def semantic_endpoint():
|
|
| 275 |
|
| 276 |
source_code = data['source_code']
|
| 277 |
|
| 278 |
-
# First, run the lexer to get tokens
|
| 279 |
tokens, lex_errors = lex(source_code)
|
| 280 |
|
| 281 |
-
# Convert tokens to serializable format
|
| 282 |
token_list = []
|
| 283 |
for token in tokens:
|
| 284 |
token_list.append({
|
|
@@ -289,7 +196,6 @@ def semantic_endpoint():
|
|
| 289 |
'description': get_token_description(token.type, token.value)
|
| 290 |
})
|
| 291 |
|
| 292 |
-
# If there are lexical errors, return them without semantic analysis
|
| 293 |
if lex_errors:
|
| 294 |
return jsonify({
|
| 295 |
'success': False,
|
|
@@ -299,12 +205,9 @@ def semantic_endpoint():
|
|
| 299 |
'stage': 'lexical'
|
| 300 |
})
|
| 301 |
|
| 302 |
-
# Run the parser — validates syntax (LL1) then builds AST
|
| 303 |
parse_result = parser.parse_and_build(tokens)
|
| 304 |
|
| 305 |
-
# If syntax or AST construction failed, return errors
|
| 306 |
if not parse_result['success']:
|
| 307 |
-
# Distinguish semantic errors caught during AST building
|
| 308 |
error_stage = parse_result.get('error_stage', 'syntax')
|
| 309 |
return jsonify({
|
| 310 |
'success': False,
|
|
@@ -314,7 +217,6 @@ def semantic_endpoint():
|
|
| 314 |
'stage': error_stage
|
| 315 |
})
|
| 316 |
|
| 317 |
-
# Run semantic analysis — tree-walking validation of the AST
|
| 318 |
semantic_result = validate_ast(parse_result['ast'], parse_result['symbol_table'])
|
| 319 |
|
| 320 |
return jsonify({
|
|
@@ -331,20 +233,8 @@ def semantic_endpoint():
|
|
| 331 |
'error': f'Server error: {str(e)}'
|
| 332 |
}), 500
|
| 333 |
|
| 334 |
-
# ============================================================================
|
| 335 |
-
# API ENDPOINT: /api/icg - LEX + PARSE + SEMANTIC + ICG (display only)
|
| 336 |
-
# ICG produces three-address code (TAC) for the IDE's "Intermediate Code"
|
| 337 |
-
# tab. The interpreter does NOT consume this output; it walks the AST directly.
|
| 338 |
-
# So ICG is a teaching/visualization layer, not a runtime layer.
|
| 339 |
-
# ============================================================================
|
| 340 |
@app.route('/api/icg', methods=['POST'])
|
| 341 |
def icg_endpoint():
|
| 342 |
-
"""
|
| 343 |
-
API endpoint for intermediate code generation.
|
| 344 |
-
Runs lexer → parser → semantic → ICG pipeline.
|
| 345 |
-
Expects JSON: {"source_code": "your GAL code here"}
|
| 346 |
-
Returns JSON with TAC instructions.
|
| 347 |
-
"""
|
| 348 |
try:
|
| 349 |
data = request.get_json()
|
| 350 |
|
|
@@ -355,7 +245,6 @@ def icg_endpoint():
|
|
| 355 |
|
| 356 |
source_code = data['source_code']
|
| 357 |
|
| 358 |
-
# 1. Lexical analysis
|
| 359 |
tokens, lex_errors = lex(source_code)
|
| 360 |
|
| 361 |
token_list = []
|
|
@@ -376,7 +265,6 @@ def icg_endpoint():
|
|
| 376 |
'stage': 'lexical'
|
| 377 |
})
|
| 378 |
|
| 379 |
-
# 2. Syntax analysis + AST construction (parser builds AST)
|
| 380 |
parse_result = parser.parse_and_build(tokens)
|
| 381 |
if not parse_result['success']:
|
| 382 |
error_stage = parse_result.get('error_stage', 'syntax')
|
|
@@ -387,7 +275,6 @@ def icg_endpoint():
|
|
| 387 |
'stage': error_stage
|
| 388 |
})
|
| 389 |
|
| 390 |
-
# 3. Semantic analysis — tree-walking validation of the AST
|
| 391 |
semantic_result = validate_ast(parse_result['ast'], parse_result['symbol_table'])
|
| 392 |
if not semantic_result['success']:
|
| 393 |
return jsonify({
|
|
@@ -398,7 +285,6 @@ def icg_endpoint():
|
|
| 398 |
'stage': 'semantic'
|
| 399 |
})
|
| 400 |
|
| 401 |
-
# 4. Intermediate code generation
|
| 402 |
icg_result = generate_icg(tokens)
|
| 403 |
|
| 404 |
return jsonify({
|
|
@@ -417,44 +303,25 @@ def icg_endpoint():
|
|
| 417 |
}), 500
|
| 418 |
|
| 419 |
|
| 420 |
-
# ============================================================================
|
| 421 |
-
# SYNCHRONOUS EXECUTION (no Socket.IO required)
|
| 422 |
-
#
|
| 423 |
-
# OutputCollector is an adapter — same .emit() interface as SessionEmitter,
|
| 424 |
-
# but instead of streaming output via WebSocket it captures it in a list.
|
| 425 |
-
# This is the adapter pattern: one Interpreter class, two delivery modes.
|
| 426 |
-
# ============================================================================
|
| 427 |
-
|
| 428 |
class OutputCollector:
|
| 429 |
-
"""Drop-in replacement for SessionEmitter that collects output in a list."""
|
| 430 |
def __init__(self):
|
| 431 |
-
self.outputs = []
|
| 432 |
-
self.needs_input = False
|
| 433 |
|
| 434 |
def emit(self, event, data=None, **kwargs):
|
| 435 |
if event == 'output' and data:
|
| 436 |
-
# Accumulate normal plant() output
|
| 437 |
self.outputs.append(data.get('output', ''))
|
| 438 |
elif event == 'input_required':
|
| 439 |
-
# No interactive channel here -> abort the interpreter so the
|
| 440 |
-
# client can switch to the Socket.IO flow that supports input.
|
| 441 |
self.needs_input = True
|
| 442 |
raise _InputNeeded()
|
| 443 |
|
| 444 |
|
| 445 |
class _InputNeeded(Exception):
|
| 446 |
-
"""Raised by OutputCollector to abort REST execution when water() is called."""
|
| 447 |
pass
|
| 448 |
|
| 449 |
|
| 450 |
-
# ============================================================================
|
| 451 |
-
# API ENDPOINT: /api/run - One-shot execution returning all output at once.
|
| 452 |
-
# Used for non-interactive programs (no water() calls). For interactive runs,
|
| 453 |
-
# the client uses the Socket.IO 'run_code' event below.
|
| 454 |
-
# ============================================================================
|
| 455 |
@app.route('/api/run', methods=['POST'])
|
| 456 |
def run_endpoint():
|
| 457 |
-
"""Run a GAL program synchronously and return all output."""
|
| 458 |
try:
|
| 459 |
data = request.get_json()
|
| 460 |
if not data or 'source_code' not in data:
|
|
@@ -462,7 +329,6 @@ def run_endpoint():
|
|
| 462 |
|
| 463 |
source_code = data['source_code']
|
| 464 |
|
| 465 |
-
# 1. Lex
|
| 466 |
tokens, lex_errors = lex(source_code)
|
| 467 |
if lex_errors:
|
| 468 |
return jsonify({
|
|
@@ -472,7 +338,6 @@ def run_endpoint():
|
|
| 472 |
'errors': lex_errors
|
| 473 |
})
|
| 474 |
|
| 475 |
-
# 2. Parse + build AST (parser builds the AST)
|
| 476 |
parse_result = parser.parse_and_build(tokens)
|
| 477 |
if not parse_result['success']:
|
| 478 |
error_stage = parse_result.get('error_stage', 'syntax')
|
|
@@ -483,7 +348,6 @@ def run_endpoint():
|
|
| 483 |
'errors': [str(e) for e in parse_result['errors']]
|
| 484 |
})
|
| 485 |
|
| 486 |
-
# 3. Semantic — tree-walking validation of the AST
|
| 487 |
semantic_result = validate_ast(parse_result['ast'], parse_result['symbol_table'])
|
| 488 |
if not semantic_result['success']:
|
| 489 |
return jsonify({
|
|
@@ -495,7 +359,6 @@ def run_endpoint():
|
|
| 495 |
|
| 496 |
ast = semantic_result['ast']
|
| 497 |
|
| 498 |
-
# 4. Interpret
|
| 499 |
collector = OutputCollector()
|
| 500 |
interp = Interpreter(socketio=collector)
|
| 501 |
try:
|
|
@@ -507,7 +370,6 @@ def run_endpoint():
|
|
| 507 |
'errors': []
|
| 508 |
})
|
| 509 |
except _InputNeeded:
|
| 510 |
-
# Program called water() — return partial output and flag
|
| 511 |
return jsonify({
|
| 512 |
'success': False,
|
| 513 |
'stage': 'execution',
|
|
@@ -536,35 +398,20 @@ def run_endpoint():
|
|
| 536 |
return jsonify({'error': f'Server error: {str(e)}'}), 500
|
| 537 |
|
| 538 |
|
| 539 |
-
# ============================================================================
|
| 540 |
-
# SOCKET.IO INTERACTIVE EXECUTION
|
| 541 |
-
#
|
| 542 |
-
# This path supports water() input. The interpreter runs in a green thread
|
| 543 |
-
# (eventlet) so it can park while waiting for input without blocking the
|
| 544 |
-
# server. Output is streamed back via 'output' events as plant() fires.
|
| 545 |
-
# ============================================================================
|
| 546 |
-
|
| 547 |
@socketio.on('connect')
|
| 548 |
def handle_connect():
|
| 549 |
-
# No setup needed — interpreter is created lazily on first 'run_code'.
|
| 550 |
pass
|
| 551 |
|
| 552 |
@socketio.on('disconnect')
|
| 553 |
def handle_disconnect():
|
| 554 |
-
# Drop the user's interpreter so memory doesn't leak across reconnects.
|
| 555 |
sid = request.sid
|
| 556 |
interpreters.pop(sid, None)
|
| 557 |
|
| 558 |
@socketio.on('run_code')
|
| 559 |
def handle_run_code(data):
|
| 560 |
-
"""
|
| 561 |
-
Run a GAL program through lex → parse → semantic → interpreter.
|
| 562 |
-
Program output is sent back via 'output' events.
|
| 563 |
-
"""
|
| 564 |
sid = request.sid
|
| 565 |
source_code = data.get('source_code', '')
|
| 566 |
|
| 567 |
-
# 1. Lexical analysis
|
| 568 |
tokens, lex_errors = lex(source_code)
|
| 569 |
if lex_errors:
|
| 570 |
for err in lex_errors:
|
|
@@ -572,10 +419,8 @@ def handle_run_code(data):
|
|
| 572 |
emit('execution_complete', {'success': False, 'stage': 'lexical'})
|
| 573 |
return
|
| 574 |
|
| 575 |
-
# Notify client that lexical analysis passed
|
| 576 |
emit('stage_complete', {'stage': 'lexical', 'success': True})
|
| 577 |
|
| 578 |
-
# 2. Syntax analysis + AST construction (parser builds AST)
|
| 579 |
parse_result = parser.parse_and_build(tokens)
|
| 580 |
if not parse_result['success']:
|
| 581 |
error_stage = parse_result.get('error_stage', 'syntax')
|
|
@@ -584,10 +429,8 @@ def handle_run_code(data):
|
|
| 584 |
emit('execution_complete', {'success': False, 'stage': error_stage})
|
| 585 |
return
|
| 586 |
|
| 587 |
-
# Notify client that syntax analysis passed
|
| 588 |
emit('stage_complete', {'stage': 'syntax', 'success': True})
|
| 589 |
|
| 590 |
-
# 3. Semantic analysis — tree-walking validation of the AST
|
| 591 |
semantic_result = validate_ast(parse_result['ast'], parse_result['symbol_table'])
|
| 592 |
if not semantic_result['success']:
|
| 593 |
for err in semantic_result['errors']:
|
|
@@ -595,23 +438,19 @@ def handle_run_code(data):
|
|
| 595 |
emit('execution_complete', {'success': False, 'stage': 'semantic'})
|
| 596 |
return
|
| 597 |
|
| 598 |
-
# Notify client that semantic analysis passed
|
| 599 |
emit('stage_complete', {'stage': 'semantic', 'success': True})
|
| 600 |
|
| 601 |
ast = semantic_result['ast']
|
| 602 |
|
| 603 |
-
# 4. Interpretation — run in background task for input support
|
| 604 |
-
# Cancel any previously-running interpreter for this session
|
| 605 |
old_interp = interpreters.get(sid)
|
| 606 |
if old_interp and hasattr(old_interp, '_cancelled'):
|
| 607 |
old_interp._cancelled = True
|
| 608 |
-
# Unblock any pending wait_for_input so the old thread can exit
|
| 609 |
for evt in list(old_interp.input_events.values()):
|
| 610 |
try:
|
| 611 |
-
evt.send(None)
|
| 612 |
except (AttributeError, AssertionError):
|
| 613 |
try:
|
| 614 |
-
evt.set()
|
| 615 |
except Exception:
|
| 616 |
pass
|
| 617 |
|
|
@@ -625,7 +464,7 @@ def handle_run_code(data):
|
|
| 625 |
if not interp._cancelled:
|
| 626 |
socketio.emit('execution_complete', {'success': True, 'stage': 'execution'}, to=sid)
|
| 627 |
except _CancelledError:
|
| 628 |
-
pass
|
| 629 |
except InterpreterError as e:
|
| 630 |
if not getattr(interp, '_cancelled', False):
|
| 631 |
socketio.emit('output', {'output': f'Runtime Error: {e}'}, to=sid)
|
|
@@ -635,46 +474,28 @@ def handle_run_code(data):
|
|
| 635 |
socketio.emit('output', {'output': f'Internal Error: {e}'}, to=sid)
|
| 636 |
socketio.emit('execution_complete', {'success': False, 'stage': 'execution'}, to=sid)
|
| 637 |
finally:
|
| 638 |
-
# Only remove ourselves — don't remove a newer interpreter
|
| 639 |
if interpreters.get(sid) is interp:
|
| 640 |
interpreters.pop(sid, None)
|
| 641 |
|
| 642 |
socketio.start_background_task(run_interpreter)
|
| 643 |
|
| 644 |
-
# ============================================================================
|
| 645 |
-
# SOCKET.IO INPUT CHANNEL - When a running program calls water(), the
|
| 646 |
-
# interpreter parks on an event. The frontend prompts the user, sends back
|
| 647 |
-
# 'capture_input', and this handler routes the value to the right interpreter.
|
| 648 |
-
# ============================================================================
|
| 649 |
@socketio.on('capture_input')
|
| 650 |
def handle_capture_input(data):
|
| 651 |
-
"""Receive input from the client and forward to the waiting interpreter."""
|
| 652 |
sid = request.sid
|
| 653 |
interp = interpreters.get(sid)
|
| 654 |
if interp:
|
| 655 |
var_name = data.get('var_name', '')
|
| 656 |
input_value = data.get('input', '')
|
| 657 |
-
# Unblock the parked interpreter so execution resumes
|
| 658 |
interp.provide_input(var_name, input_value)
|
| 659 |
|
| 660 |
|
| 661 |
-
# ============================================================================
|
| 662 |
-
# AI CHAT HELPER (Google Gemini)
|
| 663 |
-
# Optional learning aid — answers user questions about GAL syntax. Falls
|
| 664 |
-
# back to a rule-based reply if no GEMINI_API_KEY is set or the API fails.
|
| 665 |
-
# This is NOT part of the compiler pipeline.
|
| 666 |
-
# ============================================================================
|
| 667 |
-
|
| 668 |
-
# Load the system prompt that teaches Gemini how to talk about GAL
|
| 669 |
_prompt_path = os.path.join(os.path.dirname(__file__), 'ai', 'prompt.txt')
|
| 670 |
with open(_prompt_path, 'r', encoding='utf-8') as _f:
|
| 671 |
GAL_SYSTEM_PROMPT = _f.read()
|
| 672 |
|
| 673 |
|
| 674 |
-
|
| 675 |
-
# Initialize Gemini client — requires GEMINI_API_KEY env var
|
| 676 |
_gemini_client = None
|
| 677 |
-
_chat_sessions = {}
|
| 678 |
|
| 679 |
def _get_gemini_client():
|
| 680 |
global _gemini_client
|
|
@@ -687,7 +508,6 @@ def _get_gemini_client():
|
|
| 687 |
|
| 688 |
@app.route('/api/chat', methods=['POST'])
|
| 689 |
def chat_endpoint():
|
| 690 |
-
"""AI chat helper endpoint using Google Gemini."""
|
| 691 |
try:
|
| 692 |
data = request.get_json()
|
| 693 |
if not data or 'message' not in data:
|
|
@@ -699,27 +519,22 @@ def chat_endpoint():
|
|
| 699 |
|
| 700 |
client = _get_gemini_client()
|
| 701 |
if client is None:
|
| 702 |
-
# No API key — use fallback
|
| 703 |
fb_reply = fallback_reply(user_message)
|
| 704 |
return jsonify({'reply': fb_reply, 'mode': 'fallback'})
|
| 705 |
|
| 706 |
-
# Build/get conversation history for this session
|
| 707 |
if session_id not in _chat_sessions:
|
| 708 |
_chat_sessions[session_id] = []
|
| 709 |
history = _chat_sessions[session_id]
|
| 710 |
|
| 711 |
-
# Include editor code as context if provided
|
| 712 |
full_message = user_message
|
| 713 |
if editor_code.strip():
|
| 714 |
full_message = f"[Current code in editor]:\n```\n{editor_code}\n```\n\nUser question: {user_message}"
|
| 715 |
|
| 716 |
history.append({'role': 'user', 'parts': [{'text': full_message}]})
|
| 717 |
|
| 718 |
-
# Keep history manageable (last 20 messages)
|
| 719 |
if len(history) > 20:
|
| 720 |
history[:] = history[-20:]
|
| 721 |
|
| 722 |
-
# Try multiple models in case one has quota available
|
| 723 |
models_to_try = ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.0-flash', 'gemini-2.0-flash-lite']
|
| 724 |
last_error: Exception = RuntimeError("No Gemini models were available to try")
|
| 725 |
for model_name in models_to_try:
|
|
@@ -733,16 +548,15 @@ def chat_endpoint():
|
|
| 733 |
max_output_tokens=4096,
|
| 734 |
),
|
| 735 |
)
|
| 736 |
-
break
|
| 737 |
except Exception as model_err:
|
| 738 |
last_error = model_err
|
| 739 |
err_msg = str(model_err)
|
| 740 |
-
# Retry-worthy errors: quota, availability, or auth (same key fails for all models)
|
| 741 |
if '429' not in err_msg and 'RESOURCE_EXHAUSTED' not in err_msg and '503' not in err_msg and 'UNAVAILABLE' not in err_msg and '403' not in err_msg and 'PERMISSION_DENIED' not in err_msg:
|
| 742 |
-
raise
|
| 743 |
continue
|
| 744 |
else:
|
| 745 |
-
raise last_error
|
| 746 |
|
| 747 |
reply = response.text or 'No response generated.'
|
| 748 |
|
|
@@ -751,30 +565,22 @@ def chat_endpoint():
|
|
| 751 |
return jsonify({'reply': reply, 'mode': 'ai'})
|
| 752 |
|
| 753 |
except Exception as e:
|
| 754 |
-
# All Gemini models failed — use rule-based fallback
|
| 755 |
try:
|
| 756 |
fb_reply = fallback_reply(user_message)
|
| 757 |
return jsonify({'reply': fb_reply, 'mode': 'fallback'})
|
| 758 |
except Exception:
|
| 759 |
-
pass
|
| 760 |
err_str = str(e)
|
| 761 |
return jsonify({'error': f'Chat error: {err_str}'}), 500
|
| 762 |
|
| 763 |
@app.route('/api/chat/clear', methods=['POST'])
|
| 764 |
def chat_clear_endpoint():
|
| 765 |
-
"""Clear chat history for a session."""
|
| 766 |
data = request.get_json() or {}
|
| 767 |
session_id = data.get('session_id', 'default')
|
| 768 |
_chat_sessions.pop(session_id, None)
|
| 769 |
return jsonify({'success': True})
|
| 770 |
|
| 771 |
|
| 772 |
-
# ============================================================================
|
| 773 |
-
# SERVER STARTUP - Reads PORT/DEBUG env vars, prints a banner listing every
|
| 774 |
-
# endpoint, then hands the app to eventlet's WSGI server.
|
| 775 |
-
# host='0.0.0.0' makes the server reachable from any network interface,
|
| 776 |
-
# not just localhost.
|
| 777 |
-
# ============================================================================
|
| 778 |
if __name__ == '__main__':
|
| 779 |
port = int(os.environ.get('PORT', 5000))
|
| 780 |
debug = os.environ.get('DEBUG', 'False') == 'True'
|
|
@@ -788,5 +594,4 @@ if __name__ == '__main__':
|
|
| 788 |
print(f" - POST http://localhost:{port}/api/icg (Intermediate Code Generation)")
|
| 789 |
print(f" - POST http://localhost:{port}/api/chat (AI Chat Helper)")
|
| 790 |
print(f" - Socket.IO: run_code (Execute Program)")
|
| 791 |
-
# allow_unsafe_werkzeug=True is needed when running inside eventlet during dev
|
| 792 |
socketio.run(app, host='0.0.0.0', port=port, debug=debug, allow_unsafe_werkzeug=True)
|
|
|
|
| 1 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
# WARNING SUPPRESSION + EVENTLET BOOTSTRAP
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import warnings
|
| 4 |
warnings.filterwarnings("ignore", message=".*RLock.*were not greened.*")
|
| 5 |
|
| 6 |
import eventlet
|
| 7 |
eventlet.monkey_patch()
|
| 8 |
|
|
|
|
|
|
|
|
|
|
| 9 |
from flask import Flask, request, jsonify, send_from_directory
|
| 10 |
from flask_cors import CORS
|
| 11 |
from flask_socketio import SocketIO, emit
|
| 12 |
import os
|
| 13 |
+
from google import genai
|
| 14 |
+
from lexer import lex, get_token_description
|
| 15 |
+
from parser import LL1Parser
|
| 16 |
+
from cfg import cfg, first_sets, predict_sets
|
| 17 |
+
from parser.builder import analyze_semantics
|
| 18 |
+
from semantic import validate_ast
|
| 19 |
+
from icg import generate_icg
|
| 20 |
+
from interpreter import Interpreter, InterpreterError, _CancelledError
|
| 21 |
+
from ai import fallback_reply
|
| 22 |
+
|
| 23 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def _display_value(val):
|
|
|
|
| 25 |
if val is None:
|
| 26 |
return ''
|
| 27 |
s = str(val)
|
|
|
|
| 31 |
return s
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
| 34 |
app = Flask(__name__, static_folder='../UI', static_url_path='')
|
| 35 |
+
CORS(app)
|
| 36 |
+
socketio = SocketIO(app, cors_allowed_origins="*")
|
| 37 |
|
|
|
|
|
|
|
| 38 |
interpreters = {}
|
| 39 |
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
class SessionEmitter:
|
|
|
|
| 42 |
def __init__(self, sio, sid):
|
| 43 |
+
self._sio = sio
|
| 44 |
+
self._sid = sid
|
| 45 |
|
| 46 |
def emit(self, event, data=None, **kwargs):
|
|
|
|
| 47 |
self._sio.emit(event, data, to=self._sid, **kwargs)
|
| 48 |
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
parser = LL1Parser(
|
| 51 |
+
cfg=cfg,
|
| 52 |
+
predict_sets=predict_sets,
|
| 53 |
+
first_sets=first_sets,
|
| 54 |
+
start_symbol="<program>",
|
| 55 |
+
end_marker="EOF",
|
| 56 |
+
skip_token_types={'\n'}
|
| 57 |
)
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
@app.after_request
|
| 60 |
def add_no_cache(response):
|
|
|
|
| 61 |
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
| 62 |
response.headers['Pragma'] = 'no-cache'
|
| 63 |
response.headers['Expires'] = '0'
|
|
|
|
| 65 |
|
| 66 |
@app.route('/')
|
| 67 |
def index():
|
|
|
|
| 68 |
return send_from_directory('../UI', 'index.html')
|
| 69 |
|
| 70 |
@app.route('/images/<path:filename>')
|
| 71 |
def serve_images(filename):
|
|
|
|
| 72 |
return send_from_directory('../images', filename)
|
| 73 |
|
| 74 |
@app.route('/<path:path>')
|
| 75 |
def serve_static(path):
|
|
|
|
| 76 |
return send_from_directory('../UI', path)
|
| 77 |
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
@app.route('/api/lex', methods=['POST'])
|
| 80 |
def lexer_endpoint():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
try:
|
| 82 |
data = request.get_json()
|
| 83 |
|
|
|
|
| 88 |
|
| 89 |
source_code = data['source_code']
|
| 90 |
|
|
|
|
| 91 |
tokens, errors = lex(source_code)
|
| 92 |
|
|
|
|
| 93 |
token_list = []
|
| 94 |
for token in tokens:
|
| 95 |
token_list.append({
|
|
|
|
| 110 |
'error': f'Server error: {str(e)}'
|
| 111 |
}), 500
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
@app.route('/api/parse', methods=['POST'])
|
| 114 |
def parser_endpoint():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
try:
|
| 116 |
data = request.get_json()
|
| 117 |
|
|
|
|
| 122 |
|
| 123 |
source_code = data['source_code']
|
| 124 |
|
|
|
|
| 125 |
tokens, lex_errors = lex(source_code)
|
| 126 |
|
|
|
|
| 127 |
token_list = []
|
| 128 |
for token in tokens:
|
| 129 |
token_list.append({
|
|
|
|
| 134 |
'description': get_token_description(token.type, token.value)
|
| 135 |
})
|
| 136 |
|
|
|
|
| 137 |
if lex_errors:
|
| 138 |
return jsonify({
|
| 139 |
'success': False,
|
|
|
|
| 146 |
|
| 147 |
parse_success, parse_errors = parser.parse(tokens)
|
| 148 |
|
|
|
|
| 149 |
stages = []
|
| 150 |
if parse_errors:
|
| 151 |
stages.append('syntax')
|
|
|
|
| 164 |
'error': f'Server error: {str(e)}'
|
| 165 |
}), 500
|
| 166 |
|
|
|
|
|
|
|
|
|
|
| 167 |
@app.route('/api/health', methods=['GET'])
|
| 168 |
def health_check():
|
|
|
|
| 169 |
return jsonify({
|
| 170 |
'status': 'healthy',
|
| 171 |
'message': 'GAL Compiler Server is running'
|
| 172 |
})
|
| 173 |
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
@app.route('/api/semantic', methods=['POST'])
|
| 176 |
def semantic_endpoint():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
try:
|
| 178 |
data = request.get_json()
|
| 179 |
|
|
|
|
| 184 |
|
| 185 |
source_code = data['source_code']
|
| 186 |
|
|
|
|
| 187 |
tokens, lex_errors = lex(source_code)
|
| 188 |
|
|
|
|
| 189 |
token_list = []
|
| 190 |
for token in tokens:
|
| 191 |
token_list.append({
|
|
|
|
| 196 |
'description': get_token_description(token.type, token.value)
|
| 197 |
})
|
| 198 |
|
|
|
|
| 199 |
if lex_errors:
|
| 200 |
return jsonify({
|
| 201 |
'success': False,
|
|
|
|
| 205 |
'stage': 'lexical'
|
| 206 |
})
|
| 207 |
|
|
|
|
| 208 |
parse_result = parser.parse_and_build(tokens)
|
| 209 |
|
|
|
|
| 210 |
if not parse_result['success']:
|
|
|
|
| 211 |
error_stage = parse_result.get('error_stage', 'syntax')
|
| 212 |
return jsonify({
|
| 213 |
'success': False,
|
|
|
|
| 217 |
'stage': error_stage
|
| 218 |
})
|
| 219 |
|
|
|
|
| 220 |
semantic_result = validate_ast(parse_result['ast'], parse_result['symbol_table'])
|
| 221 |
|
| 222 |
return jsonify({
|
|
|
|
| 233 |
'error': f'Server error: {str(e)}'
|
| 234 |
}), 500
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
@app.route('/api/icg', methods=['POST'])
|
| 237 |
def icg_endpoint():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
try:
|
| 239 |
data = request.get_json()
|
| 240 |
|
|
|
|
| 245 |
|
| 246 |
source_code = data['source_code']
|
| 247 |
|
|
|
|
| 248 |
tokens, lex_errors = lex(source_code)
|
| 249 |
|
| 250 |
token_list = []
|
|
|
|
| 265 |
'stage': 'lexical'
|
| 266 |
})
|
| 267 |
|
|
|
|
| 268 |
parse_result = parser.parse_and_build(tokens)
|
| 269 |
if not parse_result['success']:
|
| 270 |
error_stage = parse_result.get('error_stage', 'syntax')
|
|
|
|
| 275 |
'stage': error_stage
|
| 276 |
})
|
| 277 |
|
|
|
|
| 278 |
semantic_result = validate_ast(parse_result['ast'], parse_result['symbol_table'])
|
| 279 |
if not semantic_result['success']:
|
| 280 |
return jsonify({
|
|
|
|
| 285 |
'stage': 'semantic'
|
| 286 |
})
|
| 287 |
|
|
|
|
| 288 |
icg_result = generate_icg(tokens)
|
| 289 |
|
| 290 |
return jsonify({
|
|
|
|
| 303 |
}), 500
|
| 304 |
|
| 305 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
class OutputCollector:
|
|
|
|
| 307 |
def __init__(self):
|
| 308 |
+
self.outputs = []
|
| 309 |
+
self.needs_input = False
|
| 310 |
|
| 311 |
def emit(self, event, data=None, **kwargs):
|
| 312 |
if event == 'output' and data:
|
|
|
|
| 313 |
self.outputs.append(data.get('output', ''))
|
| 314 |
elif event == 'input_required':
|
|
|
|
|
|
|
| 315 |
self.needs_input = True
|
| 316 |
raise _InputNeeded()
|
| 317 |
|
| 318 |
|
| 319 |
class _InputNeeded(Exception):
|
|
|
|
| 320 |
pass
|
| 321 |
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
@app.route('/api/run', methods=['POST'])
|
| 324 |
def run_endpoint():
|
|
|
|
| 325 |
try:
|
| 326 |
data = request.get_json()
|
| 327 |
if not data or 'source_code' not in data:
|
|
|
|
| 329 |
|
| 330 |
source_code = data['source_code']
|
| 331 |
|
|
|
|
| 332 |
tokens, lex_errors = lex(source_code)
|
| 333 |
if lex_errors:
|
| 334 |
return jsonify({
|
|
|
|
| 338 |
'errors': lex_errors
|
| 339 |
})
|
| 340 |
|
|
|
|
| 341 |
parse_result = parser.parse_and_build(tokens)
|
| 342 |
if not parse_result['success']:
|
| 343 |
error_stage = parse_result.get('error_stage', 'syntax')
|
|
|
|
| 348 |
'errors': [str(e) for e in parse_result['errors']]
|
| 349 |
})
|
| 350 |
|
|
|
|
| 351 |
semantic_result = validate_ast(parse_result['ast'], parse_result['symbol_table'])
|
| 352 |
if not semantic_result['success']:
|
| 353 |
return jsonify({
|
|
|
|
| 359 |
|
| 360 |
ast = semantic_result['ast']
|
| 361 |
|
|
|
|
| 362 |
collector = OutputCollector()
|
| 363 |
interp = Interpreter(socketio=collector)
|
| 364 |
try:
|
|
|
|
| 370 |
'errors': []
|
| 371 |
})
|
| 372 |
except _InputNeeded:
|
|
|
|
| 373 |
return jsonify({
|
| 374 |
'success': False,
|
| 375 |
'stage': 'execution',
|
|
|
|
| 398 |
return jsonify({'error': f'Server error: {str(e)}'}), 500
|
| 399 |
|
| 400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
@socketio.on('connect')
|
| 402 |
def handle_connect():
|
|
|
|
| 403 |
pass
|
| 404 |
|
| 405 |
@socketio.on('disconnect')
|
| 406 |
def handle_disconnect():
|
|
|
|
| 407 |
sid = request.sid
|
| 408 |
interpreters.pop(sid, None)
|
| 409 |
|
| 410 |
@socketio.on('run_code')
|
| 411 |
def handle_run_code(data):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
sid = request.sid
|
| 413 |
source_code = data.get('source_code', '')
|
| 414 |
|
|
|
|
| 415 |
tokens, lex_errors = lex(source_code)
|
| 416 |
if lex_errors:
|
| 417 |
for err in lex_errors:
|
|
|
|
| 419 |
emit('execution_complete', {'success': False, 'stage': 'lexical'})
|
| 420 |
return
|
| 421 |
|
|
|
|
| 422 |
emit('stage_complete', {'stage': 'lexical', 'success': True})
|
| 423 |
|
|
|
|
| 424 |
parse_result = parser.parse_and_build(tokens)
|
| 425 |
if not parse_result['success']:
|
| 426 |
error_stage = parse_result.get('error_stage', 'syntax')
|
|
|
|
| 429 |
emit('execution_complete', {'success': False, 'stage': error_stage})
|
| 430 |
return
|
| 431 |
|
|
|
|
| 432 |
emit('stage_complete', {'stage': 'syntax', 'success': True})
|
| 433 |
|
|
|
|
| 434 |
semantic_result = validate_ast(parse_result['ast'], parse_result['symbol_table'])
|
| 435 |
if not semantic_result['success']:
|
| 436 |
for err in semantic_result['errors']:
|
|
|
|
| 438 |
emit('execution_complete', {'success': False, 'stage': 'semantic'})
|
| 439 |
return
|
| 440 |
|
|
|
|
| 441 |
emit('stage_complete', {'stage': 'semantic', 'success': True})
|
| 442 |
|
| 443 |
ast = semantic_result['ast']
|
| 444 |
|
|
|
|
|
|
|
| 445 |
old_interp = interpreters.get(sid)
|
| 446 |
if old_interp and hasattr(old_interp, '_cancelled'):
|
| 447 |
old_interp._cancelled = True
|
|
|
|
| 448 |
for evt in list(old_interp.input_events.values()):
|
| 449 |
try:
|
| 450 |
+
evt.send(None)
|
| 451 |
except (AttributeError, AssertionError):
|
| 452 |
try:
|
| 453 |
+
evt.set()
|
| 454 |
except Exception:
|
| 455 |
pass
|
| 456 |
|
|
|
|
| 464 |
if not interp._cancelled:
|
| 465 |
socketio.emit('execution_complete', {'success': True, 'stage': 'execution'}, to=sid)
|
| 466 |
except _CancelledError:
|
| 467 |
+
pass
|
| 468 |
except InterpreterError as e:
|
| 469 |
if not getattr(interp, '_cancelled', False):
|
| 470 |
socketio.emit('output', {'output': f'Runtime Error: {e}'}, to=sid)
|
|
|
|
| 474 |
socketio.emit('output', {'output': f'Internal Error: {e}'}, to=sid)
|
| 475 |
socketio.emit('execution_complete', {'success': False, 'stage': 'execution'}, to=sid)
|
| 476 |
finally:
|
|
|
|
| 477 |
if interpreters.get(sid) is interp:
|
| 478 |
interpreters.pop(sid, None)
|
| 479 |
|
| 480 |
socketio.start_background_task(run_interpreter)
|
| 481 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
@socketio.on('capture_input')
|
| 483 |
def handle_capture_input(data):
|
|
|
|
| 484 |
sid = request.sid
|
| 485 |
interp = interpreters.get(sid)
|
| 486 |
if interp:
|
| 487 |
var_name = data.get('var_name', '')
|
| 488 |
input_value = data.get('input', '')
|
|
|
|
| 489 |
interp.provide_input(var_name, input_value)
|
| 490 |
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
_prompt_path = os.path.join(os.path.dirname(__file__), 'ai', 'prompt.txt')
|
| 493 |
with open(_prompt_path, 'r', encoding='utf-8') as _f:
|
| 494 |
GAL_SYSTEM_PROMPT = _f.read()
|
| 495 |
|
| 496 |
|
|
|
|
|
|
|
| 497 |
_gemini_client = None
|
| 498 |
+
_chat_sessions = {}
|
| 499 |
|
| 500 |
def _get_gemini_client():
|
| 501 |
global _gemini_client
|
|
|
|
| 508 |
|
| 509 |
@app.route('/api/chat', methods=['POST'])
|
| 510 |
def chat_endpoint():
|
|
|
|
| 511 |
try:
|
| 512 |
data = request.get_json()
|
| 513 |
if not data or 'message' not in data:
|
|
|
|
| 519 |
|
| 520 |
client = _get_gemini_client()
|
| 521 |
if client is None:
|
|
|
|
| 522 |
fb_reply = fallback_reply(user_message)
|
| 523 |
return jsonify({'reply': fb_reply, 'mode': 'fallback'})
|
| 524 |
|
|
|
|
| 525 |
if session_id not in _chat_sessions:
|
| 526 |
_chat_sessions[session_id] = []
|
| 527 |
history = _chat_sessions[session_id]
|
| 528 |
|
|
|
|
| 529 |
full_message = user_message
|
| 530 |
if editor_code.strip():
|
| 531 |
full_message = f"[Current code in editor]:\n```\n{editor_code}\n```\n\nUser question: {user_message}"
|
| 532 |
|
| 533 |
history.append({'role': 'user', 'parts': [{'text': full_message}]})
|
| 534 |
|
|
|
|
| 535 |
if len(history) > 20:
|
| 536 |
history[:] = history[-20:]
|
| 537 |
|
|
|
|
| 538 |
models_to_try = ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.0-flash', 'gemini-2.0-flash-lite']
|
| 539 |
last_error: Exception = RuntimeError("No Gemini models were available to try")
|
| 540 |
for model_name in models_to_try:
|
|
|
|
| 548 |
max_output_tokens=4096,
|
| 549 |
),
|
| 550 |
)
|
| 551 |
+
break
|
| 552 |
except Exception as model_err:
|
| 553 |
last_error = model_err
|
| 554 |
err_msg = str(model_err)
|
|
|
|
| 555 |
if '429' not in err_msg and 'RESOURCE_EXHAUSTED' not in err_msg and '503' not in err_msg and 'UNAVAILABLE' not in err_msg and '403' not in err_msg and 'PERMISSION_DENIED' not in err_msg:
|
| 556 |
+
raise
|
| 557 |
continue
|
| 558 |
else:
|
| 559 |
+
raise last_error
|
| 560 |
|
| 561 |
reply = response.text or 'No response generated.'
|
| 562 |
|
|
|
|
| 565 |
return jsonify({'reply': reply, 'mode': 'ai'})
|
| 566 |
|
| 567 |
except Exception as e:
|
|
|
|
| 568 |
try:
|
| 569 |
fb_reply = fallback_reply(user_message)
|
| 570 |
return jsonify({'reply': fb_reply, 'mode': 'fallback'})
|
| 571 |
except Exception:
|
| 572 |
+
pass
|
| 573 |
err_str = str(e)
|
| 574 |
return jsonify({'error': f'Chat error: {err_str}'}), 500
|
| 575 |
|
| 576 |
@app.route('/api/chat/clear', methods=['POST'])
|
| 577 |
def chat_clear_endpoint():
|
|
|
|
| 578 |
data = request.get_json() or {}
|
| 579 |
session_id = data.get('session_id', 'default')
|
| 580 |
_chat_sessions.pop(session_id, None)
|
| 581 |
return jsonify({'success': True})
|
| 582 |
|
| 583 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
if __name__ == '__main__':
|
| 585 |
port = int(os.environ.get('PORT', 5000))
|
| 586 |
debug = os.environ.get('DEBUG', 'False') == 'True'
|
|
|
|
| 594 |
print(f" - POST http://localhost:{port}/api/icg (Intermediate Code Generation)")
|
| 595 |
print(f" - POST http://localhost:{port}/api/chat (AI Chat Helper)")
|
| 596 |
print(f" - Socket.IO: run_code (Execute Program)")
|
|
|
|
| 597 |
socketio.run(app, host='0.0.0.0', port=port, debug=debug, allow_unsafe_werkzeug=True)
|
Backend/shared/__init__.py
CHANGED
|
@@ -1,19 +1,3 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# SHARED PACKAGE - Cross-phase types used by every compiler stage
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Contains only the building blocks that genuinely cross phase boundaries:
|
| 5 |
-
# - tokens : TT_* constants, Token class, get_token_description
|
| 6 |
-
# (lexer creates, parser/semantic/icg/interpreter read)
|
| 7 |
-
# - ast_nodes : AST node class hierarchy
|
| 8 |
-
# (parser builds, semantic/icg/interpreter walk)
|
| 9 |
-
#
|
| 10 |
-
# Phase-private types live with their phase:
|
| 11 |
-
# - lexer/positions.py : Position class (lexer-owned)
|
| 12 |
-
# - lexer/delimiters.py : FSM character sets (lexer-only)
|
| 13 |
-
# - lexer/errors.py : LexicalError
|
| 14 |
-
# - semantic/errors.py : SemanticError
|
| 15 |
-
# - interpreter/errors.py : InterpreterError, ReturnValue, etc.
|
| 16 |
-
# ============================================================================
|
| 17 |
|
| 18 |
from .tokens import * # noqa: F401,F403 - TT_* constants
|
| 19 |
from .tokens import Token, get_token_description # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
from .tokens import * # noqa: F401,F403 - TT_* constants
|
| 3 |
from .tokens import Token, get_token_description # noqa: F401
|
Backend/shared/ast_nodes.py
CHANGED
|
@@ -1,36 +1,11 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Extracted from GALsemantic.py during the modular restructure.
|
| 5 |
-
# Every compiler phase after parsing consumes these node types:
|
| 6 |
-
# - parser/builder.py constructs them
|
| 7 |
-
# - semantic/analyzer.py validates them
|
| 8 |
-
# - icg/generator.py walks them to emit TAC
|
| 9 |
-
# - interpreter/ evaluates them
|
| 10 |
-
#
|
| 11 |
-
# Named ast_nodes.py (not ast.py / not in an ast/ folder) so it does not
|
| 12 |
-
# shadow Python's stdlib "ast" module — many third-party libraries
|
| 13 |
-
# (flask, eventlet, werkzeug) call into stdlib ast and would break if
|
| 14 |
-
# we hijacked the name.
|
| 15 |
-
# ============================================================================
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
# ============================================================================
|
| 19 |
-
# AST NODE CLASSES
|
| 20 |
-
# Each node has:
|
| 21 |
-
# - node_type : a string label (e.g. "Program", "VariableDeclaration")
|
| 22 |
-
# - value : the node's primary data (function name, operator, etc.)
|
| 23 |
-
# - children : list of child ASTNodes
|
| 24 |
-
# - line : source line for error reporting
|
| 25 |
-
# - parent : back-pointer for traversals (set by add_child)
|
| 26 |
-
# Subclasses below define convenient constructors for each AST shape.
|
| 27 |
-
# ============================================================================
|
| 28 |
class ASTNode:
|
| 29 |
def __init__(self, node_type, value=None, line=None):
|
| 30 |
-
self.node_type = node_type
|
| 31 |
-
self.value = value
|
| 32 |
-
self.children = []
|
| 33 |
-
self.parent = None
|
| 34 |
self.line = line
|
| 35 |
|
| 36 |
def add_child(self, child):
|
|
@@ -38,7 +13,6 @@ class ASTNode:
|
|
| 38 |
self.children.append(child)
|
| 39 |
|
| 40 |
def print_tree(self, level=0):
|
| 41 |
-
"""Pretty-print the AST."""
|
| 42 |
indent = ' ' * (level * 3)
|
| 43 |
print(f"{indent}╚═{self.node_type}: {self.value if self.value else ''}")
|
| 44 |
for child in self.children:
|
|
@@ -105,7 +79,6 @@ class WhileLoopNode(ASTNode):
|
|
| 105 |
class DoWhileLoopNode(ASTNode):
|
| 106 |
def __init__(self, condition, line=None):
|
| 107 |
super().__init__("DoWhileLoop", line=line)
|
| 108 |
-
#self.add_child(condition)
|
| 109 |
|
| 110 |
class PrintNode(ASTNode):
|
| 111 |
def __init__(self, args, line=None):
|
|
@@ -217,18 +190,16 @@ class ListAccessNode(ASTNode):
|
|
| 217 |
|
| 218 |
|
| 219 |
class MemberAccessNode(ASTNode):
|
| 220 |
-
"""Represents bundle member access: p.age or p.addr.zip (nested)"""
|
| 221 |
def __init__(self, object_name, member_name, line=None):
|
| 222 |
super().__init__("MemberAccess", line=line)
|
| 223 |
if isinstance(object_name, ASTNode):
|
| 224 |
-
self.add_child(object_name)
|
| 225 |
else:
|
| 226 |
self.add_child(ASTNode("Object", object_name, line=line))
|
| 227 |
self.add_child(ASTNode("Member", member_name, line=line))
|
| 228 |
|
| 229 |
|
| 230 |
class ArrayMemberAccessNode(ASTNode):
|
| 231 |
-
"""Represents bundle array element member access: p[0].x"""
|
| 232 |
def __init__(self, list_access_node, member_name, line=None):
|
| 233 |
super().__init__("ArrayMemberAccess", line=line)
|
| 234 |
self.add_child(list_access_node)
|
|
@@ -236,9 +207,8 @@ class ArrayMemberAccessNode(ASTNode):
|
|
| 236 |
|
| 237 |
|
| 238 |
class BundleDefinitionNode(ASTNode):
|
| 239 |
-
"""Represents a bundle (struct) type definition."""
|
| 240 |
def __init__(self, bundle_name, members, line=None):
|
| 241 |
super().__init__("BundleDefinition", line=line)
|
| 242 |
self.bundle_name = bundle_name
|
| 243 |
-
self.members = members
|
| 244 |
|
|
|
|
| 1 |
+
|
| 2 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
class ASTNode:
|
| 4 |
def __init__(self, node_type, value=None, line=None):
|
| 5 |
+
self.node_type = node_type
|
| 6 |
+
self.value = value
|
| 7 |
+
self.children = []
|
| 8 |
+
self.parent = None
|
| 9 |
self.line = line
|
| 10 |
|
| 11 |
def add_child(self, child):
|
|
|
|
| 13 |
self.children.append(child)
|
| 14 |
|
| 15 |
def print_tree(self, level=0):
|
|
|
|
| 16 |
indent = ' ' * (level * 3)
|
| 17 |
print(f"{indent}╚═{self.node_type}: {self.value if self.value else ''}")
|
| 18 |
for child in self.children:
|
|
|
|
| 79 |
class DoWhileLoopNode(ASTNode):
|
| 80 |
def __init__(self, condition, line=None):
|
| 81 |
super().__init__("DoWhileLoop", line=line)
|
|
|
|
| 82 |
|
| 83 |
class PrintNode(ASTNode):
|
| 84 |
def __init__(self, args, line=None):
|
|
|
|
| 190 |
|
| 191 |
|
| 192 |
class MemberAccessNode(ASTNode):
|
|
|
|
| 193 |
def __init__(self, object_name, member_name, line=None):
|
| 194 |
super().__init__("MemberAccess", line=line)
|
| 195 |
if isinstance(object_name, ASTNode):
|
| 196 |
+
self.add_child(object_name)
|
| 197 |
else:
|
| 198 |
self.add_child(ASTNode("Object", object_name, line=line))
|
| 199 |
self.add_child(ASTNode("Member", member_name, line=line))
|
| 200 |
|
| 201 |
|
| 202 |
class ArrayMemberAccessNode(ASTNode):
|
|
|
|
| 203 |
def __init__(self, list_access_node, member_name, line=None):
|
| 204 |
super().__init__("ArrayMemberAccess", line=line)
|
| 205 |
self.add_child(list_access_node)
|
|
|
|
| 207 |
|
| 208 |
|
| 209 |
class BundleDefinitionNode(ASTNode):
|
|
|
|
| 210 |
def __init__(self, bundle_name, members, line=None):
|
| 211 |
super().__init__("BundleDefinition", line=line)
|
| 212 |
self.bundle_name = bundle_name
|
| 213 |
+
self.members = members
|
| 214 |
|
Backend/shared/tokens.py
CHANGED
|
@@ -1,113 +1,100 @@
|
|
| 1 |
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
# ============================================================================
|
| 82 |
-
# TOKEN CLASS - Represents a single token (lexeme)
|
| 83 |
-
# ============================================================================
|
| 84 |
class Token:
|
| 85 |
-
"""Represents a token with type, value, line number, and column number"""
|
| 86 |
|
| 87 |
def __init__(self, type_, value=None, line=1, col=0):
|
| 88 |
-
self.type = type_
|
| 89 |
-
self.value = value
|
| 90 |
-
self.line = line
|
| 91 |
-
self.col = col
|
| 92 |
|
| 93 |
|
| 94 |
-
# ============================================================================
|
| 95 |
-
# TOKEN TYPE DESCRIPTIONS - Maps token types to human-readable descriptions
|
| 96 |
-
# ============================================================================
|
| 97 |
def get_token_description(token_type: str, value: str = '') -> str:
|
| 98 |
-
"""Returns a descriptive name for each token type"""
|
| 99 |
-
# Handle negative literals: value starts with ~ but token type is intlit/dbllit
|
| 100 |
if token_type == 'intlit' and isinstance(value, str) and value.startswith('~'):
|
| 101 |
return 'negative integer'
|
| 102 |
if token_type == 'dbllit' and isinstance(value, str) and value.startswith('~'):
|
| 103 |
return 'negative float'
|
| 104 |
|
| 105 |
descriptions = {
|
| 106 |
-
# Reserved Words - I/O
|
| 107 |
'water': 'Input Function',
|
| 108 |
'plant': 'Output Function',
|
| 109 |
|
| 110 |
-
# Reserved Words - Data Types
|
| 111 |
'seed': 'Integer Type',
|
| 112 |
'leaf': 'Character Type',
|
| 113 |
'branch': 't/f',
|
|
@@ -115,7 +102,6 @@ def get_token_description(token_type: str, value: str = '') -> str:
|
|
| 115 |
'vine': 'String Type',
|
| 116 |
'empty': 'Void Type',
|
| 117 |
|
| 118 |
-
# Reserved Words - Control Flow
|
| 119 |
'spring': 'If Statement',
|
| 120 |
'wither': 'Else Statement',
|
| 121 |
'bud': 'Else-If Statement',
|
|
@@ -123,23 +109,19 @@ def get_token_description(token_type: str, value: str = '') -> str:
|
|
| 123 |
'variety': 'Case Label',
|
| 124 |
'soil': 'Default Case',
|
| 125 |
|
| 126 |
-
# Reserved Words - Loops
|
| 127 |
'grow': 'While Loop',
|
| 128 |
'cultivate': 'For Loop',
|
| 129 |
'tend': 'Do-While Loop',
|
| 130 |
'prune': 'Break Statement',
|
| 131 |
'skip': 'Continue Statement',
|
| 132 |
|
| 133 |
-
# Reserved Words - Functions
|
| 134 |
'root': 'Main Function',
|
| 135 |
'pollinate': 'Function Declaration',
|
| 136 |
'reclaim': 'Return Statement',
|
| 137 |
|
| 138 |
-
# Reserved Words - Other
|
| 139 |
'fertile': 'Constant Declaration',
|
| 140 |
'bundle': 'Struct Definition',
|
| 141 |
|
| 142 |
-
# Identifiers and Literals
|
| 143 |
'id': 'Identifier',
|
| 144 |
'intlit': 'Integer Literal',
|
| 145 |
'dbllit': 'double Literal',
|
|
@@ -148,7 +130,6 @@ def get_token_description(token_type: str, value: str = '') -> str:
|
|
| 148 |
'sunshine': 'Boolean True',
|
| 149 |
'frost': 'Boolean False',
|
| 150 |
|
| 151 |
-
# Arithmetic Operators
|
| 152 |
'+': 'Plus Operator',
|
| 153 |
'-': 'Minus Operator',
|
| 154 |
'*': 'Multiply Operator',
|
|
@@ -159,7 +140,6 @@ def get_token_description(token_type: str, value: str = '') -> str:
|
|
| 159 |
'++': 'Increment Operator',
|
| 160 |
'--': 'Decrement Operator',
|
| 161 |
|
| 162 |
-
# Assignment Operators
|
| 163 |
'=': 'Assign Operator',
|
| 164 |
'+=': 'Add-Assign Operator',
|
| 165 |
'-=': 'Sub-Assign Operator',
|
|
@@ -167,7 +147,6 @@ def get_token_description(token_type: str, value: str = '') -> str:
|
|
| 167 |
'/=': 'Div-Assign Operator',
|
| 168 |
'%=': 'Mod-Assign Operator',
|
| 169 |
|
| 170 |
-
# Comparison Operators
|
| 171 |
'==': 'Equal Operator',
|
| 172 |
'!=': 'Not-Equal Operator',
|
| 173 |
'<': 'Less-Than Operator',
|
|
@@ -175,14 +154,12 @@ def get_token_description(token_type: str, value: str = '') -> str:
|
|
| 175 |
'<=': 'Less-Equal Operator',
|
| 176 |
'>=': 'Greater-Equal Operator',
|
| 177 |
|
| 178 |
-
# Logical Operators
|
| 179 |
'&&': 'AND Operator',
|
| 180 |
'&': 'Invalid Single-Ampersand',
|
| 181 |
'||': 'OR Operator',
|
| 182 |
'|': 'Invalid Single-Pipe',
|
| 183 |
'!': 'NOT Operator',
|
| 184 |
|
| 185 |
-
# Delimiters and Punctuation
|
| 186 |
'(': 'Left Parenthesis',
|
| 187 |
')': 'Right Parenthesis',
|
| 188 |
'{': 'Left Brace',
|
|
@@ -195,7 +172,6 @@ def get_token_description(token_type: str, value: str = '') -> str:
|
|
| 195 |
'.': 'Dot Operator',
|
| 196 |
'`': 'Concatenation Operator',
|
| 197 |
|
| 198 |
-
# Special
|
| 199 |
'member': 'Struct Member',
|
| 200 |
'EOF': 'End of File',
|
| 201 |
'\n': 'Newline'
|
|
|
|
| 1 |
|
| 2 |
|
| 3 |
+
TT_RW_WATER = 'water'
|
| 4 |
+
TT_RW_PLANT = 'plant'
|
| 5 |
+
TT_RW_SEED = 'seed'
|
| 6 |
+
TT_RW_LEAF = 'leaf'
|
| 7 |
+
TT_RW_BRANCH = 'branch'
|
| 8 |
+
TT_RW_TREE = 'tree'
|
| 9 |
+
TT_RW_SPRING = 'spring'
|
| 10 |
+
TT_RW_WITHER = 'wither'
|
| 11 |
+
TT_RW_BUD = 'bud'
|
| 12 |
+
TT_RW_HARVEST = 'harvest'
|
| 13 |
+
TT_RW_GROW = 'grow'
|
| 14 |
+
TT_RW_CULTIVATE = 'cultivate'
|
| 15 |
+
TT_RW_TEND = 'tend'
|
| 16 |
+
TT_RW_EMPTY = 'empty'
|
| 17 |
+
TT_RW_PRUNE = 'prune'
|
| 18 |
+
TT_RW_SKIP = 'skip'
|
| 19 |
+
TT_RW_RECLAIM = 'reclaim'
|
| 20 |
+
TT_RW_ROOT = 'root'
|
| 21 |
+
TT_RW_POLLINATE = 'pollinate'
|
| 22 |
+
TT_RW_VARIETY = 'variety'
|
| 23 |
+
TT_RW_FERTILE = 'fertile'
|
| 24 |
+
TT_RW_SOIL = 'soil'
|
| 25 |
+
TT_RW_BUNDLE = 'bundle'
|
| 26 |
+
TT_RW_VINE = 'vine'
|
| 27 |
+
|
| 28 |
+
TT_IDENTIFIER = 'id'
|
| 29 |
+
TT_PLUS = '+'
|
| 30 |
+
TT_MINUS = '-'
|
| 31 |
+
TT_MUL = '*'
|
| 32 |
+
TT_DIV = '/'
|
| 33 |
+
TT_MOD = '%'
|
| 34 |
+
TT_EXP = '**'
|
| 35 |
+
TT_EQ = '='
|
| 36 |
+
TT_EQTO = '=='
|
| 37 |
+
TT_PLUSEQ = '+='
|
| 38 |
+
TT_MINUSEQ = '-='
|
| 39 |
+
TT_MULTIEQ = '*='
|
| 40 |
+
TT_DIVEQ = '/='
|
| 41 |
+
TT_MODEQ = '%='
|
| 42 |
+
TT_EXPEQ = '**='
|
| 43 |
+
TT_CONCAT = '`'
|
| 44 |
+
TT_LPAREN = '('
|
| 45 |
+
TT_RPAREN = ')'
|
| 46 |
+
TT_SEMICOLON = ';'
|
| 47 |
+
TT_COMMA = ','
|
| 48 |
+
TT_COLON = ':'
|
| 49 |
+
TT_BLOCK_START = '{'
|
| 50 |
+
TT_BLOCK_END = '}'
|
| 51 |
+
TT_LT = '<'
|
| 52 |
+
TT_GT = '>'
|
| 53 |
+
TT_LTEQ = '<='
|
| 54 |
+
TT_GTEQ = '>='
|
| 55 |
+
TT_NOTEQ = '!='
|
| 56 |
+
TT_EOF = 'EOF'
|
| 57 |
+
TT_AND = '&&'
|
| 58 |
+
TT_OR = '||'
|
| 59 |
+
TT_SINGLE_AND = '&'
|
| 60 |
+
TT_SINGLE_OR = '|'
|
| 61 |
+
TT_NOT = '!'
|
| 62 |
+
TT_INCREMENT = '++'
|
| 63 |
+
TT_DECREMENT = '--'
|
| 64 |
+
TT_LSQBR = '['
|
| 65 |
+
TT_RSQBR = ']'
|
| 66 |
+
TT_NEGATIVE = '~'
|
| 67 |
+
TT_MEMBER = 'member'
|
| 68 |
+
TT_INTEGERLIT = 'intlit'
|
| 69 |
+
TT_DOUBLELIT = 'dbllit'
|
| 70 |
+
TT_STRINGLIT = 'stringlit'
|
| 71 |
+
TT_CHARLIT = 'chrlit'
|
| 72 |
+
TT_BOOLLIT_TRUE = 'sunshine'
|
| 73 |
+
TT_BOOLLIT_FALSE = 'frost'
|
| 74 |
+
TT_STRCTACCESS = '.'
|
| 75 |
+
TT_NL = '\n'
|
| 76 |
+
TT_DOT = '.'
|
| 77 |
+
|
| 78 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
class Token:
|
|
|
|
| 80 |
|
| 81 |
def __init__(self, type_, value=None, line=1, col=0):
|
| 82 |
+
self.type = type_
|
| 83 |
+
self.value = value
|
| 84 |
+
self.line = line
|
| 85 |
+
self.col = col
|
| 86 |
|
| 87 |
|
|
|
|
|
|
|
|
|
|
| 88 |
def get_token_description(token_type: str, value: str = '') -> str:
|
|
|
|
|
|
|
| 89 |
if token_type == 'intlit' and isinstance(value, str) and value.startswith('~'):
|
| 90 |
return 'negative integer'
|
| 91 |
if token_type == 'dbllit' and isinstance(value, str) and value.startswith('~'):
|
| 92 |
return 'negative float'
|
| 93 |
|
| 94 |
descriptions = {
|
|
|
|
| 95 |
'water': 'Input Function',
|
| 96 |
'plant': 'Output Function',
|
| 97 |
|
|
|
|
| 98 |
'seed': 'Integer Type',
|
| 99 |
'leaf': 'Character Type',
|
| 100 |
'branch': 't/f',
|
|
|
|
| 102 |
'vine': 'String Type',
|
| 103 |
'empty': 'Void Type',
|
| 104 |
|
|
|
|
| 105 |
'spring': 'If Statement',
|
| 106 |
'wither': 'Else Statement',
|
| 107 |
'bud': 'Else-If Statement',
|
|
|
|
| 109 |
'variety': 'Case Label',
|
| 110 |
'soil': 'Default Case',
|
| 111 |
|
|
|
|
| 112 |
'grow': 'While Loop',
|
| 113 |
'cultivate': 'For Loop',
|
| 114 |
'tend': 'Do-While Loop',
|
| 115 |
'prune': 'Break Statement',
|
| 116 |
'skip': 'Continue Statement',
|
| 117 |
|
|
|
|
| 118 |
'root': 'Main Function',
|
| 119 |
'pollinate': 'Function Declaration',
|
| 120 |
'reclaim': 'Return Statement',
|
| 121 |
|
|
|
|
| 122 |
'fertile': 'Constant Declaration',
|
| 123 |
'bundle': 'Struct Definition',
|
| 124 |
|
|
|
|
| 125 |
'id': 'Identifier',
|
| 126 |
'intlit': 'Integer Literal',
|
| 127 |
'dbllit': 'double Literal',
|
|
|
|
| 130 |
'sunshine': 'Boolean True',
|
| 131 |
'frost': 'Boolean False',
|
| 132 |
|
|
|
|
| 133 |
'+': 'Plus Operator',
|
| 134 |
'-': 'Minus Operator',
|
| 135 |
'*': 'Multiply Operator',
|
|
|
|
| 140 |
'++': 'Increment Operator',
|
| 141 |
'--': 'Decrement Operator',
|
| 142 |
|
|
|
|
| 143 |
'=': 'Assign Operator',
|
| 144 |
'+=': 'Add-Assign Operator',
|
| 145 |
'-=': 'Sub-Assign Operator',
|
|
|
|
| 147 |
'/=': 'Div-Assign Operator',
|
| 148 |
'%=': 'Mod-Assign Operator',
|
| 149 |
|
|
|
|
| 150 |
'==': 'Equal Operator',
|
| 151 |
'!=': 'Not-Equal Operator',
|
| 152 |
'<': 'Less-Than Operator',
|
|
|
|
| 154 |
'<=': 'Less-Equal Operator',
|
| 155 |
'>=': 'Greater-Equal Operator',
|
| 156 |
|
|
|
|
| 157 |
'&&': 'AND Operator',
|
| 158 |
'&': 'Invalid Single-Ampersand',
|
| 159 |
'||': 'OR Operator',
|
| 160 |
'|': 'Invalid Single-Pipe',
|
| 161 |
'!': 'NOT Operator',
|
| 162 |
|
|
|
|
| 163 |
'(': 'Left Parenthesis',
|
| 164 |
')': 'Right Parenthesis',
|
| 165 |
'{': 'Left Brace',
|
|
|
|
| 172 |
'.': 'Dot Operator',
|
| 173 |
'`': 'Concatenation Operator',
|
| 174 |
|
|
|
|
| 175 |
'member': 'Struct Member',
|
| 176 |
'EOF': 'End of File',
|
| 177 |
'\n': 'Newline'
|
UI/main.js
CHANGED
|
@@ -54,7 +54,6 @@
|
|
| 54 |
[/\b(bloom|wither|spring)\b/, "io"],
|
| 55 |
// Comments
|
| 56 |
[/\/\/.*/, "comment"],
|
| 57 |
-
[/#.*/, "comment"],
|
| 58 |
[/\/\*/, 'comment', '@comment'],
|
| 59 |
// Numbers (integers and decimals)
|
| 60 |
[/\d+\.\d+/, "number"],
|
|
@@ -213,7 +212,6 @@
|
|
| 213 |
// Start of comment
|
| 214 |
if (ch === '/' && next === '/') { inLineComment = true; col++; continue; }
|
| 215 |
if (ch === '/' && next === '*') { inBlockComment = true; i++; col += 2; continue; }
|
| 216 |
-
if (ch === '#') { inLineComment = true; col++; continue; }
|
| 217 |
// Start of string/char
|
| 218 |
if (ch === '"') { inString = true; col++; continue; }
|
| 219 |
if (ch === "'") { inChar = true; col++; continue; }
|
|
@@ -268,7 +266,7 @@
|
|
| 268 |
continue;
|
| 269 |
}
|
| 270 |
// Skip empty lines, line comments, and brace-only lines
|
| 271 |
-
if (!trimmed || trimmed.startsWith('//')
|
| 272 |
// Strip inline comments to get actual code
|
| 273 |
const codePart = trimmed.replace(/\/\/.*$/, '').trimEnd();
|
| 274 |
if (!codePart) continue;
|
|
|
|
| 54 |
[/\b(bloom|wither|spring)\b/, "io"],
|
| 55 |
// Comments
|
| 56 |
[/\/\/.*/, "comment"],
|
|
|
|
| 57 |
[/\/\*/, 'comment', '@comment'],
|
| 58 |
// Numbers (integers and decimals)
|
| 59 |
[/\d+\.\d+/, "number"],
|
|
|
|
| 212 |
// Start of comment
|
| 213 |
if (ch === '/' && next === '/') { inLineComment = true; col++; continue; }
|
| 214 |
if (ch === '/' && next === '*') { inBlockComment = true; i++; col += 2; continue; }
|
|
|
|
| 215 |
// Start of string/char
|
| 216 |
if (ch === '"') { inString = true; col++; continue; }
|
| 217 |
if (ch === "'") { inChar = true; col++; continue; }
|
|
|
|
| 266 |
continue;
|
| 267 |
}
|
| 268 |
// Skip empty lines, line comments, and brace-only lines
|
| 269 |
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
| 270 |
// Strip inline comments to get actual code
|
| 271 |
const codePart = trimmed.replace(/\/\/.*$/, '').trimEnd();
|
| 272 |
if (!codePart) continue;
|