Clarkoer commited on
Commit
26393ae
·
1 Parent(s): 9f43027

fix grammar again

Browse files
.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 # track last matched topic for follow-ups
2585
- _last_query = "" # last user message for dedup
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), # None → return _DEFAULT_RESPONSE (the help menu)
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
- "", # sometimes no intro feels most natural
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
- "", # sometimes no outro
2784
  "",
2785
  ]
2786
 
2787
- # ── Conversation memory (per-process, lightweight) ──────────────────
2788
- _conv_history = [] # list of (query, topic_idx, score) tuples
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
- # Initial pass: Add terminals and epsilon that appear first in productions
37
- for lhs, productions in cfg.items(): # lhs = left-hand side (non-terminal)
38
- for prod in productions: # prod = production (right-hand side)
39
  if not prod:
40
  continue
41
- if prod[0] == epsilon: # Production derives epsilon directly
42
  first[lhs].add(epsilon)
43
- elif prod[0] not in cfg: # First symbol is a terminal (not a non-terminal)
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]) # Track size to detect changes
53
 
54
- # Process each symbol in the production
55
  for symbol in prod:
56
- if symbol in cfg: # Symbol is a non-terminal
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: # Symbol is a terminal
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
- # Start symbol gets end-of-input marker (EOF)
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: # Only compute FOLLOW for non-terminals
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: # Next symbol is non-terminal
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: # Next symbol is terminal
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: # Symbol is non-terminal
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: # Symbol is terminal
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>", # Global variables, constants, bundles (structs)
202
- "<function_definition>", # User-defined functions
203
- "root", # Main function name (like main() in C)
204
  "(",
205
  ")",
206
  "{",
207
- "<local_declaration>", # Old-style C local declarations must come first
208
- "<body_statement>", # Executable statements before the required final return
209
- "reclaim", # root() must end with 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>"], # Bundle definition or variable
219
- ["<data_type>", "id", "<array_dec>", "<var_value>", ";", "<global_declaration>"], # Variable declaration
220
- ["fertile", "<data_type>", "id", "=", "<init_val>", "<const_next>", ";", "<global_declaration>"], # Constant declaration
221
- [EPSILON], # Can be empty (no global declarations)
222
  ],
223
 
224
- # Distinguish between bundle definition and bundle variable after seeing "bundle id"
225
  "<bundle_or_var>": [
226
- ["{", "<bundle_members>", "}", ";"], # Bundle definition: bundle Person { ... };
227
- ["<bundle_mem_dec>", ";"], # Bundle variable: bundle Person p;
228
  ],
229
 
230
- # ===== LOCAL DECLARATIONS =====
231
- # Variables and constants declared inside functions
232
  "<local_declaration>": [
233
- ["<var_dec>", ";", "<local_declaration>"], # Variable/array/bundle-variable declaration
234
- ["<const_dec>", ";", "<local_declaration>"], # Constant declaration
235
- [EPSILON], # Can be empty (no local declarations)
236
  ],
237
 
238
- # ===== DATA TYPES =====
239
- # The five primitive types in GAL
240
  "<data_type>": [
241
- ["seed"], # Integer type
242
- ["tree"], # Float/Double type
243
- ["leaf"], # Character type
244
- ["branch"], # Boolean type
245
- ["vine"], # String type
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>"], # Multiple constants in one line
257
- [EPSILON], # Single constant
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>"], # Regular variable or array
267
- ["bundle", "id", "<bundle_mem_dec>"], # Struct variable
268
  ],
269
 
270
  "<bundle_mem_dec>": [
271
- ["id", "<array_dec>"], # bundle Person p; or bundle Person p[2];
272
  ],
273
 
274
  "<var_value>": [
275
- ["=", "<init_val>", "<var_value_next>"], # With initialization: seed x = 1, y = 2;
276
- ["<var_value_next>"], # Without initialization: seed x, y, z;
277
  ],
278
 
279
  "<var_value_next>": [
280
- [",", "id", "<array_dec>", "<var_value>"], # Multiple variables: seed x = 1, y = 2, z;
281
  [EPSILON],
282
  ],
283
 
284
- # ===== INITIALIZATION VALUES =====
285
  "<init_val>": [
286
- ["<array_init_opt>"], # Array initialization: seed arr[] = {1, 2, 3};
287
- ["water", "(", "<water_arg>", ")"], # Input: seed x = water(seed);
288
- ["<expression>"], # Single value: seed x = 5 + 3;
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>"], # One dimension, possibly more
298
- [EPSILON], # Not an array (base case)
299
  ],
300
 
301
  "<array_dim_opt>": [
302
- ["intlit"], # Size specified: [10]
303
- ["dblit"], # Float size (semantic error)
304
- [EPSILON], # Size not specified: []
305
  ],
306
 
307
- # ===== ARRAY INITIALIZATION =====
308
- # Examples: {1, 2, 3}
309
- # {{1, 2}, {3, 4}} (nested arrays)
310
  "<array_init_opt>": [
311
- ["{", "<init_vals>", "}"], # Array initializer
312
- [EPSILON], # No initialization
313
  ],
314
 
315
  "<init_vals>": [
316
- ["<init_val_item>", "<init_vals_next>"], # Start with first item (expression or nested init)
317
- [EPSILON], # Empty initializer
318
  ],
319
 
320
  "<init_vals_next>": [
321
- [",", "<init_val_item>", "<init_vals_next>"], # More items
322
- [EPSILON], # End of list
323
  ],
324
 
325
  "<init_val_item>": [
326
- ["{", "<init_vals>", "}"], # Nested initializer
327
- ["<expression>"], # Simple 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>"], # Primitive member fields
337
- ["id", "id", ";", "<bundle_members>"], # Nested bundle member: Address addr;
338
- [EPSILON], # Empty struct or end of members
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", # Keyword for function definition
348
- "<return_type>", # Return type (seed, tree, leaf, branch, or empty)
349
- "id", # Function name
350
  "(",
351
- "<parameters>", # Parameter list
352
  ")",
353
  "{",
354
- "<local_declaration>", # Old-style C local declarations must come first
355
- "<body_statement>", # Executable statements before the required final return
356
- "reclaim", # Every function must end with reclaim
357
- "<reclaim_value>", # Value validated against the declared return type
358
  "}",
359
- "<function_definition>", # Multiple functions (recursive definition)
360
  ],
