diff --git a/.env.example b/.env.example index 54d326c..acad787 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,7 @@ # AI panel backend config. Copy to `.env` and fill in if overriding defaults. # Both vars are optional — they default to the shared dev server on torta-server. -# k12-llm-backend URL (defaults to https://k12api.torta-server.duckdns.org) VITE_BACKEND_URL= - -# Keycloak URL (defaults to https://auth.torta-server.duckdns.org) VITE_KEYCLOAK_URL= +VITE_KEYCLOAK_REALM= +VITE_KEYCLOAK_CLIENT_ID= diff --git a/csv/praxly.test.csv b/csv/praxly.test.csv index 0cb6dc0..44f3c29 100644 --- a/csv/praxly.test.csv +++ b/csv/praxly.test.csv @@ -4,7 +4,7 @@ Print twice,"print ""1st"" print ""2nd""",,"1st 2nd", Declare integer,"int x -print x",,0, +print x",,,Runtime Error: Uninitialized variable 'x' Assign integer,"int x <- 5 print x",,5, Declare assign,"int x diff --git a/docs/AST_REFERENCE.md b/docs/AST_REFERENCE.md index 21293f0..d393c24 100644 --- a/docs/AST_REFERENCE.md +++ b/docs/AST_REFERENCE.md @@ -6,7 +6,7 @@ Every AST node type defined in [`src/language/ast.ts`](../src/language/ast.ts), - **Statement / declaration nodes** are dispatched by `ASTVisitor.visitStatement()` to an abstract `visitX()` method that every emitter (`java`, `python`, `csp`, `praxis`, `javascript`) must implement. - **Expression nodes** have no `visitX()` counterpart — they're handled inside each emitter's own `generateExpression(expr, parentPrecedence)` switch statement instead. - **Nested/helper nodes** (`Parameter`, `SwitchCase`, `ExceptionHandler`) aren't dispatched on their own, and aren't part of the `Statement` union either — they're plain data read directly by the parent node's visit method (e.g. `visitSwitch` iterates `stmt.cases`). -- **Comments** travel with the AST rather than being discarded: any node may carry `leadingComments` (own-line comment lines directly above it) and `trailingComment` (an inline comment on the same line), and `Program` carries `headerComments` (the file-top block). Lexers capture single-line comments, a shared pass attaches them by source position, and each emitter re-emits them in the target's comment delimiter (`//` or Python `#`), so comments survive translation. +- **Comments** travel with the AST rather than being discarded: any node may carry `leadingComments` (own-line comment lines directly above it, including a file-top comment block, which attaches to the first statement) and `trailingComment` (an inline comment on the same line). Lexers capture single-line comments, a shared pass attaches them by source position, and each emitter re-emits them in the target's comment delimiter (`//` or Python `#`), so comments survive translation. | Node Type | Visitor Method | Explanation | | --------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/examples/README.md b/examples/README.md index c5660cf..07fe0c2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -44,3 +44,81 @@ npm run test:run -- tests/examples.test.ts `tests/examples.test.ts` parses, interprets, and translates every demo as a regression guard, so the files stay runnable as the codebase evolves. + +## Language-Specific Notes + +### CSP + +CSP is a small procedural language, so it reaches fewer AST nodes than the +other demos. + +AST nodes NOT reachable from CSP source (covered by the other demos): +- Classes (ClassDeclaration/Constructor/Method/Field), NewExpression, +- MemberExpression, ThisExpression .... CSP has no usable OOP. +- DoWhile / RepeatUntil node .......... `REPEAT UNTIL` maps to While(NOT c). +- Switch / Try / Break / Continue ..... no such keywords. +- ConditionalExpression (ternary) ..... not parsed. +- UpdateExpression (++/--) ............ not in CSP. +- CompoundAssignment (+=) ............. write `x ← x + 1`. + +Interpreter notes: +- Assignment is `←` (ASCII `<-` also accepted); `=` means EQUALITY. +- Lists are 1-based (AP CSP). +- `REPEAT UNTIL(c)` is a PRE-condition loop (runs while NOT c). +- There is no counting FOR: use a counter + REPEAT UNTIL, or REPEAT n TIMES. +- There is no unary minus -> write `0 - x`. There is no `null`. +- DISPLAY prints its argument followed by a SPACE (no newline), per AP CSP, + so all output below lands on a single space-separated line. + +### Java + +AST nodes NOT reachable from Java source (covered by the other demos): +- FunctionDeclaration ..... Java has only methods, never free functions. +- RepeatUntil ............. no repeat/until syntax. +- NewExpression ........... `new X(...)` is encoded as a CallExpression. +- ThisExpression .......... `this` is encoded as an Identifier. + +Interpreter notes: +- System.out.println takes exactly ONE argument -> concatenate with '+'. +- A C-style for-loop update should be `i++` (an embedded plain assignment + like `i = i + 1` is a no-op). +- Scanner reads System.in, which the auto-runner can't supply, so Scanner is + exercised by the Java tests (with provided input) rather than here. + +The executable code lives in Main.main (auto-invoked by the interpreter); +the helper classes it uses are declared afterwards. + +### JavaScript + +AST nodes NOT reachable from JavaScript source (covered by the other demos): +- RepeatUntil ......... no repeat/until syntax. + +Interpreter notes: +- Output is console.log(...) (multiple args are space-joined). +- Standard JS: arrays use .push, conversions use parseInt/parseFloat/String, + integer division uses Math.trunc, .length gives length. +- `let`/`const`/`var` all behave identically (no block scoping enforced). + +### Praxis + +Praxis maps most directly to the Universal AST, so it reaches the widest +set of nodes. + +AST nodes NOT reachable from Praxis source (covered by the other demos): +- Switch / SwitchCase .. no switch/case keywords. +- ConditionalExpression no ?: ternary operator. +- CompoundAssignment ... no +=/-=; write `x ← x + 1`. +- ForEach .............. no for-each; Praxis has only the C-style for loop. + +Functions and classes are hoisted, so the driver calls them before their +declarations appear further down the file. + +### Python + +AST nodes NOT reachable from Python source (covered by the other demos): +- DoWhile / RepeatUntil ... no do-while / repeat syntax. +- Switch / SwitchCase ..... no match/switch parsing. +- UpdateExpression ........ no ++ / -- in Python. +- CompoundAssignment ...... += etc. desugar to Assignment + BinaryExpression. +- NewExpression ........... instantiation is a CallExpression, e.g. Dog("x"). +- ThisExpression .......... `self` is an ordinary Identifier. diff --git a/examples/demo.csp b/examples/demo.csp index 331a5d3..c0245d0 100644 --- a/examples/demo.csp +++ b/examples/demo.csp @@ -1,51 +1,25 @@ -// =========================================================================== -// Praxly2 feature demo -- CSP (shared notes: examples/README.md) -// --------------------------------------------------------------------------- -// CSP is a small procedural language, so it reaches fewer AST nodes than the -// other demos. -// -// AST nodes NOT reachable from CSP source (covered by the other demos): -// Classes (ClassDeclaration/Constructor/Method/Field), NewExpression, -// MemberExpression, ThisExpression .... CSP has no usable OOP. -// DoWhile / RepeatUntil node .......... `REPEAT UNTIL` maps to While(NOT c). -// Switch / Try / Break / Continue ..... no such keywords. -// ConditionalExpression (ternary) ..... not parsed. -// UpdateExpression (++/--) ............ not in CSP. -// CompoundAssignment (+=) ............. write `x <- x + 1`. -// -// CSP runtime facts honored below: -// * Assignment is `<-` (or the unicode arrow); `=` means EQUALITY. -// * Lists are 1-based (AP CSP). -// * `REPEAT UNTIL(c)` is a PRE-condition loop (runs while NOT c). -// * There is no counting FOR: use a counter + REPEAT UNTIL, or REPEAT n TIMES. -// * There is no unary minus -> write `0 - x`. There is no `null`. -// * DISPLAY prints its argument followed by a SPACE (no newline), per AP CSP, -// so all output below lands on a single space-separated line. -// =========================================================================== - - -// ========================== EXPRESSIONS ==================================== - -// ---- Literal (number, string, boolean) + Assignment + DISPLAY ------------- -name <- "csp" -greeting <- "hello " + name // BinaryExpression '+' (string concatenation) +// ========================== EXPRESSIONS ====================================== + +// ---- Literal (number, string, boolean) + Assignment + DISPLAY --------------- +name ← "csp" +greeting ← "hello " + name // BinaryExpression '+' (string concatenation) DISPLAY(greeting) // hello csp -flag <- true +flag ← true DISPLAY(flag) // true -// ---- BinaryExpression: arithmetic + MOD (division is floating point) ------- -total <- 3 + 4 * 2 +// ---- BinaryExpression: arithmetic + MOD (division is floating point) -------- +total ← 3 + 4 * 2 DISPLAY(total) // 11 DISPLAY(17 MOD 5) // 2 DISPLAY(10 / 4) // 2.5 -// ---- No unary minus in CSP -> subtract from zero --------------------------- -neg <- 0 - 7 +// ---- No unary minus in CSP -> subtract from zero ---------------------------- +neg ← 0 - 7 DISPLAY(neg) // -7 -// ---- Comparison + logical (AND / OR / NOT) incl. unicode relational forms -- -x <- 5 +// ---- Comparison + logical (AND / OR / NOT) incl. unicode relational forms --- +x ← 5 DISPLAY(x >= 5 AND x < 10) // true DISPLAY(x = 5 OR x = 6) // true DISPLAY(NOT (x = 3)) // true (UnaryExpression NOT) @@ -54,76 +28,76 @@ DISPLAY(x ≠ 3) // true (unicode not-equal) DISPLAY(x ≤ 5) // true (unicode less-or-equal) DISPLAY(x ≥ 5) // true (unicode greater-or-equal) -// ---- ArrayLiteral + IndexExpression (1-based) + index Assignment ----------- -nums <- [10, 20, 30] +// ---- ArrayLiteral + IndexExpression (1-based) + index Assignment ------------ +nums ← [10, 20, 30] DISPLAY(nums[1]) // 10 (first element is index 1) -nums[2] <- 99 +nums[2] ← 99 DISPLAY(nums[2]) // 99 -// ---- List builtins: APPEND / INSERT / REMOVE / LENGTH (1-based positions) -- +// ---- List builtins: APPEND / INSERT / REMOVE / LENGTH (1-based positions) --- APPEND(nums, 40) // [10, 99, 30, 40] INSERT(nums, 1, 5) // [5, 10, 99, 30, 40] (insert before position 1) REMOVE(nums, 2) // [5, 99, 30, 40] (remove position 2, the 10) DISPLAY(LENGTH(nums)) // 4 DISPLAY(nums[1]) // 5 -// ---- String functions: CONCAT / SUBSTRING / CHARAT / len (1-based) --------- -word <- "algorithm" +// ---- String functions: CONCAT / SUBSTRING / CHARAT / len (1-based) ---------- +word ← "algorithm" DISPLAY(CONCAT("al", "go")) // algo DISPLAY(SUBSTRING(word, 1, 4)) // algo (characters 1..4, inclusive) DISPLAY(CHARAT(word, 1)) // a (the first character) DISPLAY(len(word)) // 9 -// ========================== STATEMENTS ===================================== +// ========================== STATEMENTS ======================================= -// ---- IF / ELSE IF / ELSE --------------------------------------------------- -score <- 82 +// ---- IF / ELSE IF / ELSE ---------------------------------------------------- +score ← 82 IF (score >= 90) { DISPLAY("A") } ELSE IF (score >= 80) { - DISPLAY("B") // this runs + DISPLAY("B") // this runs } ELSE { DISPLAY("C") } -// ---- REPEAT UNTIL(c) -> pre-condition While(NOT c) ------------------------- -i <- 0 +// ---- REPEAT UNTIL(c) -> pre-condition While(NOT c) -------------------------- +i ← 0 REPEAT UNTIL (i >= 3) { DISPLAY(i) // 0 1 2 - i <- i + 1 + i ← i + 1 } -// ---- REPEAT n TIMES -> count-based For ------------------------------------- +// ---- REPEAT n TIMES -> count-based For -------------------------------------- REPEAT 3 TIMES { DISPLAY("tick") // tick tick tick } -// ---- Counting loop: counter + REPEAT UNTIL (CSP has no FOR FROM/TO) -------- -j <- 0 +// ---- Counting loop: counter + REPEAT UNTIL (CSP has no FOR FROM/TO) --------- +j ← 0 REPEAT UNTIL (j >= 6) { DISPLAY(j) // 0 2 4 - j <- j + 2 + j ← j + 2 } -// ---- FOR EACH item IN list ------------------------------------------------- +// ---- FOR EACH item IN list -------------------------------------------------- FOR EACH item IN nums { - DISPLAY(item) // 5 99 30 40 + DISPLAY(item) // 5 99 30 40 } -// ========================== FUNCTIONS ====================================== +// ========================== FUNCTIONS ======================================== -// ---- FunctionDeclaration (PROCEDURE) + Parameter + RETURN ------------------ +// ---- FunctionDeclaration (PROCEDURE) + Parameter + RETURN ------------------- PROCEDURE add(x, y) { RETURN x + y @@ -136,17 +110,17 @@ PROCEDURE isEven(n) PROCEDURE printEvens(limit) { - i <- 0 + i ← 0 REPEAT UNTIL (i >= limit) // 0 .. limit-1 { IF (isEven(i)) { DISPLAY(i) } - i <- i + 1 + i ← i + 1 } } -// ---- Procedure calls (CallExpression / ExpressionStatement) ---------------- -DISPLAY(add(4, 5)) // 9 -printEvens(6) // 0 2 4 +// ---- Procedure calls (CallExpression / ExpressionStatement) ----------------- +DISPLAY(add(4, 5)) // 9 +printEvens(6) // 0 2 4 diff --git a/examples/demo.java b/examples/demo.java index 2477aea..205fbb6 100644 --- a/examples/demo.java +++ b/examples/demo.java @@ -1,30 +1,9 @@ -// =========================================================================== -// Praxly2 feature demo -- JAVA (shared notes: examples/README.md) -// --------------------------------------------------------------------------- -// AST nodes NOT reachable from Java source (covered by the other demos): -// FunctionDeclaration ..... Java has only methods, never free functions. -// RepeatUntil ............. no repeat/until syntax. -// NewExpression ........... `new X(...)` is encoded as a CallExpression. -// ThisExpression .......... `this` is encoded as an Identifier. -// -// Interpreter notes honored below: -// * System.out.println takes exactly ONE argument -> concatenate with '+'. -// * A C-style for-loop update should be `i++` (an embedded plain assignment -// like `i = i + 1` is a no-op). -// * Scanner reads System.in, which the auto-runner can't supply, so Scanner is -// exercised by the Java tests (with provided input) rather than here. -// -// The executable code lives in Main.main (auto-invoked by the interpreter); -// the helper classes it uses are declared afterwards. -// =========================================================================== - - public class Main { public static void main(String[] args) { - // ========================= EXPRESSIONS ========================= + // ========================= EXPRESSIONS =============================== - // ---- Literal (int, double, String, boolean, null) + println -------- + // ---- Literal (int, double, String, boolean, null) + println --------- int whole = 42; double frac = 3.14; String label = "java"; @@ -36,12 +15,12 @@ public static void main(String[] args) { System.out.println(flag); // true System.out.println(nothing); // None - // ---- Bare declaration (declaredWithoutInitializer) then assign ----- + // ---- Bare declaration (declaredWithoutInitializer) then assign ------ int later; later = 100; System.out.println(later); // 100 - // ---- BinaryExpression: arithmetic (int division truncates) --------- + // ---- BinaryExpression: arithmetic (int division truncates) ---------- int a = 17; int b = 5; System.out.println(a + b); // 22 @@ -50,7 +29,7 @@ public static void main(String[] args) { System.out.println(a / b); // 3 System.out.println(a % b); // 2 - // ---- CompoundAssignment (+= -= *= /= %=) --------------------------- + // ---- CompoundAssignment (+= -= *= /= %=) ---------------------------- int acc = 10; acc += 5; // 15 acc -= 3; // 12 @@ -59,7 +38,7 @@ public static void main(String[] args) { acc %= 4; // 2 System.out.println(acc); // 2 - // ---- UpdateExpression (++ --), UnaryExpression (-, !) -------------- + // ---- UpdateExpression (++ --), UnaryExpression (-, !) --------------- int i = 5; i++; --i; @@ -67,15 +46,15 @@ public static void main(String[] args) { System.out.println(-a); // -17 System.out.println(!flag); // false - // ---- Comparison + logical (&& ||) ---------------------------------- - System.out.println(a > b && b > 0); // true - System.out.println(a == 17 || flag); // true + // ---- Comparison + logical (&& ||) ----------------------------------- + System.out.println(a > b && b > 0); // true + System.out.println(a == 17 || flag); // true - // ---- ConditionalExpression (ternary ?:) ---------------------------- + // ---- ConditionalExpression (ternary ?:) ----------------------------- int bigger = (a > b) ? a : b; - System.out.println(bigger); // 17 + System.out.println(bigger); // 17 - // ---- AP CSA String methods ----------------------------------------- + // ---- AP CSA String methods ------------------------------------------ String s = "Hello"; System.out.println(s.length()); // 5 System.out.println(s.toUpperCase()); // HELLO @@ -88,22 +67,22 @@ public static void main(String[] args) { String[] parts = "a,b,c".split(","); // split around "," System.out.println(parts[1]); // b - // ---- String equality via .equals() (AP CSA) ------------------------ + // ---- String equality via .equals() (AP CSA) ------------------------- if (label.equals("java")) { System.out.println("match"); } - // ---- char literal -------------------------------------------------- + // ---- char literal --------------------------------------------------- char grade = 'A'; System.out.println(grade); // A - // ---- Integer / Double statics -------------------------------------- - System.out.println(Integer.parseInt("42") + 1); // 43 - System.out.println(Double.parseDouble("2.5")); // 2.5 - System.out.println(Integer.MAX_VALUE); // 2147483647 - System.out.println(Integer.MIN_VALUE); // -2147483648 + // ---- Integer / Double statics --------------------------------------- + System.out.println(Integer.parseInt("42") + 1); // 43 + System.out.println(Double.parseDouble("2.5")); // 2.5 + System.out.println(Integer.MAX_VALUE); // 2147483647 + System.out.println(Integer.MIN_VALUE); // -2147483648 - // ---- Math methods -------------------------------------------------- + // ---- Math methods --------------------------------------------------- System.out.println(Math.abs(-7)); // 7 System.out.println(Math.max(3, 8)); // 8 System.out.println(Math.min(3, 8)); // 3 @@ -111,7 +90,7 @@ public static void main(String[] args) { System.out.println(Math.sqrt(16.0)); // 4 System.out.println(Math.log(1.0)); // 0.0 - // ---- ArrayList (add / add(i,x) / get / set / size / remove) -------- + // ---- ArrayList (add / add(i,x) / get / set / size / remove) --------- ArrayList list = new ArrayList(); list.add(10); list.add(20); @@ -123,9 +102,10 @@ public static void main(String[] args) { list.remove(0); System.out.println(list.size()); // 2 - // ========================= STATEMENTS ========================== - // ---- If / else if / else ------------------------------------------ + // ========================= STATEMENTS ================================ + + // ---- If / else if / else -------------------------------------------- int score = 82; if (score >= 90) { System.out.println("A"); @@ -135,74 +115,75 @@ public static void main(String[] args) { System.out.println("C"); } - // ---- While --------------------------------------------------------- + // ---- While ---------------------------------------------------------- int w = 0; while (w < 3) { System.out.println("while " + w); w++; } - // ---- DoWhile ------------------------------------------------------- + // ---- DoWhile -------------------------------------------------------- int k = 0; do { System.out.println("do " + k); k++; } while (k < 2); - // ---- For (C-style) + ArrayLiteral + IndexExpression + .length ------ + // ---- For (C-style) + ArrayLiteral + IndexExpression + .length ------- int[] nums = {10, 20, 30}; int sum = 0; for (int j = 0; j < nums.length; j++) { sum = sum + nums[j]; } - System.out.println(sum); // 60 + System.out.println(sum); // 60 - // ---- For (for-each) ------------------------------------------------ + // ---- For (for-each) ------------------------------------------------- for (int v : nums) { System.out.println("each " + v); } - // ---- IndexExpression write + MemberExpression (.length) ------------ + // ---- IndexExpression write + MemberExpression (.length) ------------- nums[0] = 99; - System.out.println(nums[0]); // 99 - System.out.println(nums.length);// 3 + System.out.println(nums[0]); // 99 + System.out.println(nums.length); // 3 - // ---- Switch / SwitchCase (matching case, default, break) ----------- + // ---- Switch / SwitchCase (matching case, default, break) ------------ int day = 3; switch (day) { case 1: System.out.println("Mon"); break; case 3: - System.out.println("Wed"); // this runs + System.out.println("Wed"); // this runs break; default: System.out.println("other day"); } - // ---- Break / Continue ---------------------------------------------- + // ---- Break / Continue ----------------------------------------------- for (int t = 0; t < 5; t++) { if (t == 1) { - continue; // skip printing 1 + continue; // skip printing 1 } if (t == 3) { - break; // stop the loop at 3 + break; // stop the loop at 3 } - System.out.println("flow " + t); // flow 0 / flow 2 + System.out.println("flow " + t); // flow 0 / flow 2 } - // ---- Try / Catch / Finally ----------------------------------------- + // ---- Try / Catch / Finally ------------------------------------------ try { - System.out.println(missingValue); // undefined variable -> error + System.out.println(1 / 0); // division by zero -> error } catch (Exception e) { - System.out.println("caught"); // this runs + System.out.println("caught"); // this runs } finally { - System.out.println("cleanup"); // always runs + System.out.println("cleanup"); // always runs } - // ========================= OBJECTS ============================= - // ---- new, methods, fields, static read, extends + super ----------- + // ========================= OBJECTS =================================== + + // ---- new, methods, fields, static read, extends + super ------------- Counter c = new Counter(5); c.increment(); c.increment(); @@ -217,23 +198,23 @@ public static void main(String[] args) { } -// ============================= CLASSES ===================================== +// ============================= CLASSES ======================================= -// ---- ClassDeclaration / FieldDeclaration / Constructor / MethodDeclaration - +// ---- ClassDeclaration / FieldDeclaration / Constructor / MethodDeclaration -- class Counter { - private int count; // FieldDeclaration, declaredWithoutInitializer - private int step = 1; // private FieldDeclaration with initializer - static int created = 0; // static FieldDeclaration with initializer + private int count; // FieldDeclaration, declaredWithoutInitializer + private int step = 1; // private FieldDeclaration with initializer + static int created = 0; // static FieldDeclaration with initializer - public Counter(int start) { // Constructor + Parameter - this.count = start; // member Assignment via `this` + public Counter(int start) { // Constructor + Parameter + this.count = start; // member Assignment via `this` } - public void increment() { // void MethodDeclaration + public void increment() { // void MethodDeclaration this.count = this.count + this.step; } - public int getCount() { // non-void returnType + Return.value + public int getCount() { // non-void returnType + Return.value return this.count; } } @@ -243,7 +224,7 @@ class Animal { public String name; public Animal(String name) { - this.name = name; // param shadows the field; `this.` disambiguates + this.name = name; // param shadows the field; `this.` disambiguates } public String describe() { @@ -253,7 +234,7 @@ public String describe() { class Dog extends Animal { public Dog(String name) { - super(name); // runs Animal's constructor on this instance + super(name); // runs Animal's constructor on this instance } public String speak() { diff --git a/examples/demo.js b/examples/demo.js index a845452..3288789 100644 --- a/examples/demo.js +++ b/examples/demo.js @@ -1,20 +1,6 @@ -// =========================================================================== -// Praxly2 feature demo -- JAVASCRIPT (shared notes: examples/README.md) -// --------------------------------------------------------------------------- -// AST nodes NOT reachable from JavaScript source (covered by the other demos): -// RepeatUntil ......... no repeat/until syntax. -// -// Interpreter notes honored below: -// * Output is console.log(...) (multiple args are space-joined). -// * Standard JS: arrays use .push, conversions use parseInt/parseFloat/String, -// integer division uses Math.trunc, .length gives length. -// * `let`/`const`/`var` all behave identically (no block scoping enforced). -// =========================================================================== - - -// ========================== EXPRESSIONS ==================================== - -// ---- Literal (number, string, boolean, null) + Assignment + console.log ---- +// ========================== EXPRESSIONS ====================================== + +// ---- Literal (number, string, boolean, null) + Assignment + console.log ----- let count = 7; const pi = 3.14; var title = "javascript"; @@ -22,18 +8,18 @@ let active = true; let nothing = null; console.log(count, pi, title, active, nothing); -// ---- Bare declaration (declaredWithoutInitializer) then assign ------------- +// ---- Bare declaration (declaredWithoutInitializer) then assign -------------- let later; later = 100; console.log(later); // 100 -// ---- BinaryExpression: arithmetic (JS division is float) + ** -------------- +// ---- BinaryExpression: arithmetic (JS division is float) + ** --------------- let a = 17; let b = 5; console.log(a + b, a - b, a * b); // 22 12 85 console.log(a / b, a % b, 2 ** 8); // 3.4 2 256 -// ---- CompoundAssignment (+= -= *= /= %=) ----------------------------------- +// ---- CompoundAssignment (+= -= *= /= %=) ------------------------------------ let acc = 10; acc += 5; // 15 acc -= 3; // 12 @@ -42,29 +28,29 @@ acc /= 4; // 6 acc %= 4; // 2 console.log("acc", acc); // acc 2 -// ---- UpdateExpression (++ --) + UnaryExpression (-, !) + comparison/logic -- +// ---- UpdateExpression (++ --) + UnaryExpression (-, !) + comparison/logic --- let i = 5; i++; --i; -console.log(i, -a, !active); // 5 -17 false -console.log(a > b && b > 0, a === 17 || active); // true true +console.log(i, -a, !active); // 5 -17 false +console.log(a > b && b > 0, a === 17 || active); // true true -// ---- ConditionalExpression (ternary) --------------------------------------- +// ---- ConditionalExpression (ternary) ---------------------------------------- let bigger = (a > b) ? a : b; -console.log("bigger", bigger); // bigger 17 +console.log("bigger", bigger); // bigger 17 -// ---- Supported String methods + conversions -------------------------------- +// ---- Supported String methods + conversions --------------------------------- let s = "Hello"; -console.log(s.length, s.toUpperCase(), s.toLowerCase(), s.substring(1, 3), s.charAt(0)); // 5 HELLO hello el H -console.log(s.indexOf("ll"), s.indexOf("l") >= 0); // 2 true (indexOf as "contains") -let parts = "a,b,c".split(","); // split -console.log(parts.length, parts[1]); // 3 b -console.log(parseInt("42"), String(7), parseFloat("1.5")); // 42 7 1.5 +console.log(s.length, s.toUpperCase(), s.toLowerCase(), s.substring(1, 3), s.charAt(0)); // 5 HELLO hello el H +console.log(s.indexOf("ll"), s.indexOf("l") >= 0); // 2 true (indexOf as "contains") +let parts = "a,b,c".split(","); // split +console.log(parts.length, parts[1]); // 3 b +console.log(parseInt("42"), String(7), parseFloat("1.5")); // 42 7 1.5 -// ========================== STATEMENTS ===================================== +// ========================== STATEMENTS ======================================= -// ---- If / else if / else --------------------------------------------------- +// ---- If / else if / else ---------------------------------------------------- let score = 82; if (score >= 90) { console.log("A"); @@ -74,7 +60,7 @@ if (score >= 90) { console.log("C"); } -// ---- While / DoWhile ------------------------------------------------------- +// ---- While / DoWhile -------------------------------------------------------- let w = 0; while (w < 3) { console.log("while " + w); @@ -86,7 +72,7 @@ do { k++; } while (k < 2); -// ---- For (C-style) + ArrayLiteral + IndexExpression + .length -------------- +// ---- For (C-style) + ArrayLiteral + IndexExpression + .length --------------- let nums = [10, 20, 30]; let sum = 0; for (let j = 0; j < nums.length; j++) { @@ -94,7 +80,7 @@ for (let j = 0; j < nums.length; j++) { } console.log("sum", sum); // sum 60 -// ---- For (for-of) over an array and over a string -------------------------- +// ---- For (for-of) over an array and over a string --------------------------- for (const v of nums) { console.log("each " + v); } @@ -102,19 +88,19 @@ for (const ch of "hi") { console.log("char " + ch); // char h / char i } -// ---- For (for-in over indices) --------------------------------------------- +// ---- For (for-in over indices) ---------------------------------------------- for (let idx in nums) { console.log("idx " + idx); // idx 0 / idx 1 / idx 2 } -// ---- IndexExpression write + array methods (.push / .pop) + .length -------- +// ---- IndexExpression write + array methods (.push / .pop) + .length --------- nums[0] = 99; nums.push(40); -console.log(nums[0], nums.length); // 99 4 +console.log(nums[0], nums.length); // 99 4 nums.pop(); -console.log(nums.length); // 3 +console.log(nums.length); // 3 -// ---- Try / ExceptionHandler / finally -------------------------------------- +// ---- Try / ExceptionHandler / finally --------------------------------------- // Reading an undefined name throws; the catch clause handles it (binding the // message to `err`) and the finally block always runs. try { @@ -125,7 +111,7 @@ try { console.log("cleanup"); // always runs } -// ---- Switch / SwitchCase (matching case, default, break) ------------------- +// ---- Switch / SwitchCase (matching case, default, break) -------------------- let day = 3; switch (day) { case 1: @@ -138,7 +124,7 @@ switch (day) { console.log("other day"); } -// ---- Break / Continue ------------------------------------------------------ +// ---- Break / Continue ------------------------------------------------------- for (let t = 0; t < 5; t++) { if (t === 1) { continue; // skip printing 1 @@ -150,14 +136,14 @@ for (let t = 0; t < 5; t++) { } -// ========================== FUNCTIONS ====================================== +// ========================== FUNCTIONS ======================================== -// ---- FunctionDeclaration / Parameter / Return (with and without a value) --- +// ---- FunctionDeclaration / Parameter / Return (with and without a value) ---- function greet(name, punct) { return "hi " + name + punct; } -function fib(n) { // recursion via CallExpression +function fib(n) { // recursion via CallExpression if (n <= 1) { return n; } @@ -166,12 +152,12 @@ function fib(n) { // recursion via CallExpression function announce(msg) { if (msg === "") { - return; // bare Return (Return.value omitted) + return; // bare Return (Return.value omitted) } console.log("announce: " + msg); } -// ---- Function calls -------------------------------------------------------- +// ---- Function calls --------------------------------------------------------- console.log(greet("sam", "!")); // hi sam! console.log(greet("sam", "?")); // hi sam? announce(""); // (prints nothing) @@ -179,22 +165,22 @@ announce("ready"); // announce: ready console.log("fib", fib(7)); // fib 13 -// ========================== CLASSES ======================================== +// ========================== CLASSES ========================================== -// ---- Class fields (FieldDeclaration) + static field + method --------------- +// ---- Class fields (FieldDeclaration) + static field + method ---------------- class Counter { - n = 0; // instance FieldDeclaration with initializer - static total = 0; // static FieldDeclaration + n = 0; // instance FieldDeclaration with initializer + static total = 0; // static FieldDeclaration bump() { this.n = this.n + 1; return this.n; } } -// ---- ClassDeclaration / Constructor / MethodDeclaration / this / super ----- +// ---- ClassDeclaration / Constructor / MethodDeclaration / this / super ------ class Animal { constructor(name) { - this.name = name; // ThisExpression + member Assignment + this.name = name; // ThisExpression + member Assignment } describe() { return this.name + " the animal"; @@ -204,14 +190,14 @@ class Animal { // ClassDeclaration.superClass via `extends` class Dog extends Animal { constructor(name) { - super(name); // super() runs the parent constructor + super(name); // super() runs the parent constructor } speak() { return "woof"; } } -// ---- Objects: new (NewExpression) + methods + this + extends + super ------- +// ---- Objects: new (NewExpression) + methods + this + extends + super -------- let c = new Counter(); console.log(c.bump(), c.bump()); // 1 2 console.log(c.n, c.total); // 2 0 (instance field + static via instance) diff --git a/examples/demo.praxis b/examples/demo.praxis index 1ea6a7a..f9240d8 100644 --- a/examples/demo.praxis +++ b/examples/demo.praxis @@ -1,45 +1,33 @@ -// =========================================================================== -// Praxly2 feature demo -- PRAXIS (shared notes: examples/README.md) -// --------------------------------------------------------------------------- -// Praxis maps most directly to the Universal AST, so it reaches the widest -// set of nodes. -// -// AST nodes NOT reachable from Praxis source (covered by the other demos): -// Switch / SwitchCase .. no switch/case keywords. -// ConditionalExpression no ?: ternary operator. -// CompoundAssignment ... no +=/-=; write `x <- x + 1`. -// ForEach .............. no for-each; Praxis has only the C-style for loop. -// -// (Functions and classes are hoisted, so the driver calls them before their -// declarations appear further down the file.) -// =========================================================================== - - -// ========================== EXPRESSIONS ==================================== +// `procedure` keyword => a void FunctionDeclaration +procedure banner(string text) + print("== " + text + " ==") +end banner + +// ========================== EXPRESSIONS ====================================== banner("literals and operators") -// ---- Literal (int, float, string, boolean, null) + Assignment -------------- -int whole <- 42 -double frac <- 3.14 -string label <- "praxis" -boolean flag <- true -string empty <- null -print(whole, frac, label, flag, empty) // Print with multiple expressions +// ---- Literal (int, float, string, boolean, null) + Assignment --------------- +int whole ← 42 +double frac ← 3.14 +string label ← "praxis" +boolean flag ← true +string empty ← null +print(whole, frac, label, flag, empty) // Print with multiple expressions -// `=` is also a valid assignment operator in Praxis (same as `<-`) -int m <- 0 +// `=` is also a valid assignment operator in Praxis (same as `←`) +int m ← 0 m = 10 print(m) -// ---- char literal + string escape sequences -------------------------------- -char grade <- 'A' // single-quoted, exactly one character -string tabbed <- "col1\tcol2" // \t \n \r \" \\ are recognized escapes +// ---- char literal + string escape sequences --------------------------------- +char grade ← 'A' // single-quoted, exactly one character +string tabbed ← "col1\tcol2" // \t \n \r \" \\ are recognized escapes print(grade) // A print(tabbed) // col1col2 -// ---- BinaryExpression: arithmetic (+ - * / % ^) ---------------------------- -int a <- 17 -int b <- 5 +// ---- BinaryExpression: arithmetic (+ - * / % ^) ----------------------------- +int a ← 17 +int b ← 5 print(a + b) // 22 print(a - b) // 12 print(a * b) // 85 @@ -47,11 +35,11 @@ print(a / b) // 3 (integer division: both operands are int) print(a % b) // 2 print(2 ^ 10) // 1024 (exponent, right-associative) -// ---- UnaryExpression (- and not) ------------------------------------------- +// ---- UnaryExpression (- and not) -------------------------------------------- print(-a) // -17 print(not flag) // false -// ---- BinaryExpression: comparison + logical (and/or) + unicode forms ------- +// ---- BinaryExpression: comparison + logical (and/or) + unicode forms -------- print(a > b and b > 0) // true print(a == 17) // true print(a != b) // true @@ -60,56 +48,56 @@ print(b ≤ a) // true (unicode ≤) print(a ≠ b) // true (unicode ≠) print(a < b or a > b) // true -// ---- UpdateExpression (++ and --, prefix and postfix) ---------------------- -int i <- 5 +// ---- UpdateExpression (++ and --, prefix and postfix) ----------------------- +int i ← 5 i++ print(i) // 6 print(++i) // 7 i-- print(i) // 6 -// ---- ArrayLiteral / IndexExpression / MemberExpression / CallExpression ---- -int[] xs <- {5, 3, 8, 1} // brace form (idiomatic); [5, 3, 8, 1] also works -xs[0] <- 50 // index Assignment +// ---- ArrayLiteral / IndexExpression / MemberExpression / CallExpression ----- +int[] xs ← {5, 3, 8, 1} // brace form (idiomatic); [5, 3, 8, 1] also works +xs[0] ← 50 // index Assignment print(xs[0]) // 50 print(xs.length) // 4 (MemberExpression) xs.append(99) // method CallExpression as ExpressionStatement print(xs.length) // 5 print(xs) // {50, 3, 8, 1, 99} -// ---- ArrayCreation: new int[n] (n default-initialized elements) ------------ -int[] buffer <- new int[3] // {0, 0, 0} -buffer[1] <- 7 +// ---- ArrayCreation: new int[n] (n default-initialized elements) ------------- +int[] buffer ← new int[3] // {0, 0, 0} +buffer[1] ← 7 print(buffer) // {0, 7, 0} print(buffer.length) // 3 -// ---- Built-in functions (min/max/abs/sqrt/log, str/int/float, contains) ---- -print(min(3, 8)) // 3 -print(max(3, 8)) // 8 -print(abs(-7)) // 7 -print(sqrt(16.0)) // 4 -print(log(1.0)) // 0 -print(str(42)) // 42 (number -> string) -print(int("7") + 1) // 8 (string -> int) -print(float("2.5")) // 2.5 (string -> float) +// ---- Built-in functions (min/max/abs/sqrt/log, str/int/float, contains) ----- +print(min(3, 8)) // 3 +print(max(3, 8)) // 8 +print(abs(-7)) // 7 +print(sqrt(16.0)) // 4 +print(log(1.0)) // 0 +print(str(42)) // 42 (number -> string) +print(int("7") + 1) // 8 (string -> int) +print(float("2.5")) // 2.5 (string -> float) print("praxis".contains("rax")) // true (String method) print("praxis".substring(0, 3)) // pra (0-based, end-exclusive) print("praxis".charAt(0)) // p print("praxis".toUpperCase()) // PRAXIS print("praxis".indexOf("x")) // 4 -// ---- Print: separator / appendLineFeed via trailing comment ---------------- -print("A", "B", "C") // "A B C" (default separator " ") -print("X", "Y") // no separator // "XY" -print("joined: ") // no line feed -print("tail") // -> "joined: tail" on one line +// ---- Print: separator / appendLineFeed via trailing comment ----------------- +print("A", "B", "C") // "A B C" (default separator " ") +print("X", "Y") // no separator +print("joined: ") // no line feed +print("tail") // -> "joined: tail" on one line -// ========================== STATEMENTS ===================================== +// ========================== STATEMENTS ======================================= banner("control flow") -// ---- If / else if / else (a single `end if` closes the whole chain) -------- -int score <- 75 +// ---- If / else if / else (a single `end if` closes the whole chain) --------- +int score ← 75 if (score >= 90) print("A") else if (score >= 70) @@ -118,46 +106,46 @@ else print("C") end if -// ---- While (pre-condition loop) -------------------------------------------- -int w <- 0 +// ---- While (pre-condition loop) --------------------------------------------- +int w ← 0 while (w < 3) print(w) // 0 1 2 - w <- w + 1 + w ← w + 1 end while -// ---- For, C-style (init / condition / update) ------------------------------ -int total <- 0 -for j <- 0; j < xs.length; j <- j + 1 - total <- total + xs[j] +// ---- For, C-style (init / condition / update) ------------------------------- +int total ← 0 +for (j ← 0; j < xs.length; j ← j + 1) + total ← total + xs[j] end for print(total) // 161 -// ---- For, iterate an array by index (Praxis has only the C-style for) ------ -for p <- 0; p < xs.length; p <- p + 1 +// ---- For, iterate an array by index (Praxis has only the C-style for) ------- +for (p ← 0; p < xs.length; p ← p + 1) print(xs[p]) end for -// ---- For, counting loop ---------------------------------------------------- -for k <- 1; k < 4; k <- k + 1 +// ---- For, counting loop ----------------------------------------------------- +for (k ← 1; k < 4; k ← k + 1) print(k) // 1 2 3 end for -// ---- DoWhile (post-condition; repeats WHILE the condition is true) --------- -int d <- 0 +// ---- DoWhile (post-condition; repeats WHILE the condition is true) ---------- +int d ← 0 do print(d) // 0 1 2 - d <- d + 1 + d ← d + 1 while (d < 3) -// ---- RepeatUntil (post-condition; repeats UNTIL the condition is true) ----- -int r <- 0 +// ---- RepeatUntil (post-condition; repeats UNTIL the condition is true) ------ +int r ← 0 repeat print(r) // 0 1 2 - r <- r + 1 + r ← r + 1 until (r >= 3) -// ---- Break / Continue ------------------------------------------------------ -for t <- 0; t < 5; t <- t + 1 +// ---- Break / Continue ------------------------------------------------------- +for (t ← 0; t < 5; t ← t + 1) if (t == 1) continue // skip printing 1 end if @@ -167,7 +155,7 @@ for t <- 0; t < 5; t <- t + 1 print(t) // 0 2 end for -// ---- Try / Catch / Finally ------------------------------------------------- +// ---- Try / Catch / Finally -------------------------------------------------- try print(missingValue) // undefined variable raises an error print("not reached") @@ -178,10 +166,10 @@ finally end try -// ========================== FUNCTIONS ====================================== +// ========================== FUNCTIONS ======================================== banner("functions and objects") -// ---- FunctionDeclaration / Parameter / Return (with and without a value) --- +// ---- FunctionDeclaration / Parameter / Return (with and without a value) ---- int add(int a, int b) return a + b // Return with a value end add @@ -194,11 +182,6 @@ int fact(int n) end if end fact -// `procedure` keyword => a void FunctionDeclaration -procedure banner(string text) - print("== " + text + " ==") -end banner - procedure maybePrint(boolean show) if (not show) return // bare Return (Return.value omitted) @@ -206,31 +189,31 @@ procedure maybePrint(boolean show) print("shown") end maybePrint -// ---- Calls, recursion, void procedures ------------------------------------- +// ---- Calls, recursion, void procedures -------------------------------------- print(add(3, 4)) // 7 print(fact(5)) // 120 maybePrint(false) // (prints nothing) maybePrint(true) // shown -// ========================== CLASSES ======================================== +// ========================== CLASSES ========================================== // Praxis has no `this`/`self`: fields are accessed by their bare names, so // field and parameter names must be unique. A constructor is named after its // class; `super` calls the superclass constructor. `public`/`private` are // optional, and fields may be initialized where they are declared. -// ---- ClassDeclaration / FieldDeclaration / Constructor / MethodDeclaration - +// ---- ClassDeclaration / FieldDeclaration / Constructor / MethodDeclaration -- class Counter private int count private int step - public Counter(int start) // constructor named after the class - count <- start // bare field assignment (no `this`) - step <- 1 + public Counter(int start) // constructor named after the class + count ← start // bare field assignment (no `this`) + step ← 1 end Counter public void increment() - count <- count + step + count ← count + step end increment public int getCount() @@ -238,14 +221,14 @@ class Counter end getCount end class Counter -// ---- Inheritance: `extends` + `super` -------------------------------------- +// ---- Inheritance: `extends` + `super` --------------------------------------- class Animal private string name private int legs - public Animal(string n) // param `n` differs from field `name` - name <- n - legs <- 4 + public Animal(string n) // param `n` differs from field `name` + name ← n + legs ← 4 end Animal public string describe() @@ -255,7 +238,7 @@ end class Animal class Dog extends Animal public Dog(string n) - super(n) // runs Animal's constructor on this instance + super(n) // runs Animal's constructor on this instance end Dog public string speak() @@ -263,26 +246,26 @@ class Dog extends Animal end speak end class Dog -// ---- Default constructor + field initializer (no constructor defined) ------ +// ---- Default constructor + field initializer (no constructor defined) ------- class Box - string contents <- "gift" // field initialized at declaration + string contents ← "gift" // field initialized at declaration public string open() return "opened " + contents end open end class Box -// ---- NewExpression / methods / fields / inheritance ------------------------ -Counter c <- new Counter(5) +// ---- NewExpression / methods / fields / inheritance ------------------------- +Counter c ← new Counter(5) c.increment() c.increment() print(c.getCount()) // 7 -Dog dog <- new Dog("Rex") +Dog dog ← new Dog("Rex") print(dog.speak()) // woof (Dog's own method) print(dog.describe()) // Rex has 4 legs (inherited field + method via super) -Box box <- new Box() // default constructor +Box box ← new Box() // default constructor print(box.open()) // opened gift banner("done") diff --git a/examples/demo.py b/examples/demo.py index ad5d66e..037ea26 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -1,19 +1,6 @@ -# =========================================================================== -# Praxly2 feature demo -- PYTHON (shared notes: examples/README.md) -# --------------------------------------------------------------------------- -# AST nodes NOT reachable from Python source (covered by the other demos): -# DoWhile / RepeatUntil ... no do-while / repeat syntax. -# Switch / SwitchCase ..... no match/switch parsing. -# UpdateExpression ........ no ++ / -- in Python. -# CompoundAssignment ...... += etc. desugar to Assignment + BinaryExpression. -# NewExpression ........... instantiation is a CallExpression, e.g. Dog("x"). -# ThisExpression .......... `self` is an ordinary Identifier. -# =========================================================================== - - -# ========================== EXPRESSIONS ==================================== - -# ---- Literal (int, float, str, True, False, None) + multi-arg Print -------- +# ========================== EXPRESSIONS ======================================= + +# ---- Literal (int, float, str, True, False, None) + multi-arg Print ---------- count = 7 pi = 3.14 title = "python" @@ -21,7 +8,7 @@ missing = None print(count, pi, title, active, missing) -# ---- BinaryExpression + augmented Assignment (desugars to `x = x y`) --- +# ---- BinaryExpression + augmented Assignment (de-sugars to `x = x y`) --- total = 10 total += 5 # 15 total -= 3 # 12 @@ -30,21 +17,21 @@ print("total", total) print(7 % 3, 2 ** 8, 10 / 4) # 1 256 2.5 -# ---- UnaryExpression (-, not) + comparison + logical (and/or) -------------- +# ---- UnaryExpression (-, not) + comparison + logical (and/or) ---------------- print(-count, not active) # -7 false print(count > 3 and pi < 4, count == 7 or active) # true true -# ---- ConditionalExpression (ternary) + != ---------------------------------- +# ---- ConditionalExpression (ternary) + != ------------------------------------ label = "big" if count > 5 else "small" # ternary print(label, count != 3) # big true -# ---- String methods (upper/lower/find/replace) ----------------------------- +# ---- String methods (upper/lower/find/replace) ------------------------------- print(title.upper()) # PYTHON print(title.lower()) # python print(title.find("th")) # 2 (index of "th") print(title.replace("p", "P")) # Python -# ---- ArrayLiteral / IndexExpression / MemberExpression / CallExpression ---- +# ---- ArrayLiteral / IndexExpression / MemberExpression / CallExpression ------ values = [5, 2, 9, 1] values[0] = 50 # index Assignment values.append(7) # method call (MemberExpression + Call) @@ -53,9 +40,10 @@ values.pop(1) # remove index 1 -> [50, 2, 9, 1] print(values, len(values), values[0]) # {50, 2, 9, 1} 4 50 -# ========================== STATEMENTS ===================================== -# ---- If / elif / else (elif => nested If in elseBranch) -------------------- +# ========================== STATEMENTS ======================================== + +# ---- If / elif / else (elif => nested If in elseBranch) ---------------------- score = 82 if score >= 90: print("A") @@ -64,13 +52,13 @@ else: print("C") -# ---- While (pre-condition loop) -------------------------------------------- +# ---- While (pre-condition loop) ---------------------------------------------- i = 0 while i < 3: print("while", i) i += 1 -# ---- For over range() / a list literal / a string -------------------------- +# ---- For over range() / a list literal / a string ---------------------------- for k in range(0, 6, 2): print("range", k) # 0 2 4 for item in [11, 22, 33]: @@ -78,7 +66,7 @@ for ch in "hi": print("char", ch) # h i -# ---- Break / Continue ------------------------------------------------------ +# ---- Break / Continue -------------------------------------------------------- for n in range(5): if n == 3: break # stop the loop at 3 @@ -86,7 +74,7 @@ continue # skip printing 1 print("flow", n) # flow 0 / flow 2 -# ---- Try / ExceptionHandler / finally -------------------------------------- +# ---- Try / ExceptionHandler / finally ---------------------------------------- # Reading an undefined name raises; the handler catches it (binding the message # to `err`) and the finally block always runs. try: @@ -97,9 +85,9 @@ print("cleanup") # always runs -# ========================== FUNCTIONS ====================================== +# ========================== FUNCTIONS ========================================= -# ---- FunctionDeclaration / Parameter / Return ------------------------------ +# ---- FunctionDeclaration / Parameter / Return -------------------------------- def greet(name, punct): return "hi " + name + punct @@ -128,9 +116,9 @@ def fib(n): print("fib", fib(7)) # fib 13 -# ========================== CLASSES ======================================== +# ========================== CLASSES =========================================== -# ---- ClassDeclaration / Constructor / MethodDeclaration / FieldDeclaration - +# ---- ClassDeclaration / Constructor / MethodDeclaration / FieldDeclaration --- class Animal: kind = "animal" # class attribute => FieldDeclaration legs: int = 4 # typed class attribute (annotation + default) diff --git a/package-lock.json b/package-lock.json index bb322b8..0d28b4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4891,13 +4891,13 @@ } }, "node_modules/adm-zip": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", - "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", + "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/agent-base": { @@ -5098,17 +5098,45 @@ } }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", diff --git a/package.json b/package.json index 172d364..c670492 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,11 @@ "tailwindcss": "^4.1.18", "zustand": "^5.0.14" }, + "overrides": { + "chromedriver": { + "adm-zip": "^0.6.0" + } + }, "lint-staged": { "*.{ts,tsx,js,jsx,json,md,css,html,yml,yaml}": [ "prettier --check" diff --git a/specs/csp.md b/specs/csp.md index c1e3c8a..3514137 100644 --- a/specs/csp.md +++ b/specs/csp.md @@ -4,6 +4,7 @@ The _AP Computer Science Principles_ exam uses the pseudocode notation described Notice that: - Assignment uses `←`, not `=` + - `<-` and `⟵` are also accepted by the interpreter (`=` is not — it is CSP's equality operator) - Indexes start at `1`, not `0` - Output uses `DISPLAY`, not `print` - Comparison uses `=`, not `==` diff --git a/specs/java.md b/specs/java.md index e25c15d..818ef34 100644 --- a/specs/java.md +++ b/specs/java.md @@ -2,6 +2,36 @@ The _AP Computer Science A_ exam may use the following methods from the Java library. +## Program Structure + +Every Java program must define a class named `Main` with a runnable entry point: + +```java +public class Main { + public static void main(String[] args) { + // your code here + } +} +``` + +There is no bare-statement/script mode — top-level statements outside a class are +a parse error, and a `Main` class without a matching `public static void main(String[] args)` +method is also a parse error. This mirrors standard Java so that code written in Praxly +compiles as-is in any other Java toolchain. Additional classes may be declared alongside +`Main` (see `Extensions for Praxly` below for OOP support), and `import` declarations are +accepted and ignored (Praxly's stdlib is always available without an explicit import). + +A variable or field declared without an initializer (`int x;`) is **uninitialized, not +defaulted to `0`/`false`/`null`** — reading it before assigning a value is a runtime error. +This mirrors Java's compile-time "variable might not have been initialized" check, enforced +dynamically here since Praxly has no static definite-assignment pass. + +When a Java program is translated to a language with free functions, the `Main` wrapper +is unwrapped rather than emitted as a class: `main`'s body becomes the top-level program, +each other **static method** of `Main` becomes a free function (e.g. a Python `def`, a CSP +`PROCEDURE`), and each **static field** of `Main` becomes a top-level variable declaration. +Classes other than `Main` translate as classes (where the target supports them). + ## String Class - `String(String str)` – Constructs a new `String` object that represents the same sequence of characters as `str` diff --git a/specs/javascript.md b/specs/javascript.md index 6a9a661..3dc126a 100644 --- a/specs/javascript.md +++ b/specs/javascript.md @@ -20,7 +20,8 @@ Assume standard JavaScript semantics unless stated otherwise. - Declarations: `let`, `const`, `var` (all treated the same; block/function scoping is not modeled — the interpreter uses a flat scope like the other languages). -- A bare `let x;` declares `x` with no initializer. +- A bare `let x;` declares `x` as uninitialized — reading it before assigning a value is a + runtime error (rather than JavaScript's usual `undefined`). - JavaScript is dynamically typed; no type annotations. - Literals: `true`, `false`, `null`, `undefined`. diff --git a/specs/praxis.md b/specs/praxis.md index effc9f1..569a5bb 100644 --- a/specs/praxis.md +++ b/specs/praxis.md @@ -7,7 +7,10 @@ Assume Java semantics unless stated otherwise. ## Basics - Comments: `// this is a single-line comment` -- Placeholder for missing code: e.g., `/* missing code */` or `/* missing condition */` +- Placeholder for missing code: e.g., `/* missing code */` or `/* missing condition */`. + A placeholder has no value — assigning one to a variable (`x ← /* missing */`) leaves + that variable uninitialized (reading it later is a runtime error), and using a + placeholder directly (e.g. `if (/* cond */)`) is an immediate runtime error. - Print: `print arg` or `print(arg)` - By default, a newline is appended to the output (i.e., like `println`). - A comment is used where necessary to indicate if a line feed or blank is appended to the argument. @@ -33,11 +36,16 @@ Assume Java semantics unless stated otherwise. - Data types: `boolean`, `char`, `double`, `float`, `int`, `int[]`, `int[][]`, `short`, `String` - Array initialization - `int[] a ← {1, 2, 3}` +- A bare declaration with no initializer (`int x`) leaves `x` uninitialized — reading it + before assigning a value is a runtime error. - Array reference - `b[0] = a[2]` ## Conditional statements +Parentheses around the condition (and around a `for` loop's `init; condition; increment` +header) are required, as shown below — the interpreter rejects a header written without them. + ``` if (condition) block of statements diff --git a/specs/python.md b/specs/python.md index 0ab5700..956d6c0 100644 --- a/specs/python.md +++ b/specs/python.md @@ -21,8 +21,9 @@ Assume standard Python semantics unless stated otherwise. - Dynamically typed. Assignment: `x = value`. - **Chained assignment**: `x = y = z = 0`. - **Augmented assignment**: `+=`, `-=`, `*=`, `/=`, `%=`, `//=`. -- **Optional type annotations**: `x: int = 5`, or a bare `x: int` (which declares `x` with a - type-appropriate default). Annotations are accepted on variables and parameters. +- **Optional type annotations**: `x: int = 5`, or a bare `x: int` (which declares `x` as + uninitialized — reading it before assigning a value is a runtime error). Annotations are + accepted on variables and parameters. - Literals: `True`, `False`, `None`. ## Operators diff --git a/src/components/OutputPanel.tsx b/src/components/OutputPanel.tsx index 9e881a8..efd0973 100644 --- a/src/components/OutputPanel.tsx +++ b/src/components/OutputPanel.tsx @@ -1,6 +1,8 @@ import React, { useState, useRef, useEffect, useId } from 'react'; import { Terminal, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react'; import { ResizeHandle } from './ResizeHandle'; +import { VariableFrames, formatVariableValue } from './VariableFrames'; +import type { StackFrame } from '../language/debugger'; export type OutputPanelState = 'open' | 'closed'; @@ -10,6 +12,8 @@ interface OutputPanelProps { /** False until the user's first Run/Debug — suppresses the placeholder after that. */ hasRun?: boolean; variables?: Record; + /** Debugger call stack (global scope first). When given, variables are shown per frame. */ + callStack?: StackFrame[]; showVariables?: boolean; height?: number; resizeActive?: boolean; @@ -25,29 +29,12 @@ interface OutputPanelProps { const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-1 focus-visible:ring-offset-slate-900'; -const formatVariableValue = (value: any): string => { - if (value === null) return 'null'; - if (value === undefined) return 'undefined'; - if (typeof value === 'boolean') return value.toString(); - if (typeof value === 'number') return value.toString(); - if (typeof value === 'string') return `"${value}"`; - if (Array.isArray(value)) { - return `[${value.map((v) => formatVariableValue(v)).join(', ')}]`; - } - if (typeof value === 'object' && value.klass?.name) { - return `${value.klass.name} instance`; - } - if (typeof value === 'object' && value.klass) { - return `JavaClass(${(value as any).name || 'unknown'})`; - } - return String(value); -}; - export const OutputPanel: React.FC = ({ output, error, hasRun = false, variables = {}, + callStack = [], showVariables = false, height, resizeActive = false, @@ -214,25 +201,36 @@ export const OutputPanel: React.FC = ({ className="flex flex-col border-t sm:border-t-0 sm:border-l border-slate-800 w-full sm:w-64 shrink-0 max-h-40 sm:max-h-none" aria-label="Debug variables" > -
- +
+ Variables + {callStack.length > 1 && ( + + in {callStack[callStack.length - 1].name}() + + )}
- {Object.keys(variables).length === 0 ? ( + {callStack.length > 0 ? ( + + ) : Object.keys(variables).length === 0 ? (
No variables
) : ( Object.entries(variables).map(([name, value]) => { if (typeof value === 'function' || name.startsWith('_')) return null; - const valueStr = formatVariableValue(value); return (
{name}: - {valueStr} + + {formatVariableValue(value)} +
); }) diff --git a/src/components/VariableFrames.tsx b/src/components/VariableFrames.tsx new file mode 100644 index 0000000..6b836d1 --- /dev/null +++ b/src/components/VariableFrames.tsx @@ -0,0 +1,90 @@ +/** + * Call-stack-aware variable table used while debugging (Editor and Embed pages). + * Renders one section per stack frame — innermost call on top, globals last — + * so students can watch locals appear when a function is entered and disappear + * when it returns. + */ + +import type { StackFrame } from '../language/debugger'; + +export const formatVariableValue = (value: any): string => { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (typeof value === 'boolean') return value.toString(); + if (typeof value === 'number') return value.toString(); + if (typeof value === 'string') return `"${value}"`; + if (Array.isArray(value)) { + return `[${value.map((v) => formatVariableValue(v)).join(', ')}]`; + } + if (typeof value === 'object' && value.klass?.name) { + return `${value.klass.name} instance`; + } + return String(value); +}; + +interface VariableFramesProps { + frames: StackFrame[]; +} + +export function VariableFrames({ frames }: VariableFramesProps) { + if (frames.length === 0) { + return
No variables
; + } + + // Innermost call first (the top of the stack), global scope last. + const ordered = [...frames].reverse(); + const inFunction = frames.length > 1; + + return ( +
+ {ordered.map((frame, idx) => { + const isCurrent = inFunction && idx === 0; + const label = frame.name === 'global' ? 'Globals' : `${frame.name}()`; + const entries = Object.entries(frame.variables); + + return ( +
+
+ + {label} + + {isCurrent && ( + + current + + )} +
+
+ {entries.length === 0 ? ( +
no variables
+ ) : ( + entries.map(([name, value]) => ( +
+ {name} + + {formatVariableValue(value)} + +
+ )) + )} +
+
+ ); + })} +
+ ); +} diff --git a/src/components/editor/BlocklyPane.tsx b/src/components/editor/BlocklyPane.tsx index 2335096..9b2a85d 100644 --- a/src/components/editor/BlocklyPane.tsx +++ b/src/components/editor/BlocklyPane.tsx @@ -16,10 +16,30 @@ interface BlocklyPaneProps { fontSize?: number; } +/** + * Locks a translation pane's blocks: whole top-level chains stay draggable so + * the user can arrange them, but child blocks are pinned to their parents (a + * chain can't be taken apart), and nothing is deletable or editable. + * + * The workspace itself is injected editable rather than with Blockly's + * `readOnly` option — read-only workspaces make every block immovable, which + * both freezes the view and silently disables `cleanUp()` (it only arranges + * movable blocks), leaving freshly loaded chains overlapping. + */ +function lockBlocks(workspace: WorkspaceSvg): void { + for (const block of workspace.getAllBlocks(false)) { + block.setDeletable(false); + block.setEditable(false); + block.contextMenu = false; + block.setMovable(block.getParent() === null); + } +} + /** * Hosts a Blockly workspace inside a panel. Editable mode shows the block * toolbox and reports edits upward as serialization JSON; read-only mode - * re-renders whenever the incoming translation JSON changes. + * re-renders whenever the incoming translation JSON changes, showing locked + * blocks (movable chains that can't be edited or taken apart). */ export function BlocklyPane({ value, @@ -46,7 +66,6 @@ export function BlocklyPane({ registerPraxlyDialogs(); const workspace = Blockly.inject(container, { - readOnly, theme: praxlyTheme(fontSizeRef.current), renderer: 'zelos', toolbox: readOnly ? undefined : PRAXLY_TOOLBOX, @@ -129,6 +148,7 @@ export function BlocklyPane({ } else if (value.trim()) { Blockly.serialization.workspaces.load(JSON.parse(value), workspace); setLoadError(null); + if (readOnly) lockBlocks(workspace); workspace.cleanUp(); } else { workspace.clear(); @@ -141,7 +161,7 @@ export function BlocklyPane({ lastJsonRef.current = value; Blockly.Events.enable(); } - }, [value]); + }, [value, readOnly]); return (
diff --git a/src/components/editor/EditorHeader.tsx b/src/components/editor/EditorHeader.tsx index da241fe..dba15da 100644 --- a/src/components/editor/EditorHeader.tsx +++ b/src/components/editor/EditorHeader.tsx @@ -4,6 +4,7 @@ import { Trash2, Home, BookOpen, + FileCode, Bug, FastForward, Square, @@ -27,10 +28,12 @@ interface EditorHeaderProps { isDebugging: boolean; isDebugComplete: boolean; examples: ExampleProgram[]; + demoAvailable: boolean; textSize: TextSize; onClear: () => void; onShare: () => void; onLoadExample: (exampleId: string) => void; + onLoadDemo: () => void; onToggleExamplesMenu: () => void; onToggleSettingsMenu: () => void; onDebugStart: () => void; @@ -50,10 +53,12 @@ export function EditorHeader({ isDebugging, isDebugComplete, examples, + demoAvailable, textSize, onClear, onShare, onLoadExample, + onLoadDemo, onToggleExamplesMenu, onToggleSettingsMenu, onDebugStart, @@ -92,6 +97,15 @@ export function EditorHeader({ className="flex items-center flex-wrap justify-end gap-2 sm:gap-3" aria-label="Editor controls" > + + {/* Examples menu */}
)} - {isDebugging && Object.keys(currentVariables).length > 0 && ( + {isDebugging && currentCallStack.length > 0 && (
-
Variables:
- {Object.entries(currentVariables).map(([key, value]) => ( -
- {key}: {JSON.stringify(value)} -
- ))} +
+ Variables + {currentCallStack.length > 1 && ( + + — in {currentCallStack[currentCallStack.length - 1].name}() + + )} +
+
)} {output.length === 0 && !error ? ( -
Run code to see output...
+
+ {isDebugging ? 'No output yet — keep stepping…' : 'Run code to see output...'} +
) : ( output.map((line, idx) => (
diff --git a/src/utils/debugHandlers.ts b/src/utils/debugHandlers.ts index 7fd133c..32217d9 100644 --- a/src/utils/debugHandlers.ts +++ b/src/utils/debugHandlers.ts @@ -1,49 +1,14 @@ /** - * Debug step computation functions that execute code and compute debug information. - * Handles both simple execution and step-by-step debugging with output and variable tracking. + * Debug highlighting helpers shared by the editor pages. */ import type { Program } from '../language/ast'; import type { SupportedLang } from '../components/LanguageSelector'; -import { Interpreter } from '../language/interpreter'; -import { findNodesAtLocation } from './debuggerUtils'; import type { SourceMap } from '../hooks/useCodeParsing'; /** - * Result of a debug step operation with all data needed by UI - */ -export interface DebugStepData { - sourceHighlightedLines: number[]; - panelHighlights: Map; // For EditorPage multi-panel - outputLines: string[]; - isComplete: boolean; -} - -/** - * Execute code and return output (shared between both pages) - */ -export function computeRunOutput(ast: Program | null, sourceCode: string = ''): string[] { - const output: string[] = []; - if (!ast) return output; - - try { - const interpreter = new Interpreter(); - const results = interpreter.interpret(ast, sourceCode); - output.push(...results); - } catch (e: any) { - // Re-throw InputPrompt errors so UI can handle input - if (e.name === 'InputPrompt') { - throw e; - } - output.push(`Error: ${e.message}`); - } - - return output; -} - -/** - * Process a debug step for multi-panel highlighting (EditorPage-specific) - * Computes which lines to highlight in each language panel + * Computes which line to highlight in each open translation panel for the + * debug step at `nodeId`, using each panel's emitter source map. */ export function computeMultiplePanelHighlighting( ast: Program | null, @@ -52,35 +17,17 @@ export function computeMultiplePanelHighlighting( ast: Program | null, lang: SupportedLang ) => { code: string; sourceMap: SourceMap }, - sourceLocation: { start: number; end: number } | null + nodeId: string | null | undefined ): Map { const panelHighlights = new Map(); - if (!ast || !sourceLocation || panels.length === 0) { + if (!ast || !nodeId || panels.length === 0) { return panelHighlights; } - const nodesAtLocation = findNodesAtLocation(ast, sourceLocation.start); - const nodeIds = nodesAtLocation.map((n) => n.id); - for (const panel of panels) { - const translation = getTranslation(ast, panel.lang); - const panelHighlightSet = new Set(); - - for (const nodeId of nodeIds) { - const mapEntry = translation.sourceMap.get - ? translation.sourceMap.get(nodeId) - : (translation.sourceMap as any)[nodeId]; - - if (mapEntry !== undefined) { - if (typeof mapEntry === 'object' && 'lineStart' in mapEntry) { - panelHighlightSet.add(mapEntry.lineStart - 1); - } else if (typeof mapEntry === 'number') { - panelHighlightSet.add(mapEntry); - } - } - } - panelHighlights.set(panel.id, Array.from(panelHighlightSet)); + const line = getTranslation(ast, panel.lang).sourceMap.get(nodeId); + panelHighlights.set(panel.id, line !== undefined ? [line] : []); } return panelHighlights; diff --git a/src/utils/debuggerUtils.ts b/src/utils/debuggerUtils.ts index cb77651..1178086 100644 --- a/src/utils/debuggerUtils.ts +++ b/src/utils/debuggerUtils.ts @@ -1,11 +1,7 @@ /** - * Debugging utility functions for mapping source code locations to AST nodes. - * Provides line number calculation and node finding for debug highlighting. + * Debugging utility functions for mapping source code locations to editor lines. */ -import type { Program, ASTNode } from '../language/ast'; -import type { SourceMap } from '../language/visitor'; - /** * Get the line number (1-based) for a character offset */ @@ -21,45 +17,3 @@ export function getRangeLines(code: string, start: number): number[] { const startLine = getLineNumber(code, start); return [startLine - 1]; // Convert to 0-based for CodeMirror } - -/** - * Find all AST nodes that span the given source location - */ -export function findNodesAtLocation(program: Program, sourceStart: number): ASTNode[] { - const foundNodes: ASTNode[] = []; - - const traverse = (node: any): void => { - if (!node || typeof node !== 'object') return; - - // Check if this node contains the source location - if (node.loc && node.loc.start <= sourceStart && sourceStart < node.loc.end) { - if (node.id && node.type) { - foundNodes.push(node); - } - } - - // Traverse children - if (Array.isArray(node)) { - node.forEach(traverse); - } else { - Object.values(node).forEach(traverse); - } - }; - - traverse(program); - return foundNodes; -} - -/** - * Get highlighted line numbers from AST node IDs using a source map - */ -export function getHighlightedLinesFromNodeIds(nodeIds: string[], sourceMap: SourceMap): number[] { - const lines = new Set(); - for (const nodeId of nodeIds) { - const lineIndex = sourceMap.get(nodeId); - if (lineIndex !== undefined) { - lines.add(lineIndex); - } - } - return Array.from(lines); -} diff --git a/src/utils/demoPrograms.ts b/src/utils/demoPrograms.ts new file mode 100644 index 0000000..f01586c --- /dev/null +++ b/src/utils/demoPrograms.ts @@ -0,0 +1,25 @@ +/** + * Per-language feature demo programs, sourced directly from `examples/demo.*` + * (the same files the round-trip/examples/blocks test suites use) so the + * content lives in exactly one place. + */ + +import pythonDemo from '../../examples/demo.py?raw'; +import javaDemo from '../../examples/demo.java?raw'; +import javascriptDemo from '../../examples/demo.js?raw'; +import cspDemo from '../../examples/demo.csp?raw'; +import praxisDemo from '../../examples/demo.praxis?raw'; +import blocksDemo from '../../examples/demo.blocks.json?raw'; + +import type { SupportedLang } from '../components/LanguageSelector'; + +export const DEMO_PROGRAMS: Partial> = { + python: pythonDemo, + java: javaDemo, + javascript: javascriptDemo, + csp: cspDemo, + praxis: praxisDemo, + blocks: blocksDemo, +}; + +export const getDemoForLang = (lang: SupportedLang): string | undefined => DEMO_PROGRAMS[lang]; diff --git a/src/utils/sampleCodes.ts b/src/utils/sampleCodes.ts index 081e80c..ab3882b 100644 --- a/src/utils/sampleCodes.ts +++ b/src/utils/sampleCodes.ts @@ -27,7 +27,7 @@ export const EXAMPLE_CATEGORIES: Record = { export const EXAMPLE_PROGRAMS: ExampleProgram[] = [ { id: 'praxis-dice-score', - title: 'Dice Score Function', + title: 'Praxis Dice Score', description: 'Nested conditionals and return values', category: 'functions', lang: 'praxis', @@ -52,9 +52,37 @@ print newScore(1, 2, 3) description: 'Simple counting loop with output', category: 'loops', lang: 'praxis', - code: `for i <- 0; i < 5; i <- i + 1 - print(i) + code: `for ( i ← 0; i < 5; i ← i + 1 ) + print i end for +`, + }, + { + id: 'csp-repeat-until', + title: 'CSP Repeat Until', + description: 'Repeat-until loop with arithmetic update', + category: 'loops', + lang: 'csp', + code: `x ← 0 +REPEAT UNTIL (x >= 5) +{ + x ← x + 1 +} +DISPLAY(x) +`, + }, + { + id: 'csp-procedure-greet', + title: 'CSP Procedure', + description: 'Procedure declaration and call', + category: 'functions', + lang: 'csp', + code: `PROCEDURE greet(name) +{ + DISPLAY("Hello " + name) +} + +greet("Praxly") `, }, { @@ -110,44 +138,20 @@ print("Rolled a 6 after", attempts, "tries") }, { id: 'java-if-branch', - title: 'Java Conditional Branch', + title: 'Java Conditional', description: 'If / else with numeric comparison', category: 'conditionals', lang: 'java', - code: `int x = 7; -if (x < 10) { - System.out.println("small"); -} else { - System.out.println("big"); -} -`, - }, - { - id: 'csp-repeat-until', - title: 'CSP Repeat Until', - description: 'Repeat-until loop with arithmetic update', - category: 'loops', - lang: 'csp', - code: `x <- 0 -REPEAT UNTIL (x >= 5) -{ - x <- x + 1 -} -DISPLAY(x) -`, - }, - { - id: 'csp-procedure-greet', - title: 'CSP Procedure', - description: 'Procedure declaration and call', - category: 'functions', - lang: 'csp', - code: `PROCEDURE greet(name) -{ - DISPLAY("Hello " + name) + code: `public class Main { + public static void main(String[] args) { + int x = 7; + if (x < 10) { + System.out.println("small"); + } else { + System.out.println("big"); + } + } } - -greet("Praxly") `, }, { diff --git a/tests/blank-lines.test.ts b/tests/blank-lines.test.ts index 25067de..6a994f6 100644 --- a/tests/blank-lines.test.ts +++ b/tests/blank-lines.test.ts @@ -55,22 +55,39 @@ describe('blank-line preservation', () => { }); it('inserts a BlankLine node for every text front-end', () => { - const cases: Array<[string, () => Program]> = [ - ['python', () => py('x = 1\n\ny = 2')], + // Java statements must live inside Main.main; extract that method's block + // instead of the top-level program body (which is just [ClassDeclaration]). + const javaMainBody = (p: Program) => + ((p.body[0] as any).body.find((m: any) => m.name === 'main').body as any).body.map( + (s: any) => s.type + ); + const cases: Array<[string, () => Program, (p: Program) => string[]]> = [ + ['python', () => py('x = 1\n\ny = 2'), bodyTypes], [ 'javascript', () => new JavaScriptParser(new JavaScriptLexer('let x = 1;\n\nlet y = 2;').tokenize()).parse(), + bodyTypes, ], - ['java', () => new JavaParser(new JavaLexer('int x = 1;\n\nint y = 2;').tokenize()).parse()], - ['csp', () => new CSPParser(new CSPLexer('x <- 1\n\ny <- 2').tokenize()).parse()], + [ + 'java', + () => + new JavaParser( + new JavaLexer( + 'public class Main {\n public static void main(String[] args) {\n int x = 1;\n\n int y = 2;\n }\n}' + ).tokenize() + ).parse(), + javaMainBody, + ], + ['csp', () => new CSPParser(new CSPLexer('x <- 1\n\ny <- 2').tokenize()).parse(), bodyTypes], [ 'praxis', () => new PraxisParser(new PraxisLexer('int x <- 1\n\nint y <- 2').tokenize()).parse(), + bodyTypes, ], ]; - for (const [lang, parse] of cases) { - expect(bodyTypes(parse()), `${lang} should insert a BlankLine`).toEqual([ + for (const [lang, parse, extract] of cases) { + expect(extract(parse()), `${lang} should insert a BlankLine`).toEqual([ 'Assignment', 'BlankLine', 'Assignment', diff --git a/tests/blocks.test.ts b/tests/blocks.test.ts index 4f7e87f..e3e328a 100644 --- a/tests/blocks.test.ts +++ b/tests/blocks.test.ts @@ -526,3 +526,25 @@ describe('Blocks Option B (interpret round trip)', () => { }); }); }); + +describe('Java Main class unwrapping in the Blocks view', () => { + it('converts a Java Main program (helper method + main body) to blocks and back', async () => { + const { JavaLexer } = await import('../src/language/java/lexer'); + const { JavaParser } = await import('../src/language/java/parser'); + const javaCode = `public class Main { + public static int doubleIt(int n) { + return n * 2; + } + + public static void main(String[] args) { + System.out.println(doubleIt(5)); + } +}`; + const program = new JavaParser(new JavaLexer(javaCode).tokenize()).parse(); + const workspaceJson = programToBlocksJson(program); + const roundTripped = blocksToProgram(workspaceJson); + + expect(roundTripped.body.some((s: any) => s.type === 'FunctionDeclaration')).toBe(true); + expect(new Interpreter().interpret(roundTripped, '')).toEqual(['10']); + }); +}); diff --git a/tests/bugfixes.test.ts b/tests/bugfixes.test.ts index 67fe16b..f1d7fef 100644 --- a/tests/bugfixes.test.ts +++ b/tests/bugfixes.test.ts @@ -24,7 +24,9 @@ describe('bugs found while building the example demos', () => { // Bug 1: the Java parser built CompoundAssignment without `left`/`right`, // which the interpreter reads -> "Cannot read properties of undefined". it('Java compound assignment executes', () => { - const out = runJava('int acc = 10; acc += 5; acc *= 2; System.out.println(acc);'); + const out = runJava( + 'public class Main { public static void main(String[] args) { int acc = 10; acc += 5; acc *= 2; System.out.println(acc); } }' + ); expect(out.join('\n')).not.toMatch(/runtime error/i); expect(out).toContain('30'); }); @@ -33,7 +35,7 @@ describe('bugs found while building the example demos', () => { // parsed as an identifier -> "Undefined variable 'continue'". it('Java `continue` parses as a keyword rather than an undefined identifier', () => { const out = runJava( - 'for (int t = 0; t < 3; t++) { if (t == 1) { continue; } System.out.println(t); }' + 'public class Main { public static void main(String[] args) { for (int t = 0; t < 3; t++) { if (t == 1) { continue; } System.out.println(t); } } }' ); expect(out.join('\n')).not.toMatch(/undefined variable/i); }); diff --git a/tests/comments.test.ts b/tests/comments.test.ts index d242ce7..309abf7 100644 --- a/tests/comments.test.ts +++ b/tests/comments.test.ts @@ -1,7 +1,7 @@ /** * Comment translation: comments captured by each lexer are attached to * statements and re-emitted in the target language's delimiter, so a source - * program's comments survive translation (header block, leading, and trailing). + * program's comments survive translation (file-top, leading, and trailing). */ import { describe, it, expect } from 'vitest'; @@ -9,22 +9,26 @@ import { Lexer as PythonLexer } from '../src/language/python/lexer'; import { Parser as PythonParser } from '../src/language/python/parser'; import { PraxisLexer } from '../src/language/praxis/lexer'; import { PraxisParser } from '../src/language/praxis/parser'; +import { JavaLexer } from '../src/language/java/lexer'; +import { JavaParser } from '../src/language/java/parser'; +import { Interpreter } from '../src/language/interpreter'; import { Translator } from '../src/language/translator'; const py = (src: string) => new PythonParser(new PythonLexer(src).tokenize()).parse(); const px = (src: string) => new PraxisParser(new PraxisLexer(src).tokenize()).parse(); +const jv = (src: string) => new JavaParser(new JavaLexer(src).tokenize()).parse(); const to = (parse: () => any, target: any) => new Translator().translate(parse(), target); describe('comment translation', () => { - it('translates a header block, a leading comment, and a trailing comment (python -> java)', () => { + it('translates a file-top comment block, a leading comment, and a trailing comment (python -> java)', () => { const src = `# banner one # banner two x = 5 # keeps x # describe y y = x + 1`; const java = to(() => py(src), 'java'); - // Header is pinned to the top with the target delimiter. - expect(java.startsWith('// banner one\n// banner two\n')).toBe(true); + // File-top comments attach to the first statement as leading comments. + expect(java).toContain('// banner one\n // banner two\n double x = 5;'); // Leading comment sits on its own line above its statement. expect(java).toContain('// describe y'); // Trailing comment stays inline on the statement's line. @@ -49,6 +53,48 @@ y = x + 1`; expect(out).not.toContain('no separator'); }); + it('attaches leading and trailing comments to a top-level function declaration', () => { + const src = `# a top-level function +def greet(name): # says hi + print(name)`; + const java = to(() => py(src), 'java'); + expect(java).toContain('// a top-level function\n public static void greet'); + expect(java).toMatch( + /greet\(Object name\) \{\s+System\.out\.println\(name\);\s+\}\s+\/\/ says hi/ + ); + }); + + it('attaches leading and trailing comments to a top-level class declaration', () => { + const src = `# a class comment +class Dog: # a good boy + def __init__(self): + self.name = "Rex"`; + const java = to(() => py(src), 'java'); + expect(java).toContain('// a class comment\npublic class Dog'); + expect(java).toMatch(/^\}\s+\/\/ a good boy/m); + }); + + it('does not spawn an empty synthetic Main class from a blank line between top-level classes', () => { + // Regression: once ClassDeclaration carries a `loc`, insertBlankLines starts + // inserting BlankLine nodes at the top level too — those must not count as + // "real" main-body content when the Java emitter decides whether to wrap a + // synthetic `public class Main { public static void main(...) {} }`. + const src = `public class Main { + public static void main(String[] args) { + System.out.println("hi"); + } +} + +public class Helper { +} +`; + const program = jv(src); + const java = new Translator().translate(program, 'java'); + const reparsed = jv(java); + expect(java.match(/public class Main/g)).toHaveLength(1); + expect(new Interpreter().interpret(reparsed, java)).toEqual(['hi']); + }); + it('comments do not alter emitted program behavior', () => { // Same program with and without comments yields identical code sans comments. const withC = to(() => py('# c\nx = 1 # t\nprint(x)'), 'java'); diff --git a/tests/control-flow.test.ts b/tests/control-flow.test.ts index a336cbd..1031951 100644 --- a/tests/control-flow.test.ts +++ b/tests/control-flow.test.ts @@ -13,8 +13,10 @@ import { JavaScriptLexer } from '../src/language/javascript/lexer'; import { JavaScriptParser } from '../src/language/javascript/parser'; import { Interpreter } from '../src/language/interpreter'; -const java = (src: string): string[] => - new Interpreter().interpret(new JavaParser(new JavaLexer(src).tokenize()).parse(), src); +const java = (body: string): string[] => { + const src = `public class Main {\n public static void main(String[] args) {\n${body}\n }\n}`; + return new Interpreter().interpret(new JavaParser(new JavaLexer(src).tokenize()).parse(), src); +}; const python = (src: string): string[] => new Interpreter().interpret(new PythonParser(new PythonLexer(src).tokenize()).parse(), src); const js = (src: string): string[] => diff --git a/tests/csp.test.ts b/tests/csp.test.ts index a94b5a6..c955c2b 100644 --- a/tests/csp.test.ts +++ b/tests/csp.test.ts @@ -49,12 +49,25 @@ describe('CSP Lexer', () => { expect(tokens).toContainEqual(expect.objectContaining({ type: 'OPERATOR', value: '<-' })); }); + it('accepts ← and ⟵ as assignment operator variants (not =, which is equality)', () => { + for (const arrow of ['←', '⟵']) { + const tokens = new CSPLexer(`x ${arrow} 5`).tokenize(); + expect(tokens).toContainEqual(expect.objectContaining({ type: 'OPERATOR', value: '<-' })); + } + }); + it('should tokenize not-equal operator', () => { const lexer = new CSPLexer('x <> y'); const tokens = lexer.tokenize(); expect(tokens).toContainEqual(expect.objectContaining({ type: 'OPERATOR', value: '<>' })); }); + it('should tokenize != as a not-equal operator variant', () => { + const lexer = new CSPLexer('x != y'); + const tokens = lexer.tokenize(); + expect(tokens).toContainEqual(expect.objectContaining({ type: 'OPERATOR', value: '!=' })); + }); + it('should tokenize comparison operators', () => { const lexer = new CSPLexer('a < b a > c a <= d a >= e'); const tokens = lexer.tokenize(); @@ -247,7 +260,7 @@ describe('CSP Emitter', () => { }); emitter.visitProgram(program); const code = emitter.getGeneratedCode(); - expect(code).toContain('<-'); + expect(code).toContain('←'); }); it('should emit display statement', () => { @@ -364,7 +377,7 @@ describe('CSP Translation', () => { const translator = new Translator(); const result = translator.translate(program, 'csp'); expect(result).toContain('x'); - expect(result).toContain('<-'); + expect(result).toContain('←'); expect(result).toContain('5'); }); @@ -505,7 +518,7 @@ DISPLAY("after")`; const result = new Translator().translate(program, 'csp'); expect(result).not.toContain('FROM'); expect(result).toContain('REPEAT UNTIL'); - expect(result).toContain('i <- i + 2'); + expect(result).toContain('i ← i + 2'); }); it('should handle REPEAT n TIMES with variable', () => { diff --git a/tests/debugger.test.ts b/tests/debugger.test.ts new file mode 100644 index 0000000..b12a333 --- /dev/null +++ b/tests/debugger.test.ts @@ -0,0 +1,399 @@ +/** + * Debugger step-through tests: stepping into user-defined functions/methods, + * per-frame variable state, and control flow while debugging. + */ + +import { describe, it, expect } from 'vitest'; +import { PraxisLexer } from '../src/language/praxis/lexer'; +import { PraxisParser } from '../src/language/praxis/parser'; +import { Lexer as PythonLexer } from '../src/language/python/lexer'; +import { Parser as PythonParser } from '../src/language/python/parser'; +import { JavaLexer } from '../src/language/java/lexer'; +import { JavaParser } from '../src/language/java/parser'; +import { Debugger, type DebugStep, type SupportedLang } from '../src/language/debugger'; +import { Translator } from '../src/language/translator'; +import type { Program } from '../src/language/ast'; + +function parse(lang: SupportedLang, code: string): Program { + switch (lang) { + case 'praxis': + return new PraxisParser(new PraxisLexer(code).tokenize()).parse(); + case 'python': + return new PythonParser(new PythonLexer(code).tokenize()).parse(); + case 'java': + return new JavaParser(new JavaLexer(code).tokenize()).parse(); + default: + throw new Error(`unsupported test language ${lang}`); + } +} + +/** Runs the debugger to completion (or `maxSteps`) and returns every step. */ +function collectSteps(lang: SupportedLang, code: string, maxSteps = 200): DebugStep[] { + const dbg = new Debugger(); + dbg.init(parse(lang, code), lang, code); + const steps: DebugStep[] = []; + for (let i = 0; i < maxSteps; i++) { + const step = dbg.step(); + if (!step) break; + steps.push(step); + if (step.isComplete) break; + } + return steps; +} + +const lineOf = (code: string, step: DebugStep): number | null => + step.sourceLocation ? code.slice(0, step.sourceLocation.start).split('\n').length : null; + +describe('Debugger stepping into functions', () => { + const praxisFn = `int double_it(int n) + int result <- n * 2 + return result +end double_it + +int x <- 5 +int y <- double_it(x) +print y +`; + + it('steps through the function body line by line for a call in an assignment', () => { + const steps = collectSteps('praxis', praxisFn); + const lines = steps.map((s) => lineOf(praxisFn, s)); + // call site announced (7), body assignment (2), return (3), back at call site (7) + expect(lines).toEqual([6, 7, 2, 3, 7, 8, null]); + expect(steps[steps.length - 1].output).toEqual(['10']); + }); + + it('shows the function locals while inside and destroys them after return', () => { + const steps = collectSteps('praxis', praxisFn); + const bodyStep = steps.find((s) => lineOf(praxisFn, s) === 2)!; + expect(bodyStep.variables).toMatchObject({ n: 5, result: 10, x: 5 }); + + const returnStep = steps.find((s) => s.nodeType === 'Return')!; + expect(returnStep.variables).toMatchObject({ n: 5, result: 10 }); + + // Back at the call site: the function frame's locals are gone. + const afterReturn = steps[steps.indexOf(returnStep) + 1]; + expect(afterReturn.variables).toEqual({ x: 5, y: 10 }); + expect(afterReturn.callStack).toHaveLength(1); + }); + + it('tracks the call stack while inside the function', () => { + const steps = collectSteps('praxis', praxisFn); + const bodyStep = steps.find((s) => lineOf(praxisFn, s) === 2)!; + expect(bodyStep.callStack.map((f) => f.name)).toEqual(['global', 'double_it']); + expect(bodyStep.callStack[1].variables).toEqual({ n: 5, result: 10 }); + // Function and class declarations never appear as variables. + for (const step of steps) { + expect(step.variables).not.toHaveProperty('double_it'); + } + }); + + it('steps into a call used inside a print statement', () => { + const code = `int double_it(int n) + return n * 2 +end double_it + +print double_it(4) +`; + const steps = collectSteps('praxis', code); + expect(steps.some((s) => s.nodeType === 'Return' && lineOf(code, s) === 2)).toBe(true); + expect(steps[steps.length - 1].output).toEqual(['8']); + }); + + it('steps into a call used inside an if condition', () => { + const code = `boolean big(int n) + return n > 10 +end big + +if (big(20)) + print "big" +end if +`; + const steps = collectSteps('praxis', code); + expect(steps.some((s) => s.nodeType === 'Return' && lineOf(code, s) === 2)).toBe(true); + expect(steps[steps.length - 1].output).toEqual(['big']); + }); + + it('steps through nested calls innermost-first', () => { + const code = `int inc(int n) + return n + 1 +end inc + +int twice(int n) + return n * 2 +end twice + +print twice(inc(3)) +`; + const steps = collectSteps('praxis', code); + const frameNames = steps + .filter((s) => s.nodeType === 'Return') + .map((s) => s.callStack[s.callStack.length - 1].name); + expect(frameNames).toEqual(['inc', 'twice']); + expect(steps[steps.length - 1].output).toEqual(['8']); + }); + + it('stacks frames for recursive calls', () => { + const code = `int fact(int n) + if (n <= 1) + return 1 + end if + return n * fact(n - 1) +end fact + +print fact(3) +`; + const steps = collectSteps('praxis', code); + const deepest = Math.max(...steps.map((s) => s.callStack.length)); + expect(deepest).toBe(4); // global + fact(3) + fact(2) + fact(1) + expect(steps[steps.length - 1].output).toEqual(['6']); + }); + + it('produces the same output as a normal run when a call mixes with arithmetic', () => { + const code = `int askQ(int n) + print "asked" + return n +end askQ + +int score <- 1 +score <- score + askQ(10) +print score +`; + const steps = collectSteps('praxis', code); + // The call body ran exactly once (one "asked"), and arithmetic used its result. + expect(steps[steps.length - 1].output).toEqual(['asked', '11']); + }); +}); + +describe('Debugger stepping into methods (Java / Python)', () => { + it('steps into a sibling static method called from main()', () => { + const code = `public class Main { + public static int doubleIt(int n) { + int result = n * 2; + return result; + } + + public static void main(String[] args) { + int x = 5; + int y = doubleIt(x); + System.out.println(y); + } +} +`; + const steps = collectSteps('java', code); + expect(steps[steps.length - 1].output).toEqual(['10']); + + const bodyStep = steps.find((s) => lineOf(code, s) === 3)!; + expect(bodyStep).toBeDefined(); + expect(bodyStep.callStack.map((f) => f.name)).toEqual(['global', 'main', 'doubleIt']); + expect(bodyStep.callStack[2].variables).toEqual({ n: 5, result: 10 }); + }); + + it('steps into an instance method called on an object', () => { + const code = `class Counter: + def __init__(self): + self.count = 0 + + def bump(self, amount): + self.count = self.count + amount + return self.count + +c = Counter() +print(c.bump(2)) +`; + const steps = collectSteps('python', code); + expect(steps[steps.length - 1].output).toEqual(['2']); + const inMethod = steps.find((s) => s.callStack[s.callStack.length - 1]?.name === 'bump')!; + expect(inMethod).toBeDefined(); + expect(inMethod.callStack[1].variables).toMatchObject({ amount: 2 }); + }); +}); + +describe('Debugger control flow', () => { + it('handles break inside a while loop without aborting the program', () => { + const code = `int i <- 0 +while (i < 10) + if (i == 2) + break + end if + print i + i <- i + 1 +end while +print "done" +`; + const steps = collectSteps('praxis', code); + expect(steps[steps.length - 1].output).toEqual(['0', '1', 'done']); + expect(steps.some((s) => s.nodeType === 'Break')).toBe(true); + }); + + it('yields a step for the return line itself', () => { + const code = `void hello() + print "hi" + return +end hello + +hello() +`; + const steps = collectSteps('praxis', code); + expect(steps.some((s) => s.nodeType === 'Return')).toBe(true); + expect(steps[steps.length - 1].output).toEqual(['hi']); + }); + + it('reports arity errors the same way a normal run does', () => { + const code = `int inc(int n) + return n + 1 +end inc + +print inc(1, 2) +`; + const steps = collectSteps('praxis', code); + const last = steps[steps.length - 1]; + expect(last.isComplete).toBe(true); + expect(last.output.join('\n')).toContain('Expected 1 arguments but got 2'); + }); +}); + +describe('Debugger input handling inside functions', () => { + it('pauses for input() inside a function body and resumes after provideInput', () => { + const code = `String ask() + String name <- input("name?") + return name +end ask + +String who <- ask() +print who +`; + const dbg = new Debugger(); + dbg.init(parse('praxis', code), 'praxis', code); + + let step: DebugStep | null = null; + for (let i = 0; i < 20; i++) { + step = dbg.step(); + expect(step).not.toBeNull(); + if (step!.waitingForInput) break; + } + expect(step!.waitingForInput).toBe(true); + expect(step!.inputPrompt).toBe('name?'); + // We paused inside the function: its frame is on the stack. + expect(step!.callStack.map((f) => f.name)).toEqual(['global', 'ask']); + + dbg.provideInput('Ada'); + const after: DebugStep[] = []; + for (let i = 0; i < 20; i++) { + const s = dbg.step(); + if (!s) break; + after.push(s); + if (s.isComplete) break; + } + const last = after[after.length - 1]; + expect(last.isComplete).toBe(true); + expect(last.output).toEqual(['> Ada', 'Ada']); + }); +}); + +describe('Debugger translation-panel highlighting (emitter source maps)', () => { + const TARGETS = ['python', 'javascript', 'java', 'csp', 'praxis'] as const; + + it('maps a nested else-if to the elif / else-if line in every target', () => { + // The newScore example: the nested If inside `else` is emitted as a + // collapsed elif / else-if chain by the emitters. + const code = `int newScore ( int diceOne, int diceTwo, int oldScore ) + if ( diceOne == diceTwo ) + return 0 + else + if ( ( diceOne == 6 ) or ( diceTwo == 6 ) ) + return oldScore + else + return oldScore + diceOne + diceTwo + end if + end if +end newScore + +print newScore(1, 2, 3) +`; + const program = parse('praxis', code); + + // Step until the debugger sits on the nested If (the second If yielded). + const dbg = new Debugger(); + dbg.init(program, 'praxis', code); + let nestedIf: DebugStep | undefined; + for (let i = 0; i < 50; i++) { + const s = dbg.step(); + if (!s || s.isComplete) break; + if (s.nodeType === 'If' && lineOf(code, s) === 5) nestedIf = s; + } + expect(nestedIf).toBeDefined(); + + const expectedLineText: Record = { + python: 'elif', + javascript: 'else if', + java: 'else if', + csp: 'ELSE IF', + praxis: 'else if', + }; + for (const target of TARGETS) { + const { code: out, sourceMap } = new Translator().translateWithMap(program, target); + const line = sourceMap.get(nestedIf!.nodeId); + expect(line, `${target} should map the nested If`).toBeDefined(); + expect(out.split('\n')[line!], `${target} line ${line}`).toContain(expectedLineText[target]); + } + }); + + it('maps every debugger step to a line in every target for class-free code', () => { + const code = `int helper(int n) + if (n > 3) + return n + else if (n > 1) + return n * 10 + else + return 0 + end if +end helper + +int i <- 0 +while (i < 3) + if (i == 1) + i <- i + 2 + continue + end if + print helper(i) + i <- i + 1 +end while + +for (int j <- 0; j < 4; j <- j + 1) + if (j == 2) + break + end if + print j +end for + +do + i <- i - 1 +while (i > 2) + +repeat + i <- i + 1 +until (i > 4) +`; + // Node ids are random per parse, so the debugger and the translators must + // share one parsed program — exactly as the editor pages do. + const program = parse('praxis', code); + const maps = TARGETS.map( + (t) => [t, new Translator().translateWithMap(program, t).sourceMap] as const + ); + + const dbg = new Debugger(); + dbg.init(program, 'praxis', code); + for (let i = 0; i < 500; i++) { + const step = dbg.step(); + if (!step || step.isComplete) break; + if (!step.nodeId) continue; + for (const [target, map] of maps) { + expect( + map.get(step.nodeId), + `${target} is missing a line for ${step.nodeType} (praxis line ${lineOf(code, step)})` + ).toBeDefined(); + } + } + }); +}); diff --git a/tests/java.test.ts b/tests/java.test.ts index 31cf1d4..76d2b7b 100644 --- a/tests/java.test.ts +++ b/tests/java.test.ts @@ -10,6 +10,25 @@ import { PraxisLexer } from '../src/language/praxis/lexer'; import { PraxisParser } from '../src/language/praxis/parser'; import { Interpreter } from '../src/language/interpreter'; +// Real Java requires every statement to live inside a method, and the runtime +// entry point must be `public class Main { public static void main(String[] args) { ... } }`. +// Most test sources below are single statements/expressions, so wrap them in that +// boilerplate rather than repeating it inline everywhere. +const wrapMain = (body: string): string => + `public class Main {\n public static void main(String[] args) {\n${body}\n }\n}`; + +// Structural parser assertions used to check `program.body` directly; now that +// top-level statements live inside Main.main, drill into that method's block. +const mainBodyOf = (program: any): any[] => { + const mainClass = program.body.find( + (s: any) => s.type === 'ClassDeclaration' && s.name === 'Main' + ); + const mainMethod = mainClass.body.find( + (m: any) => m.type === 'MethodDeclaration' && m.name === 'main' + ); + return mainMethod.body.body; +}; + describe('Java Lexer', () => { describe('Basic Tokens', () => { it('should tokenize numbers', () => { @@ -110,53 +129,55 @@ describe('Java Parser', () => { describe('Statements', () => { it('should parse variable declaration', () => { - const lexer = new JavaLexer('int x = 5;'); + const lexer = new JavaLexer(wrapMain('int x = 5;')); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); const program = parser.parse(); - expect(program.body).toHaveLength(1); - expect(program.body[0].type).toBe('Assignment'); + const body = mainBodyOf(program); + expect(body).toHaveLength(1); + expect(body[0].type).toBe('Assignment'); }); it('should parse if statement', () => { - const source = `int x = 7; + const source = wrapMain(`int x = 7; if (x < 10) { x++; -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); const program = parser.parse(); - expect(program.body.some((s) => s.type === 'If')).toBe(true); + expect(mainBodyOf(program).some((s) => s.type === 'If')).toBe(true); }); it('should parse while loop', () => { - const source = `int i = 0; + const source = wrapMain(`int i = 0; while (i < 10) { i++; -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); const program = parser.parse(); - expect(program.body.some((s) => s.type === 'While')).toBe(true); + expect(mainBodyOf(program).some((s) => s.type === 'While')).toBe(true); }); it('should parse for loop', () => { - const source = `for (int i = 0; i < 10; i++) { -}`; + const source = wrapMain(`for (int i = 0; i < 10; i++) { +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); const program = parser.parse(); - expect(program.body[0].type).toBe('For'); + expect(mainBodyOf(program)[0].type).toBe('For'); }); }); describe('Classes', () => { it('should parse class declaration', () => { - const source = `public class MyClass { + const source = `public class Main { int x = 0; + public static void main(String[] args) {} }`; const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); @@ -166,7 +187,8 @@ while (i < 10) { }); it('should parse method', () => { - const source = `public class MyClass { + const source = `public class Main { + public static void main(String[] args) {} public void myMethod() { System.out.println("test"); } @@ -179,12 +201,65 @@ while (i < 10) { expect(classDecl.body.some((m: any) => m.type === 'MethodDeclaration')).toBe(true); }); }); + + describe('Entry point requirement', () => { + it('rejects bare top-level statements', () => { + const source = `int x = 5;`; + expect(() => new JavaParser(new JavaLexer(source).tokenize()).parse()).toThrow( + /class declaration/ + ); + }); + + it('rejects a program with no Main class', () => { + const source = `public class Helper { + int x = 0; +}`; + expect(() => new JavaParser(new JavaLexer(source).tokenize()).parse()).toThrow( + /must define a 'Main' class/ + ); + }); + + it('rejects a Main class without a valid main method', () => { + const source = `public class Main { + public void main(String[] args) {} +}`; + expect(() => new JavaParser(new JavaLexer(source).tokenize()).parse()).toThrow( + /must define an entry point/ + ); + }); + + it('accepts a Main class alongside other classes', () => { + const source = `public class Helper { + int x; +} +public class Main { + public static void main(String[] args) {} +}`; + const program = new JavaParser(new JavaLexer(source).tokenize()).parse(); + expect(program.body.map((s: any) => s.type)).toEqual([ + 'ClassDeclaration', + 'ClassDeclaration', + ]); + }); + + it('accepts and discards import declarations', () => { + const source = `import java.util.Scanner; +import java.util.ArrayList; + +public class Main { + public static void main(String[] args) {} +}`; + const program = new JavaParser(new JavaLexer(source).tokenize()).parse(); + expect(program.body).toHaveLength(1); + expect(program.body[0].type).toBe('ClassDeclaration'); + }); + }); }); describe('Java Emitter', () => { describe('Expressions', () => { it('should emit arithmetic expression', () => { - const source = `5 + 3`; + const source = wrapMain(`System.out.println(5 + 3);`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -203,7 +278,7 @@ describe('Java Emitter', () => { }); it('should emit power operation as Math.pow', () => { - const source = `2 ** 8`; + const source = wrapMain(`System.out.println(2 ** 8);`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -222,7 +297,7 @@ describe('Java Emitter', () => { describe('Statements', () => { it('should emit variable declaration', () => { - const source = `int x;`; + const source = wrapMain(`int x;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -240,9 +315,9 @@ describe('Java Emitter', () => { }); it('should emit if statement', () => { - const source = `if (x < 10) { + const source = wrapMain(`if (x < 10) { x++; -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -265,7 +340,7 @@ describe('Java Emitter', () => { describe('Java Translation', () => { describe('Basic Programs', () => { it('should translate simple assignment', () => { - const source = `int x = 5;`; + const source = wrapMain(`int x = 5;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -277,7 +352,7 @@ describe('Java Translation', () => { }); it('should translate print statement', () => { - const source = `System.out.println("hello");`; + const source = wrapMain(`System.out.println("hello");`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -288,7 +363,7 @@ describe('Java Translation', () => { }); it('should not add implicit defaults for uninitialized Java declarations', () => { - const source = `int x;`; + const source = wrapMain(`int x;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -301,7 +376,7 @@ describe('Java Translation', () => { }); it('should emit Python type hint only for uninitialized Java declarations', () => { - const source = `int x;\nint y = 3;`; + const source = wrapMain(`int x;\nint y = 3;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -317,10 +392,10 @@ describe('Java Translation', () => { describe('Control Flow', () => { it('should translate if statement correctly', () => { - const source = `int x = 7; + const source = wrapMain(`int x = 7; if (x < 10) { x++; -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -334,7 +409,7 @@ if (x < 10) { describe('Arrays', () => { it('should translate array declaration', () => { - const source = `int[] xs = {12, 103, 80};`; + const source = wrapMain(`int[] xs = {12, 103, 80};`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -346,8 +421,8 @@ if (x < 10) { }); it('should translate array access', () => { - const source = `int[] xs = {12, 103, 80}; -System.out.println(xs[0]);`; + const source = wrapMain(`int[] xs = {12, 103, 80}; +System.out.println(xs[0]);`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -362,6 +437,9 @@ System.out.println(xs[0]);`; it('should translate simple class', () => { const source = `public class Count { public int count = 0; +} +public class Main { + public static void main(String[] args) {} }`; const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); @@ -380,6 +458,9 @@ System.out.println(xs[0]);`; public void inc() { this.count = this.count + 1; } +} +public class Main { + public static void main(String[] args) {} }`; const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); @@ -397,6 +478,9 @@ System.out.println(xs[0]);`; public int add(int x, double y) { return x; } +} +public class Main { + public static void main(String[] args) {} }`; const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); @@ -475,9 +559,9 @@ public class Main { describe('Advanced Features', () => { it('should translate multiple statements in c-style for loop', () => { - const source = `for (int i = 0, j = 10; i < j; i++, j--) { + const source = wrapMain(`for (int i = 0, j = 10; i < j; i++, j--) { System.out.println(i); -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -490,7 +574,9 @@ public class Main { }); it('should translate switch-case with fall through', () => { - const source = `switch (x) { + const source = wrapMain(`int x = 1; +int y = 0; +switch (x) { case 1: y = 10; case 2: @@ -498,7 +584,7 @@ public class Main { break; default: y = 0; -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -511,16 +597,19 @@ public class Main { }); it('parses and re-emits a char literal', () => { - const source = `char grade = 'A';`; + const source = wrapMain(`char grade = 'A';`); const program = new JavaParser(new JavaLexer(source).tokenize()).parse(); const result = new Translator().translate(program, 'java'); expect(result).toContain("char grade = 'A'"); }); it('should translate other compound assignment operators', () => { - const source = `x *= 2; + const source = wrapMain(`int x = 0; +int y = 10; +int z = 20; +x *= 2; y /= 3; -z %= 5;`; +z %= 5;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -533,7 +622,9 @@ z %= 5;`; }); it('should translate ternary operator', () => { - const source = `int max = a > b ? a : b;`; + const source = wrapMain(`int a = 1; +int b = 2; +int max = a > b ? a : b;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -545,8 +636,8 @@ z %= 5;`; }); it('should translate array element mutation', () => { - const source = `int[] nums = {1, 2, 3}; -nums[1] = 4;`; + const source = wrapMain(`int[] nums = {1, 2, 3}; +nums[1] = 4;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -559,8 +650,9 @@ nums[1] = 4;`; }); it('should translate compound assignment correctly', () => { - const source = `int total = 0; -total += k;`; + const source = wrapMain(`int total = 0; +int k = 5; +total += k;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -572,9 +664,10 @@ total += k;`; }); it('should translate negated method call', () => { - const source = `if (!password.equals("ABC123")) { + const source = wrapMain(`String password = "abc"; +if (!password.equals("ABC123")) { System.out.println("Invalid"); -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -586,10 +679,10 @@ total += k;`; }); it('should use .equals() for String equality comparison', () => { - const source = `String name = "John"; + const source = wrapMain(`String name = "John"; if (name == "John") { System.out.println("Match"); -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -600,11 +693,11 @@ if (name == "John") { }); it('should handle String inequality with .equals()', () => { - const source = `String a = "test"; + const source = wrapMain(`String a = "test"; String b = "other"; if (a != b) { System.out.println("Different"); -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -616,7 +709,9 @@ if (a != b) { }); it('should support ternary operators', () => { - const source = `int max = a > b ? a : b;`; + const source = wrapMain(`int a = 1; +int b = 2; +int max = a > b ? a : b;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -628,9 +723,9 @@ if (a != b) { }); it('should handle multiple statements in c-style for loop', () => { - const source = `for (int i = 0, j = 10; i < j; i++, j--) { + const source = wrapMain(`for (int i = 0, j = 10; i < j; i++, j--) { System.out.println(i); -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -643,14 +738,15 @@ if (a != b) { }); it('should add break statements in switch cases', () => { - const source = `switch (x) { + const source = wrapMain(`int x = 1; +switch (x) { case 1: System.out.println("one"); case 2: System.out.println("two"); default: System.out.println("other"); -}`; +}`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -660,9 +756,9 @@ if (a != b) { expect(result).toContain('break'); }); - it('should translate array element mutation', () => { - const source = `int[] nums = {1, 2, 3}; -nums[0] = 5;`; + it('should translate array element mutation (index 0)', () => { + const source = wrapMain(`int[] nums = {1, 2, 3}; +nums[0] = 5;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -674,11 +770,11 @@ nums[0] = 5;`; }); it('should translate compound assignment operators', () => { - const source = `int x = 10; + const source = wrapMain(`int x = 10; x += 5; x -= 3; x *= 2; -x /= 4;`; +x /= 4;`); const lexer = new JavaLexer(source); const tokens = lexer.tokenize(); const parser = new JavaParser(tokens); @@ -762,35 +858,41 @@ describe('Java braceless control-flow bodies', () => { it('braceless if does not swallow the following statement (condition false)', () => { // Regression: `block()` used to read to EOF when no `{` was present, absorbing // the trailing println into the if-body so it was skipped when the guard failed. - expect(runJava('int x = -1; if (x > 0) x = 5; System.out.println(x);')).toEqual(['-1']); + expect(runJava(wrapMain('int x = -1; if (x > 0) x = 5; System.out.println(x);'))).toEqual([ + '-1', + ]); }); it('braceless if runs its body and the sibling (condition true)', () => { - expect(runJava('int x = 1; if (x > 0) x = 5; System.out.println(x);')).toEqual(['5']); + expect(runJava(wrapMain('int x = 1; if (x > 0) x = 5; System.out.println(x);'))).toEqual(['5']); }); it('parses a braceless if body as exactly one statement', () => { - const program = parseJava('int x = 1; if (x > 0) x = 5; System.out.println(x);'); + const program = parseJava(wrapMain('int x = 1; if (x > 0) x = 5; System.out.println(x);')); // Statements: the declaration, the if, and the println as a sibling (not absorbed). - expect(program.body.map((s: any) => s.type)).toEqual(['Assignment', 'If', 'Print']); - const ifStmt = program.body[1] as any; + const body = mainBodyOf(program); + expect(body.map((s: any) => s.type)).toEqual(['Assignment', 'If', 'Print']); + const ifStmt = body[1] as any; expect(ifStmt.thenBranch.body).toHaveLength(1); expect(ifStmt.thenBranch.body[0].type).toBe('Assignment'); }); it('braceless if/else picks the right branch and keeps the sibling', () => { - const src = - 'int x = -1; if (x > 0) System.out.println("pos"); else System.out.println("neg"); System.out.println("after");'; + const src = wrapMain( + 'int x = -1; if (x > 0) System.out.println("pos"); else System.out.println("neg"); System.out.println("after");' + ); expect(runJava(src)).toEqual(['neg', 'after']); }); it('braceless while body does not swallow the following statement', () => { - expect(runJava('int i = 0; while (i < 3) i++; System.out.println(i);')).toEqual(['3']); + expect(runJava(wrapMain('int i = 0; while (i < 3) i++; System.out.println(i);'))).toEqual([ + '3', + ]); }); it('braceless for body does not swallow the following statement', () => { expect( - runJava('int s = 0; for (int i = 0; i < 3; i++) s += i; System.out.println(s);') + runJava(wrapMain('int s = 0; for (int i = 0; i < 3; i++) s += i; System.out.println(s);')) ).toEqual(['3']); }); }); @@ -822,3 +924,92 @@ describe('Java random (seeded, deterministic)', () => { expect(out).toEqual(['true']); }); }); + +describe('Java uninitialized variables', () => { + const runJava = (src: string): string[] => + new Interpreter().interpret(new JavaParser(new JavaLexer(src).tokenize()).parse(), src); + + it('reading a declared-but-unassigned variable is a runtime error, not a default value', () => { + const out = runJava(wrapMain('int x;\nSystem.out.println(x);')); + expect(out.join('\n')).toMatch(/uninitialized variable 'x'/i); + }); + + it('assigning it first, then reading it, works fine', () => { + const out = runJava(wrapMain('int x;\nx = 5;\nSystem.out.println(x);')); + expect(out).toEqual(['5']); + }); + + it('an unassigned field read via `this` is also a runtime error', () => { + const out = runJava(`public class Counter { + private int count; + public int read() { + return this.count; + } +} +public class Main { + public static void main(String[] args) { + Counter c = new Counter(); + System.out.println(c.read()); + } +}`); + expect(out.join('\n')).toMatch(/uninitialized field 'count'/i); + }); +}); + +describe('Java Main class unwrapping in translations', () => { + const diceProgram = `public class Main { + static int bonus = 7; + + public static int newScore(int diceOne, int diceTwo, int oldScore) { + if (diceOne == diceTwo) { + return 0; + } else if (diceOne == 6 || diceTwo == 6) { + return oldScore; + } else { + return oldScore + diceOne + diceTwo; + } + } + + public static void main(String[] args) { + System.out.println(newScore(1, 2, 3)); + System.out.println(newScore(4, 4, 3)); + System.out.println(newScore(6, 2, 3) + bonus); + } +}`; + + const parseJava = (code: string) => new JavaParser(new JavaLexer(code).tokenize()).parse(); + + it('emits static helper methods as free functions in every target', () => { + const program = parseJava(diceProgram); + const expectations: Array<[string, RegExp]> = [ + ['python', /def newScore\(/], + ['praxis', /int newScore\(/], + ['javascript', /function newScore\(/], + ['csp', /PROCEDURE newScore/], + ]; + for (const [target, pattern] of expectations) { + const out = new Translator().translate(program, target as any); + expect(out, `${target} should keep newScore`).toMatch(pattern); + } + }); + + it('emits static fields as top-level declarations', () => { + const program = parseJava(diceProgram); + expect(new Translator().translate(program, 'python')).toMatch(/^bonus = 7$/m); + expect(new Translator().translate(program, 'praxis')).toMatch(/int bonus ← 7/); + }); + + it('translated programs reparse and produce the same output as the Java run', () => { + const program = parseJava(diceProgram); + const javaOutput = new Interpreter().interpret(program, diceProgram); + expect(javaOutput).toEqual(['6', '0', '10']); + + const pyCode = new Translator().translate(program, 'python'); + const pyProgram = new PythonParser(new PythonLexer(pyCode).tokenize()).parse(); + expect(new Interpreter().interpret(pyProgram, pyCode)).toEqual(javaOutput); + + const praxCode = new Translator().translate(program, 'praxis'); + const praxProgram = new PraxisParser(new PraxisLexer(praxCode).tokenize()).parse(); + expect(new Interpreter().interpret(praxProgram, praxCode)).toEqual(javaOutput); + }); +}); diff --git a/tests/javascript.test.ts b/tests/javascript.test.ts index fdf8395..55c310b 100644 --- a/tests/javascript.test.ts +++ b/tests/javascript.test.ts @@ -583,35 +583,40 @@ describe('Translation to JavaScript', () => { const ast = new JavaParser(tokens).parse(); return translateToJS(ast); } + const wrapMain = (body: string) => + `public class Main {\n public static void main(String[] args) {\n${body}\n }\n}`; it('translates System.out.println to console.log', () => { - const out = javaToJS('System.out.println("hi");'); + const out = javaToJS(wrapMain('System.out.println("hi");')); expect(out).toContain('console.log("hi")'); }); it('translates typed variable declaration', () => { - const out = javaToJS('int x = 5;'); + const out = javaToJS(wrapMain('int x = 5;')); expect(out).toContain('5'); }); it('translates for loop', () => { - const out = javaToJS('for (int i = 0; i < 3; i++) { System.out.println(i); }'); + const out = javaToJS(wrapMain('for (int i = 0; i < 3; i++) { System.out.println(i); }')); expect(out).toContain('for ('); expect(out).toContain('console.log(i)'); }); it('translates if / else', () => { const out = javaToJS( - 'if (x > 0) { System.out.println("pos"); } else { System.out.println("neg"); }' + wrapMain('if (x > 0) { System.out.println("pos"); } else { System.out.println("neg"); }') ); expect(out).toContain('if (x > 0)'); }); it('translates function inside a class', () => { const src = ` - public class Main { + public class MathUtil { public static int add(int a, int b) { return a + b; } } + public class Main { + public static void main(String[] args) {} + } `; const out = javaToJS(src); expect(out).toMatch(/add\s*\(/); diff --git a/tests/placeholder.test.ts b/tests/placeholder.test.ts index 029d484..5ac8c0c 100644 --- a/tests/placeholder.test.ts +++ b/tests/placeholder.test.ts @@ -1,8 +1,14 @@ /** * Praxis slash-star placeholder: a hole for missing exam-question code. It - * parses to a Placeholder expression node, evaluates to a default 0 so programs - * with holes still run, is preserved verbatim in Praxis output, and lowers to - * `0` in every other target (which don't support the placeholder syntax). + * parses to a Placeholder expression node. Standing alone as a statement + * (e.g. a missing loop body line) it's a no-op. Used directly as part of a + * real expression, interpreting it is a runtime error — assigning it to a + * variable stores an uninitialized sentinel (reading the variable later is + * the error), and using it directly (a condition, an operand, ...) errors + * immediately. It is preserved verbatim in Praxis output, and still lowers to + * `0` when translated to every other target (which don't support the + * placeholder syntax) — translation is a text-emission concern, not + * interpretation, so the emitted default is unchanged. */ import { describe, it, expect } from 'vitest'; @@ -22,9 +28,19 @@ describe('Praxis placeholder', () => { expect(ast.body[0].value.text).toBe('fill me'); }); - it('evaluates to 0 so a program with holes still runs', () => { - expect(run('int x <- /* missing */\nprint(x)')).toEqual(['0']); - expect(run('if (/* cond */)\n print("t")\nelse\n print("f")\nend if')).toEqual(['f']); + it('assigning a placeholder is uninitialized; reading it later is an error', () => { + const out = run('int x <- /* missing */\nprint(x)'); + expect(out.join('\n')).toMatch(/uninitialized variable 'x'/i); + }); + + it('using a placeholder directly (e.g. as a condition) is an immediate error', () => { + const out = run('if (/* cond */)\n print("t")\nelse\n print("f")\nend if'); + expect(out.join('\n')).toMatch(/uninitialized value/i); + }); + + it('standing alone as a statement is a no-op, not an error', () => { + const out = run('print "Before"\n/* missing code */\nprint "After"'); + expect(out.join('\n')).toBe('Before\nAfter'); }); it('is preserved verbatim in Praxis output', () => { @@ -34,6 +50,6 @@ describe('Praxis placeholder', () => { it('lowers to a default 0 in other targets', () => { expect(to('x <- /* hole */\nprint(x)', 'python')).toContain('x = 0'); expect(to('int x <- /* hole */\nprint(x)', 'java')).toContain('int x = 0;'); - expect(to('x <- /* hole */\nprint(x)', 'csp')).toContain('x <- 0'); + expect(to('x <- /* hole */\nprint(x)', 'csp')).toContain('x ← 0'); }); }); diff --git a/tests/praxis.test.ts b/tests/praxis.test.ts index 23e8ad1..646ed2a 100644 --- a/tests/praxis.test.ts +++ b/tests/praxis.test.ts @@ -190,7 +190,7 @@ end while`; }); it('should parse for loop', () => { - const source = `for i <- 0; i < 10; i <- i + 1 + const source = `for (i <- 0; i < 10; i <- i + 1) print(i) end for`; const lexer = new PraxisLexer(source); @@ -200,6 +200,26 @@ end for`; expect(program.body[0].type).toBe('For'); }); + it('should reject a for loop header missing parentheses', () => { + const source = `for i <- 0; i < 5; i <- i + 1 + print(i) +end for`; + const lexer = new PraxisLexer(source); + const tokens = lexer.tokenize(); + const parser = new PraxisParser(tokens); + expect(() => parser.parse()).toThrow(); + }); + + it('should reject an if statement missing parentheses', () => { + const source = `if x > 5 + print(x) +end if`; + const lexer = new PraxisLexer(source); + const tokens = lexer.tokenize(); + const parser = new PraxisParser(tokens); + expect(() => parser.parse()).toThrow(); + }); + it('should parse an else-if chain as nested If nodes', () => { const source = `if (x == 1) print("a") @@ -284,7 +304,7 @@ end add`; }); emitter.visitProgram(program); const code = emitter.getGeneratedCode(); - expect(code).toContain('<-'); + expect(code).toContain('←'); }); it('should emit if statement', () => { @@ -341,7 +361,7 @@ describe('Praxis Translation', () => { const result = translator.translate(program, 'praxis'); expect(result).toContain('int'); expect(result).toContain('x'); - expect(result).toContain('<-'); + expect(result).toContain('←'); }); it('should translate print statement', () => { @@ -482,7 +502,7 @@ print(numbers[2])`; it('should handle array iteration with 0-based indexing', () => { const source = `int[] xs <- {1, 2, 3} -for i <- 0; i < 3; i <- i + 1 +for (i <- 0; i < 3; i <- i + 1) print(xs[i]) end for`; const lexer = new PraxisLexer(source); @@ -497,7 +517,7 @@ end for`; it('should correctly handle 0-based array access in for loops', () => { const source = `int[] items <- {10, 20, 30} -for i <- 0; i < 3; i <- i + 1 +for (i <- 0; i < 3; i <- i + 1) print(items[i]) end for`; const lexer = new PraxisLexer(source); @@ -670,7 +690,7 @@ end class Box`; const praxis = new Translator().translate(program, 'praxis'); expect(praxis).toContain('public Box(int v)'); expect(praxis).toContain('end Box'); - expect(praxis).toContain('value <- v'); + expect(praxis).toContain('value ← v'); expect(praxis).not.toContain('this.'); expect(praxis).not.toContain('procedure new'); }); @@ -704,10 +724,13 @@ print(s.label())`; public String name; public Tag(String name) { this.name = name; } public String get() { return name; } +} +public class Main { + public static void main(String[] args) {} }`; const program = new JavaParser(new JavaLexer(src).tokenize()).parse(); const praxis = new Translator().translate(program, 'praxis'); - expect(praxis).toContain('this.name <- name'); // shadowed -> qualified + expect(praxis).toContain('this.name ← name'); // shadowed -> qualified expect(praxis).toContain('return name'); // not shadowed -> bare }); }); diff --git a/tests/round-trip.test.ts b/tests/round-trip.test.ts index 6774b81..2562ba2 100644 --- a/tests/round-trip.test.ts +++ b/tests/round-trip.test.ts @@ -141,7 +141,9 @@ else: def add(x, y): return x + y print(add(4, 5))`, - java: `String s = "hi " + "csp"; + java: `public class Main { + public static void main(String[] args) { +String s = "hi " + "csp"; System.out.println(s); int a = 20; int b = 6; @@ -171,6 +173,8 @@ if (a >= 100) { System.out.println("med"); } else { System.out.println("small"); +} + } }`, javascript: `let s = "hi " + "csp"; console.log(s);