Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/org/editor/EditorWindow.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.editor;

import org.editor.panels.DockablePanel;
import org.editor.panels.DashboardPanel;
import com.formdev.flatlaf.FlatLightLaf;
import com.vlsolutions.swing.docking.Dockable;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.editor;
package org.editor.panels;

import com.vlsolutions.swing.docking.DockKey;
import com.vlsolutions.swing.docking.Dockable;
Expand Down
108 changes: 91 additions & 17 deletions src/main/java/org/piccode/ast/WhenAst.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.piccode.ast;

import java.util.List;
import org.piccode.rt.PiccodeException;
import org.piccode.rt.PiccodeValue;

import org.piccode.rt.*;
import java.util.*;


/**
*
* @author hexaredecimal
*/
public class WhenAst implements Ast {

public Ast cond;
public List<WhenCase> cases;
public Ast else_case;
Expand All @@ -22,11 +25,10 @@ public WhenAst(Ast cond, List<WhenCase> cases, Ast else_case) {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("when ").append(cond).append("{\n");
for (var when_c: cases) {
sb.append("when ").append(cond).append(" {\n");
for (var when_c : cases) {
sb.append(when_c.toString().indent(4));
}

if (else_case != null) {
sb.append(String.format("else %s", else_case).indent(4));
}
Expand All @@ -38,31 +40,103 @@ public String toString() {
public PiccodeValue execute() {
var cond_value = cond.execute();

for (var match_case: cases) {
if (isMatching(match_case.match, cond_value)) {
return match_case.value.execute();
for (var match_case : cases) {
var tempSymtable = new HashMap<String, PiccodeValue>();
if (isMatching(match_case.match, cond_value, tempSymtable)) {
Context.top.pushStack();
for (var entry : tempSymtable.entrySet()) {
Context.top.putLocal(entry.getKey(), entry.getValue());
}
var result = match_case.value.execute();
Context.top.dropStackFrame();
return result;
}
Comment on lines +44 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure stack-frame cleanup with try/finally

If match_case.value.execute() throws, dropStackFrame() is skipped, leaking the pushed scope and corrupting subsequent evaluation.

-Context.top.pushStack();
-for (var entry : tempSymtable.entrySet()) {
-    Context.top.putLocal(entry.getKey(), entry.getValue());
-}
-var result = match_case.value.execute();
-Context.top.dropStackFrame();
-return result;
+Context.top.pushStack();
+try {
+    for (var entry : tempSymtable.entrySet()) {
+        Context.top.putLocal(entry.getKey(), entry.getValue());
+    }
+    return match_case.value.execute();
+} finally {
+    Context.top.dropStackFrame();
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var tempSymtable = new HashMap<String, PiccodeValue>();
if (isMatching(match_case.match, cond_value, tempSymtable)) {
Context.top.pushStack();
for (var entry : tempSymtable.entrySet()) {
Context.top.putLocal(entry.getKey(), entry.getValue());
}
var result = match_case.value.execute();
Context.top.dropStackFrame();
return result;
}
var tempSymtable = new HashMap<String, PiccodeValue>();
if (isMatching(match_case.match, cond_value, tempSymtable)) {
Context.top.pushStack();
try {
for (var entry : tempSymtable.entrySet()) {
Context.top.putLocal(entry.getKey(), entry.getValue());
}
return match_case.value.execute();
} finally {
Context.top.dropStackFrame();
}
}
🤖 Prompt for AI Agents
In src/main/java/org/piccode/ast/WhenAst.java around lines 44 to 53, the call to
Context.top.dropStackFrame() is skipped if match_case.value.execute() throws an
exception, causing a stack-frame leak. To fix this, wrap the code between
Context.top.pushStack() and Context.top.dropStackFrame() in a try/finally block,
ensuring dropStackFrame() is always called regardless of exceptions.

}

if (else_case == null) {
throw new PiccodeException("Inexhaustive when expression has hit an unexpected state where no pattern was matched: when " + cond + " { ...");
throw new PiccodeException("Inexhaustive when expression: no pattern matched: when " + cond + " { ... }");
}

return else_case.execute();
}

private boolean isMatching(List<Ast> match, PiccodeValue cond_value) {
for (var node: match) {
if (node instanceof IdentifierAst id) {
continue; // TODO: Add Ids to the symtable
private boolean isMatching(List<Ast> patterns, PiccodeValue cond_value, Map<String, PiccodeValue> temp) {
for (Ast pattern : patterns) {
if (matchPattern(pattern, cond_value, temp)) {
return true;
}
}
return false;
}

var value = node.execute();
if (node.equals(cond_value)) {
return true;
private boolean matchPattern(Ast pattern, PiccodeValue value, Map<String, PiccodeValue> temp) {
if (pattern instanceof IdentifierAst id) {
temp.put(id.text, value);
return true;
}

if (pattern instanceof NumberAst lit) {
var litVal = lit.execute();
return Objects.equals(litVal, value);
}

if (pattern instanceof StringAst lit) {
var litVal = lit.execute();
return Objects.equals(litVal, value);
}

if (pattern instanceof TupleAst tup && value instanceof PiccodeTuple vTup) {
var items = tup.nodes;
var vItems = vTup.nodes;
if (items.size() != vItems.size()) {
return false;
}

for (int i = 0; i < items.size(); i++) {
if (!matchPattern(items.get(i), vItems.get(i), temp)) {
return false;
}
}
return true;
}
if (pattern instanceof ArrayAst listPat && value instanceof PiccodeArray listVal) {
var patItems = listPat.nodes;
var valItems = listVal.nodes;
if (patItems.size() != valItems.size()) {
return false;
}

for (int i = 0; i < patItems.size(); i++) {
if (!matchPattern(patItems.get(i), valItems.get(i), temp)) {
return false;
}
}
return true;
}
if (pattern instanceof ListConstAst cons && value instanceof PiccodeArray listVal2) {
if (listVal2.nodes.isEmpty()) {
return false;
}
var head = listVal2.nodes.getFirst();
var tail = new PiccodeArray(listVal2.nodes.subList(1, listVal2.nodes.size()));

return matchPattern(cons.lhs, head, temp) && matchPattern(cons.rhs, tail, temp);
}
if (pattern instanceof ObjectAst objPat && value instanceof PiccodeObject objVal) {
for (var entry : objPat.objs.entrySet()) {
if (!objVal.obj.containsKey(entry.getKey())) {
return false;
}
if (!matchPattern(entry.getValue(), objVal.obj.get(entry.getKey()), temp)) {
return false;
}
}
return true;
}

if (pattern instanceof ArrayAst && value instanceof PiccodeArray vList && vList.nodes.isEmpty()) {
return true;
}
Comment on lines +138 to +139
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Array pattern matches even when the pattern is non-empty

The catch-all branch accepts any ArrayAst pattern whenever the value is an empty list, regardless of whether the pattern itself has elements, resulting in incorrect matches.

Add a check that the pattern contains zero elements:

-if (pattern instanceof ArrayAst && value instanceof PiccodeArray vList && vList.nodes.isEmpty()) {
-    return true;
+if (pattern instanceof ArrayAst arrPat
+        && value instanceof PiccodeArray vList
+        && arrPat.nodes.isEmpty()
+        && vList.nodes.isEmpty()) {
+    return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return true;
}
if (pattern instanceof ArrayAst arrPat
&& value instanceof PiccodeArray vList
&& arrPat.nodes.isEmpty()
&& vList.nodes.isEmpty()) {
return true;
}
🤖 Prompt for AI Agents
In src/main/java/org/piccode/ast/WhenAst.java at lines 138-139, the code
currently returns true for any ArrayAst pattern when the value is an empty list,
even if the pattern is non-empty. To fix this, add a condition to check that the
pattern's element count is zero before returning true, ensuring only empty
patterns match empty lists.

return false;
}
}
2 changes: 1 addition & 1 deletion src/main/java/org/piccode/rt/PiccodeArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @author hexaredecimal
*/
public class PiccodeArray implements PiccodeValue {
private List<PiccodeValue> nodes;
public List<PiccodeValue> nodes;

public PiccodeArray(List<PiccodeValue> nodes) {
this.nodes = nodes;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/piccode/rt/PiccodeObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* @author hexaredecimal
*/
public class PiccodeObject implements PiccodeValue {
private HashMap<String, PiccodeValue> obj;
public HashMap<String, PiccodeValue> obj;

public PiccodeObject(HashMap<String, PiccodeValue> obj) {
this.obj = obj;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/piccode/rt/PiccodeTuple.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @author hexaredecimal
*/
public class PiccodeTuple implements PiccodeValue {
private List<PiccodeValue> nodes;
public List<PiccodeValue> nodes;

public PiccodeTuple(List<PiccodeValue> nodes) {
this.nodes = nodes;
Expand Down
95 changes: 95 additions & 0 deletions src/test/java/org/piccode/ast/TopLevel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.piccode.ast;

import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.editor.AccessFrame;
import org.editor.errors.IDEErrorListener;
import org.junit.jupiter.api.Assertions;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.piccode.antlr4.PiccodeScriptLexer;
import org.piccode.antlr4.PiccodeScriptParser;

/**
*
* @author hexaredecimal
*/
public class TopLevel {
@Test
public void function() {
var code = "function add(x, y) = x + y";
var ast = compile(code);

assertEquals(ast.nodes.size(), 1);
var func = ast.nodes.getFirst();

assertFalse(!(func instanceof FunctionAst));
var node = (FunctionAst) func;
assertEquals(node.name, "add");
assertEquals(node.arg.size(), 2);
assertTrue(node.body instanceof BinOpAst);
}

@Test
public void variable() {
var code = "let foo = 1";
var ast = compile(code);

assertEquals(ast.nodes.size(), 1);
var let = ast.nodes.getFirst();

assertFalse(!(let instanceof VarDecl));
var node = (VarDecl) let;
assertEquals(node.name, "foo");
assertTrue(node.value instanceof NumberAst num && num.text.equals("1"));
}

@Test
public void module() {
var code =
"""
module Foo {
function bar () = ()
}
""";
var ast = compile(code);

assertEquals(ast.nodes.size(), 1);
var mod = ast.nodes.getFirst();

assertFalse(!(mod instanceof ModuleAst));
var node = (ModuleAst) mod;
assertEquals(node.name, "Foo");
assertEquals(node.nodes.size(), 1);

var inner = node.nodes.getFirst();
assertTrue(inner instanceof FunctionAst);
}

@Test
public void importModule() {
var code = "import pkg:io";
var ast = compile(code);
assertEquals(ast.nodes.size(), 1);
var import_ = ast.nodes.getFirst();
assertFalse(!(import_ instanceof ImportAst));
var node = (ImportAst) import_;
assertEquals(node.pkg, "pkg");
assertEquals(node.module, "io");
}

private static StatementList compile(String code) {
var lexer = new PiccodeScriptLexer(CharStreams.fromString(code));
var parser = new PiccodeScriptParser(new CommonTokenStream(lexer));
lexer.removeErrorListeners();
parser.removeErrorListeners();

IDEErrorListener errorListener = new IDEErrorListener();
lexer.addErrorListener(errorListener);
parser.addErrorListener(errorListener);

var visitor = new PiccodeVisitor();

return (StatementList) visitor.visit(parser.stmts());
}
Comment on lines +81 to +94
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

The compile method should handle exceptions gracefully

The compile method doesn't catch or handle potential exceptions that might occur during parsing. This could cause tests to fail with unhelpful stack traces rather than meaningful assertion failures.

Consider adding exception handling to provide more informative test failures:

 	private static StatementList compile(String code) {
+		try {
 		var lexer = new PiccodeScriptLexer(CharStreams.fromString(code));
 		var parser = new PiccodeScriptParser(new CommonTokenStream(lexer));
 		lexer.removeErrorListeners();
 		parser.removeErrorListeners();

 		IDEErrorListener errorListener = new IDEErrorListener();
 		lexer.addErrorListener(errorListener);
 		parser.addErrorListener(errorListener);

 		var visitor = new PiccodeVisitor();

 		return (StatementList) visitor.visit(parser.stmts());
+		} catch (Exception e) {
+			fail("Failed to compile code: " + code + " - " + e.getMessage());
+			return null; // This line won't be reached due to fail() above
+		}
 	}
🤖 Prompt for AI Agents
In src/test/java/org/piccode/ast/TopLevel.java around lines 81 to 94, the
compile method lacks exception handling for parsing errors, which can lead to
uninformative test failures. Wrap the parsing and visiting logic in a try-catch
block that catches relevant exceptions, then throw a runtime exception or
assertion error with a clear, descriptive message to improve test failure
diagnostics.

}
75 changes: 75 additions & 0 deletions src/test/java/org/piccode/rt/Runtime.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.piccode.rt;

import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.editor.AccessFrame;
import org.editor.errors.IDEErrorListener;
import org.junit.jupiter.api.Assertions;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.piccode.antlr4.PiccodeScriptLexer;
import org.piccode.antlr4.PiccodeScriptParser;
import org.piccode.ast.*;


/**
*
* @author hexaredecimal
*/
public class Runtime {
@Test
public void function() {
var code = "function add(x, y) = x + y";
var ast = compile(code);

assertEquals(ast.nodes.size(), 1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Swap expected/actual arguments in assertEquals

JUnit’s signature is assertEquals(expected, actual). Inverting the order can produce misleading failure messages.

-assertEquals(ast.nodes.size(), 1);
+assertEquals(1, ast.nodes.size());

Apply the same change in the other two occurrences.

Also applies to: 38-38, 53-53

🤖 Prompt for AI Agents
In src/test/java/org/piccode/rt/Runtime.java at lines 25, 38, and 53, the
arguments to assertEquals are reversed; JUnit expects the first argument to be
the expected value and the second to be the actual value. Swap the arguments in
these three lines so that the expected value comes first and the actual value
second to ensure accurate failure messages.

var func = ast.nodes.getFirst();

assertFalse(!(func instanceof FunctionAst));
var node = func.execute();
assertTrue(node instanceof PiccodeClosure);
}

@Test
public void variable() {
var code = "let foo = 1";
var ast = compile(code);

assertEquals(ast.nodes.size(), 1);
var let = ast.nodes.getFirst();

assertFalse(!(let instanceof VarDecl));

Context.top.pushStack();
var node = let.execute();
Context.top.dropStackFrame();
assertTrue(node instanceof PiccodeNumber num && num.toString().equals("1"));
}

@Test
public void importModule() {
var code = "import pkg:io";
var ast = compile(code);
assertEquals(ast.nodes.size(), 1);
var import_ = ast.nodes.getFirst();
assertFalse(!(import_ instanceof ImportAst));
var node = import_.execute();
assertTrue(node instanceof PiccodeBoolean bool && bool.toString().equals("true"));
}

private static StatementList compile(String code) {
var lexer = new PiccodeScriptLexer(CharStreams.fromString(code));
var parser = new PiccodeScriptParser(new CommonTokenStream(lexer));
lexer.removeErrorListeners();
parser.removeErrorListeners();

IDEErrorListener errorListener = new IDEErrorListener();
lexer.addErrorListener(errorListener);
parser.addErrorListener(errorListener);

Comment on lines +66 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Check and assert on syntax errors during compilation

An IDEErrorListener is attached but its collected errors are ignored.
Add a post-parse assertion to fail fast when the input code contains syntax errors.

var visitor = new PiccodeVisitor();
var stmtList = (StatementList) visitor.visit(parser.stmts());

+assertTrue(errorListener.getSyntaxErrorInfos().isEmpty(),
+        () -> "Unexpected syntax errors: " + errorListener.getSyntaxErrorInfos());
+
return stmtList;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
IDEErrorListener errorListener = new IDEErrorListener();
lexer.addErrorListener(errorListener);
parser.addErrorListener(errorListener);
var visitor = new PiccodeVisitor();
var stmtList = (StatementList) visitor.visit(parser.stmts());
assertTrue(errorListener.getSyntaxErrorInfos().isEmpty(),
() -> "Unexpected syntax errors: " + errorListener.getSyntaxErrorInfos());
return stmtList;
🤖 Prompt for AI Agents
In src/test/java/org/piccode/rt/Runtime.java around lines 66 to 69, the
IDEErrorListener is added to the lexer and parser but its collected syntax
errors are not checked. After parsing, add an assertion to check if the
errorListener has recorded any syntax errors and fail the test immediately if
errors exist, ensuring the compilation process fails fast on invalid input.

var visitor = new PiccodeVisitor();

return (StatementList) visitor.visit(parser.stmts());
}

}