361
- [EPSILON], # No more functions
362
  ],
363
 
364
- # ===== RETURN TYPE =====
365
  "<return_type>": [
366
- ["<data_type>"], # Returns seed, tree, leaf, or branch
367
- ["empty"], # Void function (no return value)
368
- ["id"], # User-defined bundle type (e.g., Pair, Person)
369
  ],
370
 
371
- # ===== FUNCTION PARAMETERS =====
372
- # Examples: (seed x, tree y)
373
- # () (no parameters)
374
  "<parameters>": [
375
- [EPSILON], # No parameters
376
- ["<param>", "<param_next>"], # One or more parameters
377
  ],
378
 
379
  "<param>": [
380
- ["<data_type>", "id", "<param_array>"], # Parameter: primitive type + name (optionally array)
381
- ["id", "id"], # Parameter: bundle type + name (e.g., Pair p)
382
  ],
383
 
384
  "<param_array>": [
385
- [EPSILON], # Scalar parameter
386
- ["[", "]"], # Array parameter: seed arr[]
387
  ],
388
 
389
  "<param_next>": [
390
- [EPSILON], # Last parameter
391
- [",", "<param>", "<param_next>"], # More parameters
392
  ],
393
 
394
- # ===== RETURN STATEMENT =====
395
- # Examples: reclaim x + y;
396
- # reclaim; (for empty functions)
397
  "<reclaim_value>": [
398
- ["<expression>", ";"], # Return with value
399
- [";"], # Return without value
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>"], # Assignment, unary, or function call
412
- ["<inc_dec_op>", "id", "<id_next>", ";"],# Prefix: ++x; ++arr[i]; ++obj.field;
413
- ["<io_stmt>"], # water() or plant()
414
- ["<conditional_stmt>"], # spring/bud/wither
415
- ["<loop_stmt>"], # grow/cultivate/tend
416
- ["<switch_stmt>"], # harvest
417
- ["<control_stmt>"], # prune/skip
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>"], # One statement followed by more
424
- [EPSILON], # No more statements (end of block)
425
  ],
426
 
427
- # ===== TYPES OF STATEMENTS =====
428
  "<simple_stmt>": [
429
- ["<non_reclaim_stmt>"], # Regular executable nested statement
430
- ["reclaim", "<reclaim_value>"], # Early return: reclaim x; or reclaim;
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>"], # Assignment OR postfix inc/dec on id/arr[i]/obj.f
439
- ["(", "<arguments>", ")", ";"], # Function call: func();
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>", ";"], # x = 5; arr[i] = 5; obj.f += 2;
447
- ["<inc_dec_op>", ";"], # x++; arr[i]++; obj.field--;
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>", ")"], # Input: x = water(seed);
458
- ["<expression>"], # Expression: x = 5 + 3;
459
  ],
460
 
461
- # Assignment operators
462
  "<assign_op>": [
463
- ["="], # Simple assignment
464
- ["+="], # Add and assign
465
- ["-="], # Subtract and assign
466
- ["*="], # Multiply and assign
467
- ["/="], # Divide and assign
468
- ["%="], # Modulo and assign
469
- ["**="], # Exponent and assign (x **= 2 → x = x ** 2)
470
  ],
471
 
472
  "<id_next>": [
473
- ["<array_access>", "<post_array_access>"], # Array indexing: arr[0] or arr[0].member
474
- ["<struct_access>"], # Struct member: person.name
475
- [EPSILON], # Just a simple variable: x
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>"], # Additional dimensions
487
- [EPSILON], # End of array indexing
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>"], # Nested member access
499
- [EPSILON], # End of member access
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>"], # Chained member access: p[0].addr.zip
505
- [EPSILON], # Just array access: arr[0]
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>", ")", ";"], # Output: plant("hello")
515
- ["water", "(", "<water_arg>", ")", ";"], # Input standalone: water(); or water(seed);
516
  ],
517
 
518
- # Optional type argument for water()
519
  "<water_arg>": [
520
- ["<data_type>"], # Typed input: water(seed), water(tree)
521
- ["id", "<water_id_tail>"], # Variable or list element: water(name) or water(arr[i])
522
- [EPSILON], # Untyped input: water()
523
  ],
524
 
525
  "<water_id_tail>": [
526
- ["[", "<expression>", "]", "<water_id_tail>"], # List element: water(arr[i]) or water(arr[i][j])
527
- [EPSILON], # Plain variable: water(name) / end of indices
528
  ],
529
 
530
- # Argument list for function calls and I/O
531
  "<arguments>": [
532
- ["<expression>", "<arg_next>"], # One or more arguments
533
- [EPSILON], # No arguments
534
  ],
535
 
536
  "<arg_next>": [
537
- [",", "<expression>", "<arg_next>"], # More arguments
538
- [EPSILON], # Last argument
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", # if keyword
558
  "(",
559
- "<expression>", # condition
560
  ")",
561
  "{",
562
- "<local_declaration>", # Declarations before executable statements
563
- "<statement>", # if body
564
  "}",
565
- "<elseif_chain>", # else-if chain (optional)
566
- "<else_opt>", # else clause (optional)
567
  ]
568
  ],
569
 
570
  "<elseif_chain>": [
571
  [
572
- "bud", # else-if keyword
573
  "(",
574
- "<expression>", # condition
575
  ")",
576
  "{",
577
- "<local_declaration>", # Declarations before executable statements
578
- "<statement>", # else-if body
579
  "}",
580
- "<elseif_chain>", # More else-ifs
581
  ],
582
- [EPSILON], # No else-if
583
  ],
