diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/lib/AutoCloseableScope.java b/ownlang-core/src/main/java/com/annimon/ownlang/lib/AutoCloseableScope.java new file mode 100644 index 0000000..bde190d --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/lib/AutoCloseableScope.java @@ -0,0 +1,8 @@ +package com.annimon.ownlang.lib; + +public final class AutoCloseableScope implements AutoCloseable { + @Override + public void close() { + ScopeHandler.pop(); + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/lib/ScopeHandler.java b/ownlang-core/src/main/java/com/annimon/ownlang/lib/ScopeHandler.java index 79d6a4b..67debfc 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/lib/ScopeHandler.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/lib/ScopeHandler.java @@ -36,6 +36,11 @@ public final class ScopeHandler { scope = rootScope; } + public static AutoCloseableScope closeableScope() { + push(); + return new AutoCloseableScope(); + } + public static void push() { synchronized (lock) { scope = new Scope(scope); diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/Parser.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/Parser.java index ea6333a..0f70be0 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/Parser.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/Parser.java @@ -22,7 +22,7 @@ public final class Parser { final Parser parser = new Parser(tokens); final Statement program = parser.parse(); if (parser.getParseErrors().hasErrors()) { - throw new ParseException(); + throw new ParseException(parser.getParseErrors().toString()); } return program; } diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/ForeachArrayStatement.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/ForeachArrayStatement.java index 30b4b99..608b301 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/ForeachArrayStatement.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/ForeachArrayStatement.java @@ -23,22 +23,14 @@ public final class ForeachArrayStatement extends InterruptableNode implements St @Override public void execute() { super.interruptionCheck(); - // TODO removing without checking shadowing is dangerous - final Value previousVariableValue = ScopeHandler.getVariable(variable); - - final Value containerValue = container.eval(); - switch (containerValue.type()) { - case Types.STRING -> iterateString(containerValue.asString()); - case Types.ARRAY -> iterateArray((ArrayValue) containerValue); - case Types.MAP -> iterateMap((MapValue) containerValue); - default -> throw new TypeException("Cannot iterate " + Types.typeToString(containerValue.type())); - } - - // Restore variables - if (previousVariableValue != null) { - ScopeHandler.setVariable(variable, previousVariableValue); - } else { - ScopeHandler.removeVariable(variable); + try (final var ignored = ScopeHandler.closeableScope()) { + final Value containerValue = container.eval(); + switch (containerValue.type()) { + case Types.STRING -> iterateString(containerValue.asString()); + case Types.ARRAY -> iterateArray((ArrayValue) containerValue); + case Types.MAP -> iterateMap((MapValue) containerValue); + default -> throw new TypeException("Cannot iterate " + Types.typeToString(containerValue.type())); + } } } diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/ForeachMapStatement.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/ForeachMapStatement.java index b0588ae..816d8f4 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/ForeachMapStatement.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/ForeachMapStatement.java @@ -24,28 +24,14 @@ public final class ForeachMapStatement extends InterruptableNode implements Stat @Override public void execute() { super.interruptionCheck(); - // TODO removing without checking shadowing is dangerous - final Value previousVariableValue1 = ScopeHandler.getVariable(key); - final Value previousVariableValue2 = ScopeHandler.getVariable(value); - - final Value containerValue = container.eval(); - switch (containerValue.type()) { - case Types.STRING -> iterateString(containerValue.asString()); - case Types.ARRAY -> iterateArray((ArrayValue) containerValue); - case Types.MAP -> iterateMap((MapValue) containerValue); - default -> throw new TypeException("Cannot iterate " + Types.typeToString(containerValue.type()) + " as key, value pair"); - } - - // Restore variables - if (previousVariableValue1 != null) { - ScopeHandler.setVariable(key, previousVariableValue1); - } else { - ScopeHandler.removeVariable(key); - } - if (previousVariableValue2 != null) { - ScopeHandler.setVariable(value, previousVariableValue2); - } else { - ScopeHandler.removeVariable(value); + try (final var ignored = ScopeHandler.closeableScope()) { + final Value containerValue = container.eval(); + switch (containerValue.type()) { + case Types.STRING -> iterateString(containerValue.asString()); + case Types.ARRAY -> iterateArray((ArrayValue) containerValue); + case Types.MAP -> iterateMap((MapValue) containerValue); + default -> throw new TypeException("Cannot iterate " + Types.typeToString(containerValue.type()) + " as key, value pair"); + } } } diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/MatchExpression.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/MatchExpression.java index e1a3404..d1602c0 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/MatchExpression.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/MatchExpression.java @@ -54,13 +54,10 @@ public final class MatchExpression extends InterruptableNode implements Expressi } } if ((value.type() == Types.ARRAY) && (p instanceof ListPattern pattern)) { - if (matchListPattern((ArrayValue) value, pattern)) { - // Clean up variables if matched - final Value result = evalResult(p.result); - for (String var : pattern.parts) { - ScopeHandler.removeVariable(var); + try (final var ignored = ScopeHandler.closeableScope()) { + if (matchListPattern((ArrayValue) value, pattern)) { + return evalResult(p.result); } - return result; } } if ((value.type() == Types.ARRAY) && (p instanceof TuplePattern pattern)) { @@ -91,20 +88,12 @@ public final class MatchExpression extends InterruptableNode implements Expressi final int arraySize = array.size(); switch (partsSize) { case 0: // match [] { case []: ... } - if ((arraySize == 0) && optMatches(p)) { - return true; - } - return false; + return (arraySize == 0) && optMatches(p); case 1: // match arr { case [x]: x = arr ... } final String variable = parts.get(0); ScopeHandler.defineVariableInCurrentScope(variable, array); - if (optMatches(p)) { - return true; - } - // TODO remove is dangerous - ScopeHandler.removeVariable(variable); - return false; + return optMatches(p); default: { // match arr { case [...]: .. } if (partsSize == arraySize) { @@ -124,16 +113,7 @@ public final class MatchExpression extends InterruptableNode implements Expressi for (int i = 0; i < partsSize; i++) { ScopeHandler.defineVariableInCurrentScope(parts.get(i), array.get(i)); } - if (optMatches(p)) { - // Clean up will be provided after evaluate result - return true; - } - // Clean up variables if no match - for (String var : parts) { - // TODO removing without checking shadowing is dangerous - ScopeHandler.removeVariable(var); - } - return false; + return optMatches(p); } private boolean matchListPatternWithTail(ListPattern p, List parts, int partsSize, ArrayValue array, int arraySize) { @@ -148,16 +128,7 @@ public final class MatchExpression extends InterruptableNode implements Expressi tail.set(i - lastPart, array.get(i)); } ScopeHandler.defineVariableInCurrentScope(parts.get(lastPart), tail); - // Check optional condition - if (optMatches(p)) { - // Clean up will be provided after evaluate result - return true; - } - // Clean up variables - for (String var : parts) { - ScopeHandler.removeVariable(var); - } - return false; + return optMatches(p); } private boolean match(Value value, Value constant) { diff --git a/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java b/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java index 9a41306..44d4e50 100644 --- a/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java +++ b/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java @@ -57,6 +57,14 @@ public class ProgramsTest { () -> ((FunctionValue) args[0]).getValue().execute()); return NumberValue.ONE; }); + ScopeHandler.setFunction("fail", (args) -> { + if (args.length > 0) { + fail(args[0].asString()); + } else { + fail(); + } + return NumberValue.ONE; + }); } @ParameterizedTest diff --git a/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ast/VariableExpressionTest.java b/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ast/VariableExpressionTest.java index 87d9b00..beb4d5f 100644 --- a/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ast/VariableExpressionTest.java +++ b/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ast/VariableExpressionTest.java @@ -1,5 +1,7 @@ package com.annimon.ownlang.parser.ast; +import com.annimon.ownlang.lib.ScopeHandler; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.annimon.ownlang.exceptions.VariableDoesNotExistsException; @@ -11,6 +13,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; * @author aNNiMON */ public class VariableExpressionTest { + + @BeforeEach + void setUp() { + ScopeHandler.resetScope(); + } @Test public void testVariable() { diff --git a/ownlang-parser/src/test/resources/expressions/foreachKeyValue.own b/ownlang-parser/src/test/resources/expressions/foreachKeyValue.own index d0256c8..e45b19b 100644 --- a/ownlang-parser/src/test/resources/expressions/foreachKeyValue.own +++ b/ownlang-parser/src/test/resources/expressions/foreachKeyValue.own @@ -28,3 +28,16 @@ def testStringIterate() { assertEquals("ABCD", str) assertEquals(394/*97 + 98 + 99 + 100*/, sum) } + +def testScope() { + a = 100 + b = 200 + sum = 0 + for a, b : {14: 3} { + sum += a + sum += b + } + assertEquals(17, sum) + assertEquals(14, a) + assertEquals(3, b) +} \ No newline at end of file diff --git a/ownlang-parser/src/test/resources/expressions/foreachValue.own b/ownlang-parser/src/test/resources/expressions/foreachValue.own index 36942a0..6f1b8a4 100644 --- a/ownlang-parser/src/test/resources/expressions/foreachValue.own +++ b/ownlang-parser/src/test/resources/expressions/foreachValue.own @@ -36,6 +36,6 @@ def testScope() { sum += v } assertEquals(6, sum) - assertEquals(45, v) + assertEquals(3, v) } diff --git a/ownlang-parser/src/test/resources/expressions/matchExpression.own b/ownlang-parser/src/test/resources/expressions/matchExpression.own new file mode 100644 index 0000000..2e1bcb7 --- /dev/null +++ b/ownlang-parser/src/test/resources/expressions/matchExpression.own @@ -0,0 +1,179 @@ +use "types" + +def testMatchValue() { + value = 20 + result = match value { + case 10: "ten" + case 20: "twenty" + } + assertEquals("twenty", result) +} + +def testMatchValueAny() { + value = 20 + result = match value { + case 0: "zero" + case 1: "one" + case _: "other" + } + assertEquals("other", result) +} + +def testMatchAdditionalCheck() { + value = 20 + result = match value { + case 10: "ten" + case x if x < 10: "" + x + "<10" + case x if x > 10: "" + x + ">10" + } + assertEquals("20>10", result) +} + +def testMatchAdditionalCheckScope() { + x = 20 + result = match x { + case 10: "ten" + case x if x < 10: fail() + case y if y > 10: assertEquals(20, y) + } + assertEquals(20, x) + assertEquals(true, result) +} + +def printArrayRecursive(arr) = match arr { + case [head :: tail]: "[" + head + ", " + printArrayRecursive(tail) + "]" + case []: "[]" + case last: "[" + last + ", []]" +} + +def testMatchEmptyArray() { + result = printArrayRecursive([]) + assertEquals("[]", result) +} + +def testMatchOneElementArray() { + result = printArrayRecursive([1]) + assertEquals("[[1], []]", result) +} + +def testMatchTwoElementsArray() { + result = printArrayRecursive([1, 2]) + assertEquals("[1, [2, []]]", result) +} + +def testMatchArray() { + result = printArrayRecursive([1, 2, 3, 4]) + assertEquals("[1, [2, [3, [4, []]]]]", result) +} + +def testMatchArray2() { + def elementsCount(arr) = match arr { + case [a :: b :: c :: d :: e]: 5 + case [a :: b :: c :: d]: 4 + case [a :: b :: c]: 3 + case [a :: b]: 2 + case (7): -7 // special case 1 + case [a] if a == [8]: -8 // special case 2 + case []: 0 + case [a]: 1 + } + assertEquals(4, elementsCount([1, 2, 3, 4])) + assertEquals(3, elementsCount([1, 2, 3])) + assertEquals(2, elementsCount([1, 2])) + assertEquals(1, elementsCount([1])) + assertEquals(-7, elementsCount([7])) + assertEquals(-8, elementsCount([8])) + assertEquals(0, elementsCount([])) +} + +def testMatchOneElementArrayScope() { + head = 100 + tail = 200 + result = match [1] { + case [head :: tail]: fail("Multi-array") + case []: fail("Empty array") + case last: assertEquals(1, last[0]) + } + assertEquals(100, head) + assertEquals(200, tail) + assertEquals(true, result) +} + +def testMatchOneElementArrayDefinedVariableScope() { + head = 100 + tail = 200 + last = 300 + result = match [1] { + case [head :: tail]: fail("Multi-array") + case []: fail("Empty array") + case last: fail("Array should not be equal " + last) + case rest: assertEquals(1, rest[0]) + } + assertEquals(100, head) + assertEquals(200, tail) + assertEquals(300, last) + assertEquals(true, result) +} + +def testMatchArrayScope() { + head = 100 + tail = 200 + result = match [1, 2, 3] { + case [head :: tail]: assertEquals(1, head) + case []: fail("Empty array") + case last: fail("One element") + } + assertEquals(100, head) + assertEquals(200, tail) + assertEquals(true, result) +} + +def testMatchTuple() { + result = match [1, 2] { + case (0, 1): "(0, 1)" + case (1, 2): "(1, 2)" + case (2, 3): "(2, 3)" + } + assertEquals("(1, 2)", result) +} + +def testMatchTupleDifferentLength() { + result = match [1, 2] { + case (1): "(1)" + case (1, 2, 3, 4): "(1, 2, 3, 4)" + case _: "not matched" + } + assertEquals("not matched", result) +} + +def testMatchTupleAny1() { + result = match [1, 2] { + case (0, _): "(0, _)" + case (1, _): "(1, _)" + case (2, _): "(2, _)" + } + assertEquals("(1, _)", result) +} + +def testMatchTupleAny2() { + result = match [2, 3] { + case (0, _): "(0, _)" + case (1, _): "(1, _)" + case (_, _): "(_, _)" + } + assertEquals("(_, _)", result) +} + +def testMatchTupleAny3() { + result = match [2, 3] { + case (0, _): "(0, _)" + case (1, _): "(1, _)" + case _: "_" + } + assertEquals("_", result) +} + +def testScope() { + +} +