584
 
585
  "<else_opt>": [
586
- ["wither", "{", "<local_declaration>", "<statement>", "}"], # else clause
587
- [EPSILON], # No else
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>", # Initialization
603
  ";",
604
- "<expression>", # Condition
605
  ";",
606
- "<for_update>", # 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>"], # Variable declaration: seed i = 0
620
- ["id", "<id_next>", "<assign_op>", "<expression>"], # Assignment: i = 0
621
- [EPSILON], # Empty initialization
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], # Empty update
630
  ],
631
 
632
  "<for_update_tail>": [
633
- ["<inc_dec_op>"], # i++ arr[i]++ obj.f--
634
- ["<assign_op>", "<expression>"], # i = i + 1 obj.f += 2
635
  ],
636
 
637
- # ===== UNARY STATEMENTS =====
638
- # Examples: x++; y--;
639
  "<inc_dec_op>": [
640
- ["++"], # Increment
641
- ["--"], # Decrement
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", # case keyword
662
- "<case_literal>", # case value (literal only, no expressions)
663
  ":",
664
- "<local_declaration>", # Declarations must precede executable case statements
665
- "<case_statements>", # zero or more executable statements (including prune)
666
- "<case_list>", # More cases
667
  ],
668
- [EPSILON], # No more cases
669
  ],
670
 
671
- # Only literal values are allowed after variety (case)
672
  "<case_literal>": [
673
- ["intlit"], # Integer literal (e.g., 1)
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>"], # One statement, then more
684
- [EPSILON], # No more statements (stops at variety/soil/})
685
  ],
686
 
687
- # Executable statement types allowed in a case body
688
  "<case_statement>": [
689
- ["id", "<id_stmt>"], # Assignment/increment/function call
690
- ["<inc_dec_op>", "id", "<id_next>", ";"], # Prefix: ++x; ++arr[i]; ++obj.f;
691
- ["<io_stmt>"], # water() or plant()
692
- ["<conditional_stmt>"], # spring (if), bud (else-if), wither (else)
693
- ["<loop_stmt>"], # grow (while), cultivate (for), tend (do-while)
694
- ["<switch_stmt>"], # harvest (nested switch)
695
- ["{", "<local_declaration>", "<statement>", "}"], # Nested block
696
- ["prune", ";"], # prune (break)
697
- ["skip", ";"], # skip (continue)
698
- ["reclaim", "<reclaim_value>"], # Early return: reclaim x; or reclaim;
699
  ],
700
 
701
  "<default_opt>": [
702
- ["soil", ":", "<local_declaration>", "<case_statements>"], # default case
703
- [EPSILON], # No default
704
  ],
705
 
706
- # ===== CONTROL FLOW STATEMENTS =====
707
  "<control_stmt>": [
708
- ["prune", ";"], # break
709
- ["skip", ";"], # continue
710
- ],
711
-
712
- # ===== FUNCTION CALL =====
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>"], # Right-associative assignment
738
- [EPSILON], # Ordinary expression
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>"], # More OR operations
749
- [EPSILON], # End
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>"], # More AND operations
760
- [EPSILON], # End
761
  ],
762
 
763
- # Level 3: Relational operators
764
- # Example: a > b, x == y
765
  "<relational>": [
766
- ["<arithmetic>", "<relational_next>"], # Arithmetic with optional comparison
767
  ],
768
 
769
  "<relational_next>": [
770
- ["<relational_op>", "<arithmetic>"], # Comparison operator
771
- [EPSILON], # Just arithmetic, no comparison
772
  ],
773
 
774
  "<relational_op>": [
775
- [">"], # Greater than
776
- ["<"], # Less than
777
- [">="], # Greater than or equal
778
- ["<="], # Less than or equal
779
- ["=="], # Equal
780
- ["!="], # Not equal
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>"], # Addition
791
- ["-", "<term>", "<arithmetic_next>"], # Subtraction
792
- ["`", "<term>", "<arithmetic_next>"], # String concatenation: vine/leaf operands produce vine
793
- [EPSILON], # End
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>"], # Multiplication
809
- ["/", "<power>", "<term_next>"], # Division
810
- ["%", "<power>", "<term_next>"], # Modulo
811
- [EPSILON], # End
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>"], # Exponentiation
822
- [EPSILON], # End
823
  ],
824
 
825
- # Level 7: Factor (highest precedence - literals, variables, etc.)
826
  "<factor>": [
827
- ["(", "<paren_expr>"], # Cast or parenthesized expression
828
- ["<unary_op>", "<factor>"], # Unary operation (e.g., ~x, !flag)
829
- ["id", "<factor_id_next>"], # Variable, array, struct, or function
830
- ["intlit"], # Integer literal (e.g., 42)
831
- ["dblit"], # Double/float literal (e.g., 3.14)
832
- ["chrlit"], # Character literal (e.g., 'a')
833
- ["stringlit"], # String literal (e.g., "hello")
834
- ["sunshine"], # Boolean true
835
- ["frost"], # Boolean false
836
- ],
837
-
838
- # Disambiguate after '(' — either a type cast or a grouped expression
839
- # No LL(1) conflict: FIRST(<data_type>) = {seed,tree,leaf,branch,vine}
840
- # FIRST(<expression>) = {(,~,!,id,intlit,dblit,...}
 
841
  "<paren_expr>": [
842
- ["<data_type>", ")", "<factor>"], # Type cast: (seed)x, (tree)3.14
843
- ["<expression>", ")"], # Parenthesized expression: (x + 1)
844
  ],
845
 
846
- # Unary operators
847
  "<unary_op>": [
848
- ["~"], # Arithmetic negation
849
- ["!"], # Logical NOT
850
  ],
851
 
852
- # For identifiers in expressions - could be variable, array, struct, or function
853
  "<factor_id_next>": [
854
- ["<array_access>", "<post_array_access>"], # Array element: arr[0] or arr[0].member
855
- ["<struct_access>"], # Struct member: person.name
856
- ["(", "<arguments>", ")"], # Function call: add(2, 3)
857
- [EPSILON], # Simple variable: x
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 # current position in token stream
143
- self.code: List[TACInstruction] = [] # generated TAC
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() # bundle
253
  name_tok = self._expect("id")
254
  nxt = self._peek()
255
  if nxt.type == "{":
256
- # bundle definition: bundle Id { members }
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() # data type
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() # data type keyword
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() # data type
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") # dynamic / unspecified
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() # pollinate
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() # skip '['
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
- """id <id_next> <assign_op> <expression> ; | id <inc_dec_op> ; | id ( <arguments> ) ;"""
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
- # compound assignment: x += e → x = x + e
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
- """water ( args ) ; | plant ( args ) ;"""
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: # water
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() # bud
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() # variety
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() # prune or skip
914
  self._expect(";")
915
  if tok.type == "prune":
916
- self._emit("GOTO", None, None, "BREAK") # placeholder; real label resolved later
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
- # INTERPRETER - Tree-walking executor for the validated AST
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
- # ---- Output state ----
81
- self.output = [] # legacy collector, kept for compat
82
- self.socketio = socketio # plant() uses this to emit 'output'
83
-
84
- # ---- Control-flow flags (set by break/continue, cleared by loops) ----
85
- self.loop_stack = [] # nested loop-type strings
86
- self.break_flag = False # set by 'prune'
87
- self.continue_flag = False # set by 'skip'
88
-
89
- # ---- Input plumbing for water() ----
90
- self.input_required = False # True while waiting for the client
91
- self.input_events = {} # var_name -> Event (eventlet/threading)
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
- # ---- Symbol tables ----
99
- self.variables = {} # global variables (visible everywhere)
100
- self.global_variables = {} # mirror used by declare_variable for dup-check
101
- self.functions = {} # pollinate definitions (name -> info)
102
- self.scopes = [{}] # scope stack; [0] is the global scope
103
- self.current_func_name = None # name of the function currently executing
104
- self.function_variables = {} # per-function locals (legacy, partially unused)
105
- self.bundle_types = {} # bundle (struct) layouts
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) # member name
536
- current = current.children[0] # go deeper
537
 
538
- chain.reverse() # now ordered from root to leaf
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
- #print(f"Removed value {removed} from list '{list_name}': {list_info['value']}")
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: # post
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: # post
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) # int -> ASCII character
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 # Runaway-loop safety net
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() # cooperative yield
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}: " # Create a prompt message for the input
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 # Reset the input flag
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 # the FSM + main entry point
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
- # --- CHARACTER CLASSES ---
10
- ZERO = '0' # Zero digit (special case for leading zeros)
11
- DIGIT = '123456789' # Non-zero digits
12
- ZERODIGIT = ZERO + DIGIT # All digits (0-9)
13
 
14
- LOW_ALPHA = 'abcdefghijklmnopqrstuvwxyz' # Lowercase letters
15
- UPPER_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' # Uppercase letters
16
- ALPHA = LOW_ALPHA + UPPER_ALPHA # All letters
17
- ALPHANUM = ALPHA + ZERODIGIT + '_' # Identifier characters
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 = {':'} # after 'soil' (default case)
24
- delim3 = {'{'} # after 'tend', 'wither'
25
- delim4 = {':', '('} # after 'bud', 'cultivate', 'harvest'
26
- delim5 = {'('} # after keywords requiring (
27
- delim6 = {';', ',', '=', '>', '<', '!', '}', ')', '('} # after 'spring', 'plant', 'water'
28
- delim7 = {'('} # after 'root'
29
- delim8 = {';'} # after statements needing ;
30
- delim9 = set(ALPHA + '(' + ',' + ';' + ')') # function-related contexts
31
- delim10 = {';', ')'} # closing statements
32
- delim11 = set([LOW_ALPHA, ZERODIGIT, ']', '~']) # newline-only
33
- delim12 = set(ALPHA + ZERODIGIT + ']' + '~') # array/negation contexts
34
- delim13 = {';', ')', '['} # mixed statement endings
35
- delim14 = set(ALPHA + ZERODIGIT + '"' + "'" + '{') # before literals/blocks
36
- delim15 = {'\n', ';', '}', ','} # multi-context endings
37
- delim16 = set(ALPHANUM + ')' + '"' + '!' + '(' + '[' + '\'') # expression contexts
38
- delim17 = {'}', ';', ',', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|'} # operator contexts
39
- delim18 = {';', '{', ')', '&', '|', '+', '-', '*', '/', '%'} # logical/arithmetic contexts
40
- delim19 = {';', ',', '}', ')', '=', '>', '<', '!'} # comparison contexts
41
- delim20 = set(ALPHA + ZERODIGIT + '"' + "'" + '{') # before string/char literals
42
- delim21 = set(DIGIT) # digit delimiters
43
- delim22 = {',', ';', '(', ')', '{', '[', ']'} # punctuation contexts
44
- delim23 = {';', ',', '}', ']', ')', ':', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|'} # after literals
45
- delim24 = set(ZERODIGIT + ALPHA + '~!(' + "\"'" + ' \t\n') # expression starts after + or +=
46
- delim25 = set(ALPHANUM + ';}) \t\n') # after prefix/postfix ++ or --
47
- delim26 = {'(', '['} | set(ALPHA) | {' ', '\t', '\n'} # { (, alphabet, [, whitespace }
48
  idf_delim = {' ', ',', ';', '(', ')', '{', '}', '[', ']', ':', '+', '-', '*', '/', '%',
49
- '>', '<', '=', '\t', '\n', '.', '"', "'"} # after identifiers
50
  whlnum_delim = {';', ' ', ',', '}', ']', ')', ':', '+', '-', '*', '/', '%', '=', '>', '<',
51
- '!', '&', '|', '\t', '\n'} # after integers
52
  decim_delim = {'}', ';', ',', '+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|', ' ',
53
- '\t', '\n', ')', ']'} # after floats/doubles
54
- comment_delim = set(ALPHANUM + ';+-*/%}{()' + '\n') # within comments
 
 
 
 
 
 
 
 
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 # Position object where error occurred
14
- self.details = details # Error description
15
 
16
  def as_string(self):
17
- """Returns the error message in readable string format"""
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 # Character index in source code (0-based)
15
- self.ln = ln # Line number (1-based)
16
- self.col = col # Column number (0-based)
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': # New line detected
24
  self.ln += 1
25
- self.col = 0 # Reset column to start of line
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', '') # Strip carriage returns (Windows \r\n → \n)
31
- self.pos = Position(-1, 1, -1) # Current position (starts before first char)
32
- self.current_char = None # Current character being examined
33
- self.advance() # Move to first character
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
- Main tokenization method - converts source code into tokens.
44
- Returns: (tokens, errors) tuple
45
- - tokens: list of Token objects
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
- # Main loop: drive the FSM one character at a time until source is exhausted (current_char becomes None).
54
- while self.current_char != None: # None sentinel = end of source_code reached
55
 
56
- # =====================================================================
57
- # KEYWORD & IDENTIFIER RECOGNITION
58
- # Hand-written transition diagram: each branch matches one keyword.
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
- # --- Letter B: 'branch', 'bud', 'bundle' ---
67
- if self.current_char == "b": # FSM root for words starting with 'b'
68
- ident_str += self.current_char # buffer the 'b'
69
- self.advance() # consume it; current_char now points to the 2nd letter
70
- if self.current_char == "r": # 'b'+'r' → potentially "branch"
71
- ident_str += self.current_char # buffer the 'r'
72
- self.advance() # consume 'r'; look at 3rd letter
73
- if self.current_char == "a": # 'br'+'a' → still on track for "branch"
74
- ident_str += self.current_char # buffer the 'a'
75
- self.advance() # consume 'a'
76
- if self.current_char == "n": # 'bra'+'n'
77
- ident_str += self.current_char # buffer the 'n'
78
- self.advance() # consume 'n'
79
- if self.current_char == "c": # 'bran'+'c'
80
- ident_str += self.current_char # buffer the 'c'
81
- self.advance() # consume 'c'
82
- if self.current_char == "h": # 'branc'+'h' → full keyword spelled out
83
- ident_str += self.current_char # buffer the trailing 'h'
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)) # emit reserved-word token
89
- continue # restart the outer while loop — do NOT fall through to identifier scan
90
- elif self.current_char not in ALPHANUM: # any non-alphanumeric (e.g. '(', '{') also closes the keyword
91
- tokens.append(Token(TT_RW_BRANCH, ident_str, line, pos.col)) # emit reserved-word token
92
- continue # restart the outer while loop
93
- elif self.current_char == "u": # 'b'+'u' → either "bud" or "bundle"
94
- ident_str += self.current_char # buffer the 'u'
95
- self.advance() # consume 'u'
96
- if self.current_char == "d": # 'bu'+'d' → potentially "bud"
97
- ident_str += self.current_char # buffer the 'd'
98
- self.advance() # consume 'd'; check what follows
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)) # emit BUD reserved-word token
102
- continue # restart outer loop on next char
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() # skip the bad char so we don't loop forever
107
- continue # back to outer scan
108
- elif self.current_char == "n": # 'bu'+'n' → potentially "bundle"
109
- ident_str += self.current_char # buffer 'n'
110
- self.advance() # consume 'n'
111
- if self.current_char == "d": # 'bun'+'d'
112
- ident_str += self.current_char # buffer 'd'
113
- self.advance() # consume 'd'
114
- if self.current_char == "l": # 'bund'+'l'
115
- ident_str += self.current_char # buffer 'l'
116
- self.advance() # consume 'l'
117
- if self.current_char == "e": # 'bundl'+'e' → "bundle" complete
118
- ident_str += self.current_char # buffer trailing 'e'
119
- self.advance() # consume 'e'
120
- if self.current_char is not None and self.current_char in space_delim: # bundle requires whitespace next
121
- tokens.append(Token(TT_RW_BUNDLE, ident_str, line, pos.col)) # emit BUNDLE token
122
- continue # next iteration
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() # skip bad char
127
- continue # restart outer scan
128
 
129
- #Letter C
130
- elif self.current_char == "c": # FSM root for words starting with 'c' (only "cultivate")
131
- ident_str += self.current_char # buffer 'c'
132
- self.advance() # consume 'c'
133
- if self.current_char == "u": # 'c'+'u' → on track for "cultivate"
134
- ident_str += self.current_char # buffer 'u'
135
- self.advance() # consume 'u'
136
- if self.current_char == "l": # 'cu'+'l'
137
- ident_str += self.current_char # buffer 'l'
138
- self.advance() # consume 'l'
139
- if self.current_char == "t": # 'cul'+'t'
140
- ident_str += self.current_char # buffer 't'
141
- self.advance() # consume 't'
142
- if self.current_char == "i": # 'cult'+'i'
143
- ident_str += self.current_char # buffer 'i'
144
- self.advance() # consume 'i'
145
- if self.current_char == "v": # 'culti'+'v'
146
- ident_str += self.current_char # buffer 'v'
147
- self.advance() # consume 'v'
148
- if self.current_char == "a": # 'cultiv'+'a'
149
- ident_str += self.current_char # buffer 'a'
150
- self.advance() # consume 'a'
151
- if self.current_char == "t": # 'cultiva'+'t'
152
- ident_str += self.current_char # buffer 't'
153
- self.advance() # consume 't'
154
- if self.current_char == "e": # 'cultivat'+'e' → "cultivate" complete
155
- ident_str += self.current_char # buffer trailing 'e'
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)) # emit CULTIVATE token
160
- continue # next iteration
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}'")) # illegal trailer
163
- self.advance() # skip bad char
164
- continue # restart outer scan
165
 
166
- # Letter E
167
- elif self.current_char == "e": # FSM root for 'e' (only "empty")
168
- ident_str += self.current_char # buffer 'e'
169
- self.advance() # consume 'e'
170
- if self.current_char == "m": # 'e'+'m' → potentially "empty"
171
- ident_str += self.current_char # buffer 'm'
172
- self.advance() # consume 'm'
173
- if self.current_char == "p": # 'em'+'p'
174
- ident_str += self.current_char # buffer 'p'
175
- self.advance() # consume 'p'
176
- if self.current_char == "t": # 'emp'+'t'
177
- ident_str += self.current_char # buffer 't'
178
- self.advance() # consume 't'
179
- if self.current_char == "y": # 'empt'+'y' → "empty" complete
180
- ident_str += self.current_char # buffer trailing 'y'
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)) # emit EMPTY token
185
- continue # next iteration
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}'")) # illegal trailer
188
- self.advance() # skip bad char
189
- continue # restart outer scan
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": # frost
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)) # Boolean Literal
209
  continue
210
- # If followed by alphanum/underscore, continue to identifier parsing
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": # grow
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": # harvest
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": # leaf
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": # plant
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": # pollinate
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": # prune
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": # reclaim
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": # root
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() # Skip the invalid character
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": # seed
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": # skip
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": # soil
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": # spring
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": # sunshine
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)) # Boolean Literal
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": # tend
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": # tree
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": # vine
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": # variety
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": # water
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": # wither
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
- #Identifier
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() # Consume invalid delimiter
729
- continue # Don't add token - eat the error
730
  tokens.append(Token(TT_MINUS, ident_str, line, pos.col))
731
  continue
732
 
733
- elif self.current_char == "~": # Added for negative prefix
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() # Consume invalid delimiter
824
- continue # Don't add token - eat the error
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() # Consume invalid delimiter
881
- continue # Don't add token - eat the error
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() # Consume invalid delimiter
979
- continue # Don't add token - eat the error
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 == "=": # Check for second '='
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 == "/": # Single-line comment: // comment text
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 == "*": # Multi-line comment: /* comment text */
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() # Skip *
1101
- self.advance() # Skip /
1102
  found_close = True
1103
  break
1104
  else:
1105
  ident_str += self.current_char
1106
- if self.current_char == "\n": # Track line numbers inside comment
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() # Consume invalid delimiter
1126
- continue # Don't add token - eat the error
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 # Count decimal points (max 1)
1178
- ident_str = "" # Build the number string
1179
- pos = self.pos.copy() # Save position for errors
1180
- integer_digit_count = 0 # Count digits before decimal (max 15)
1181
- fractional_digit_count = 0 # Count digits after decimal (max 8)
1182
- has_e = False # Track if scientific notation (e.g., 1e10)
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 # Mark that we found a decimal point
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: # Float case
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: # Only re-format if not scientific notation
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" # Keep at least one zero
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 = '' # Full string including quotes
1391
- pos = self.pos.copy() # Save position for error reporting
1392
- escape_character = False # Track if we're processing an escape sequence
1393
- string += self.current_char # Include opening quote
1394
  self.advance()
1395
 
1396
- # Supported escape sequences
1397
  escape_characters = {
1398
- 'n': '\n', # Newline
1399
- 't': '\t', # Tab
1400
- '{': '\\{', # Literal left brace
1401
- '}': '\\}', # Literal right brace
1402
- '"': '"', # Escaped double quote
1403
- '\\': '\\', # Escaped backslash
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 = '' # Full text including quotes (for display)
1458
- char = '' # Actual character value
1459
  pos = self.pos.copy()
1460
- string += self.current_char # Include opening quote
1461
  self.advance()
1462
- has_error = False # Track if an error was already reported
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': # Newline breaks character literal
1472
  break
1473
- elif self.current_char == '\\': # Escape sequence
1474
- # Handle escape sequences for characters
1475
  string += self.current_char
1476
  self.advance()
1477
  if self.current_char is None:
1478
- break # Will be caught as a missing closing quote
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 # Return tuple: (list of tokens, list of 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 = {} # Stores variables
60
- symbol_table.functions = {} # Stores function definitions
61
  symbol_table.scopes = [{}]
62
  symbol_table.function_variables = {}
63
- symbol_table.bundle_types = {} # Reset bundle type definitions
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 # skip 'bundle' and bundle name
151
 
152
  if tokens[index].type == "{":
153
- # Bundle definition: bundle Person { seed age; vine name; }
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 # skip type and name
164
  if tokens[index].type == ";":
165
- index += 1 # skip ';'
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 # skip type and name
174
  if tokens[index].type == ";":
175
- index += 1 # skip ';'
176
  else:
177
  raise SemanticError(f"Semantic Error: Invalid member type '{tokens[index].value}' in bundle definition.", tokens[index].line)
178
- index += 1 # skip '}'
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 # consume keyword
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 # skip '['
296
  if tokens[index].type != "]":
297
  raise SemanticError(f"Semantic Error: Expected ']' after '[' in array parameter.", line)
298
- index += 1 # skip ']'
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 # skip 'water'
405
  if tokens[index].type != "(":
406
  raise SemanticError(f"Semantic Error: Expected '(' after water.", water_line)
407
- index += 1 # skip '('
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 # skip ')'
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 # skip '['
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 # skip size
437
  if tokens[index].type != "]":
438
  raise SemanticError(f"Syntax Error: Expected ']' after list size.", line)
439
- index += 1 # skip ']'
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 # skip '='
461
  if tokens[index].type == "{":
462
- index += 1 # skip '{'
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 # skip '}'
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 # skip 'bundle', type name, var name
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
- # Array of bundles: bundle Point p[2];
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 # skip size
561
  if tokens[index].type != "]":
562
  raise SemanticError(f"Syntax Error: Expected ']' after array size.", token.line)
563
- index += 1 # skip ']'
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
- # Bundle array member assignment: p[0].x = 1; or p[0].addr.zip = 1;
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 # skip member name
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 # skip '.', member
647
 
648
  if tokens[index].type == "=":
649
- index += 1 # skip '='
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 # skip 'water'
671
  if tokens[index].type != "(":
672
  raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
673
- index += 1 # skip '('
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 # skip ')'
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 # skip id, '.', member
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 # skip '.', member
723
 
724
  if tokens[index].type == "=":
725
- index += 1 # skip '='
726
- # Handle water() input for bundle members
727
  if tokens[index].value == "water":
728
  water_line = tokens[index].line
729
- index += 1 # skip 'water'
730
  if tokens[index].type != "(":
731
  raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
732
- index += 1 # skip '('
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 # skip ')'
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 # skip compound op
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 # skip ++/--
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
- # Compound assignment: x += 5 => x = x + 5
793
- compound_op = tokens[index + 1].value # e.g. '+='
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 # advance past the trailing ']'
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 # skip id, '.', member
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
- # Bare block: { ... }
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 # skip '}'
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 # skip ']' and '['
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): # Function not found
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 # skip '['
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 # skip ']'
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): # Function not found
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 # skip id, '.', member
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 # skip '.', member
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 # skip '['
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 # skip ']'
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 # skip '.'
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 # skip member name
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 # skip '.', member
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 # skip 'water'
2093
  if tokens[index].type != "(":
2094
  raise SemanticError(f"Syntax Error: Expected '(' after water.", water_line)
2095
- index += 1 # skip '('
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 # skip ')'
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 # Get expected type
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 # skip 'water'
2183
 
2184
  if tokens[index].type != "(":
2185
  raise SemanticError(f"Syntax Error: Expected '(' after water.", line)
2186
- index += 1 # skip '('
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 # skip ')'
2195
  if tokens[index].type != ";":
2196
  raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
2197
- index += 1 # skip ';'
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
- # water() with no args
2204
- index += 1 # skip ')'
2205
  if tokens[index].type != ";":
2206
  raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
2207
- index += 1 # skip ';'
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 # skip identifier
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 # skip '['
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 # skip ']'
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 # skip '['
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 # skip ']'
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 # skip ')'
2255
  if tokens[index].type != ";":
2256
  raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
2257
- index += 1 # skip ';'
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 # skip ')'
2265
  if tokens[index].type != ";":
2266
  raise SemanticError(f"Syntax Error: Expected ';' after water statement.", line)
2267
- index += 1 # skip ';'
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 # Move past "spring"
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) # Parse branch expression
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 == "(": # Function call case
2761
  func_info = symbol_table.lookup_function(identifier)
2762
 
2763
- if isinstance(func_info, str): # Function not found
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: # Variable case
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 # past ']' and ++/--
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 # advance past the trailing ']'
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 # skip id, '.', member
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 # track the type of the harvest expression
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" # default for grouped expressions
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() # track variety values to detect duplicates
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 # reclaim already present in source; omit from suggestions
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 # skip non-terminal names
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: # Previous token on different 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: # token_type == '}'
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 # skip 'prune'
902
  break
903
  index += 1
904
  if index < len(toks) and toks[index].type == ';':
905
- index += 1 # skip ';'
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'}, # seed = integer, also accepts double (truncated)
931
- 'tree': {'dblit', 'intlit'}, # tree = double, also accepts integer (promoted)
932
- 'leaf': {'chrlit'}, # leaf = char (single character only, validated below)
933
- 'branch': {'sunshine', 'frost'}, # branch = boolean (sunshine=true, frost=false only)
934
- 'vine': {'stringlit'} # vine = string only
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 # literal is part of a comparison expression (boolean)
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 # literal is part of a comparison expression (boolean)
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 # Use prev_tok line for deduplication
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 = {} # Stores variables
15
- self.global_variables = {} # Stores global variables
16
- self.functions = {} # Stores function definitions
17
- self.scopes = [{}] # Stack of scopes (for local/global tracking)
18
  self.current_func_name = None
19
  self.function_variables = {}
20
- self.bundle_types = {} # Stores bundle (struct) type definitions
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 # AI chat helper (optional)
34
- from lexer import lex, get_token_description # Stage 1: Lexical
35
- from parser import LL1Parser # Stage 2: Syntax (LL(1))
36
- from cfg import cfg, first_sets, predict_sets # Grammar + parse table
37
- from parser.builder import analyze_semantics # Stage 3a: legacy entry
38
- from semantic import validate_ast # Stage 3b: Semantic validator
39
- from icg import generate_icg # Stage 4: ICG (display)
40
- from interpreter import Interpreter, InterpreterError, _CancelledError # Stage 5: Run
41
- from ai import fallback_reply # Rule-based AI fallback
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) # Allow browser frontend on any origin (dev)
64
- socketio = SocketIO(app, cors_allowed_origins="*") # WebSocket layer for live program execution
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 # the global SocketIO instance
79
- self._sid = sid # this client's session ID
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, # Grammar productions
92
- predict_sets=predict_sets, # Pre-computed parse table
93
- first_sets=first_sets, # FIRST sets (for error recovery hints)
94
- start_symbol="<program>", # Top of the GAL grammar
95
- end_marker="EOF", # Matches the synthetic EOF token from the lexer
96
- skip_token_types={'\n'} # Newlines are filtered before the parser sees them
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 = [] # Accumulated plant() output strings
432
- self.needs_input = False # Flips to True if program calls water()
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) # eventlet.event.Event uses .send()
612
  except (AttributeError, AssertionError):
613
  try:
614
- evt.set() # threading.Event fallback
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 # Old interpreter was cancelled by a new run — exit silently
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 = {} # sid -> list of message dicts
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 # success
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 # unexpected error, don't retry
743
  continue
744
  else:
745
- raise last_error # all models failed
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 # fallback itself failed, return original error
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
- # AST NODE CLASSES - The shape of every node in the GAL syntax tree
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 # Type of node (e.g., 'VariableDeclaration', 'BinaryOp')
31
- self.value = value # E.g. variable name, operator, etc.
32
- self.children = [] # List of child nodes
33
- self.parent = None # Reference to parent node
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) # Nested MemberAccessNode for chained access
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 # dict: {member_name: member_type}
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
- # --- Reserved Words (Keywords) ---
4
- TT_RW_WATER = 'water' # Input function - reads user input
5
- TT_RW_PLANT = 'plant' # Output function - prints to console
6
- TT_RW_SEED = 'seed' # Data Type - integer (int)
7
- TT_RW_LEAF = 'leaf' # Data Type - character (char)
8
- TT_RW_BRANCH = 'branch' # Data Type - boolean (true/false)
9
- TT_RW_TREE = 'tree' # Data Type - double/float
10
- TT_RW_SPRING = 'spring' # Conditional statement - if
11
- TT_RW_WITHER = 'wither' # Conditional statement - else
12
- TT_RW_BUD = 'bud' # Conditional statement - else-if
13
- TT_RW_HARVEST = 'harvest' # Switch statement
14
- TT_RW_GROW = 'grow' # Loop - while
15
- TT_RW_CULTIVATE = 'cultivate' # Loop - for
16
- TT_RW_TEND = 'tend' # Loop - do-while
17
- TT_RW_EMPTY = 'empty' # Void return type
18
- TT_RW_PRUNE = 'prune' # Break statement - exit loop
19
- TT_RW_SKIP = 'skip' # Continue statement - skip to next iteration
20
- TT_RW_RECLAIM = 'reclaim' # Return statement - return from function
21
- TT_RW_ROOT = 'root' # Main function entry point
22
- TT_RW_POLLINATE = 'pollinate' # Function declaration
23
- TT_RW_VARIETY = 'variety' # Case label in switch statement
24
- TT_RW_FERTILE = 'fertile' # Constant declaration
25
- TT_RW_SOIL = 'soil' # Default case in switch statement
26
- TT_RW_BUNDLE = 'bundle' # Struct definition
27
- TT_RW_VINE = 'vine' # String data type
28
-
29
- # --- Operators & Symbols ---
30
- TT_IDENTIFIER = 'id' # Variable/function names (e.g., myVar, calcTotal)
31
- TT_PLUS = '+' # Addition operator
32
- TT_MINUS = '-' # Subtraction operator
33
- TT_MUL = '*' # Multiplication operator
34
- TT_DIV = '/' # Division operator
35
- TT_MOD = '%' # Modulo operator (remainder)
36
- TT_EXP = '**' # Exponentiation operator (power)
37
- TT_EQ = '=' # Assignment operator
38
- TT_EQTO = '==' # Equality comparison operator
39
- TT_PLUSEQ = '+=' # Add and assign operator
40
- TT_MINUSEQ = '-=' # Subtract and assign operator
41
- TT_MULTIEQ = '*=' # Multiply and assign operator
42
- TT_DIVEQ = '/=' # Divide and assign operator
43
- TT_MODEQ = '%=' # Modulo and assign operator
44
- TT_EXPEQ = '**=' # Exponent and assign operator (x **= 2 → x = x ** 2)
45
- TT_CONCAT = '`' # String concatenation operator
46
- TT_LPAREN = '(' # Left parenthesis
47
- TT_RPAREN = ')' # Right parenthesis
48
- TT_SEMICOLON = ';' # Statement terminator
49
- TT_COMMA = ',' # Separator (function args, array elements)
50
- TT_COLON = ':' # Colon (used in switch cases)
51
- TT_BLOCK_START = '{' # Block start (scope begin)
52
- TT_BLOCK_END = '}' # Block end (scope close)
53
- TT_LT = '<' # Less than comparison
54
- TT_GT = '>' # Greater than comparison
55
- TT_LTEQ = '<=' # Less than or equal comparison
56
- TT_GTEQ = '>=' # Greater than or equal comparison
57
- TT_NOTEQ = '!=' # Not equal comparison
58
- TT_EOF = 'EOF' # End of file marker
59
- TT_AND = '&&' # Logical AND operator
60
- TT_OR = '||' # Logical OR operator
61
- TT_SINGLE_AND = '&' # Invalid single ampersand
62
- TT_SINGLE_OR = '|' # Invalid single pipe
63
- TT_NOT = '!' # Logical NOT operator
64
- TT_INCREMENT = '++' # Increment operator (e.g., x++)
65
- TT_DECREMENT = '--' # Decrement operator (e.g., x--)
66
- TT_LSQBR = '[' # Left square bracket (array indexing)
67
- TT_RSQBR = ']' # Right square bracket
68
- TT_NEGATIVE = '~' # Unary negation operator
69
- TT_MEMBER = 'member' # Member token for struct access
70
- TT_INTEGERLIT = 'intlit' # Integer literal token (e.g., 42, 100)
71
- TT_DOUBLELIT = 'dbllit' # Double/float literal token (e.g., 3.14, 2.5)
72
- TT_STRINGLIT = 'stringlit' # String literal token (e.g., "hello")
73
- TT_CHARLIT = 'chrlit' # Character literal token (e.g., 'a')
74
- TT_BOOLLIT_TRUE = 'sunshine' # Boolean true literal
75
- TT_BOOLLIT_FALSE = 'frost' # Boolean false literal
76
- TT_STRCTACCESS = '.' # Struct member access operator
77
- TT_NL = '\n' # Newline token
78
- TT_DOT = '.' # Dot operator (struct access)
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_ # Token type (e.g., TT_IDENTIFIER, TT_INTEGERLIT)
89
- self.value = value # Token text/value (e.g., "myVar", "42")
90
- self.line = line # Line number where token appears
91
- self.col = col # Column number where token starts (0-based)
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('//') || trimmed.startsWith('#')) continue;
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;