diff --git a/README.md b/README.md index a5c1641..3a943d6 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,10 @@ > Tahini is a paste made from sesame seeds that can be used as a dip, spread, or dressing. It's versatile, flavorful, and adds a unique touch to many dishes, while being a healthy choice. Inspired by the simplicity, flexibility, and richness of tahini, we present Tahini, a programming language that aims to be a joy to use, with a focus on simplicity, expressiveness, and extensive testing support. -**Tahini** is a lightweight, tree-based interpreted programming language that is written using Java, and which runs on the JVM (Java Virtual Machine), inspired by Lox and Python. It aims to provide simplicity and expressiveness alongside extensive testing and contract support, making it a joy for developers to use. Currently, Tahini supports a number of core language and testing features, with an exciting roadmap of future capabilities, including an import system, auto function mocking, and cross-language support. +**Tahini** is a lightweight, tree-based interpreted programming language that is written using Java, and which runs on the JVM (Java Virtual Machine), inspired by Lox and Python. It aims to provide simplicity and expressiveness alongside extensive testing and contract support, making it a joy for developers to use. Currently, Tahini supports a number of core language and testing features, with an exciting roadmap of future capabilities. ``` -// import the kitchen file to get the bake function and ovenTemperature variable -scoop "./kitchen.tah"; +scoop "./kitchen.tah" into kitchen; fun totalIngredients(ingredientQuantities) // contract @@ -24,7 +23,7 @@ fun totalIngredients(ingredientQuantities) } fun prepareDish() { - return bake(100, ovenTemperature); + return kitchen::bake(100, kitchen::ovenTemperature); } test "totalIngredients test" { @@ -62,6 +61,8 @@ print prepareDish(); - [Conditionals](#conditionals) - [Loops](#loops) - [Imports](#imports) + - [Flat Imports](#flat-imports) + - [Namespaced Imports](#namespaced-imports) - [Built-in Functions](#built-in-functions) - [Stretch Goals](#stretch-goals) - [The Theory Behind This Implementation of Tahini](#the-theory-behind-this-implementation-of-tahini) @@ -377,19 +378,51 @@ while (i < 5) { Tahini supports importing other Tahini files to reuse code and create modular applications. You can import a file using the `scoop` keyword, followed by the path to the file. The imported file will be executed in the current scope, allowing you to access its variables and functions. +#### Flat Imports + ```tahini scoop "../kitchen.tah"; + +function_from_kitchen(); ``` -The above would do a **flat import** of the `kitchen.tah` file, executing it in the current scope, and making every variable and function in `kitchen.tah` available in the current file's global scope. This should be used with caution, as it can lead to naming conflicts, pollution and unintended side effects. +The above would do a **flat import** of the `kitchen.tah` file, executing it in the current scope, and making every variable and function in `kitchen.tah` available in the current file's global scope without any prefix. This should be used with caution, as it can lead to naming conflicts, pollution and unintended side effects. -To avoid polluting the global environment, future versions of Tahini will support **namespaced imports**. +#### Namespaced Imports + +To avoid polluting the global environment, it is recommended to use **namespaced imports**. ```tahini -// future support scoop "../kitchen.tah" into kitchen; + +kitchen::function_from_kitchen(); ``` +With this, all functions and variables from kitchen.tah are accessible only through the kitchen namespace. If `kitchen.tah` defines a function `prepare()`, you would now call it as `kitchen::prepare()` in your current file. + +Tahini also allows **nested imports**, so if a file you import also imports other files, they will follow the same flat or namespaced rules. For example: + +```tahini +// C.tah +fun function_from_c() {...} + +// D.tah +fun function_from_d() {...} + +// B.tah +scoop "C.tah"; +scoop "D.tah" into D; +fun function_from_b() {...} + +// A.tah +scoop "B.tah" into B; +B::function_from_b(); +B::function_from_c(); +B::D::function_from_d(); +``` + +See [tests/namescoop](./tahini/tests/namescoop1.tah) for an example of how imports work. + ### Built-in Functions Currently, Tahini does not have a standard library. However, it does provide a set of built-in functions (filling some of the core gaps which an imported standard library would have provided) for common operations: diff --git a/tahini/app/src/main/java/com/tahini/lang/Environment.java b/tahini/app/src/main/java/com/tahini/lang/Environment.java index 64c4588..09ec316 100644 --- a/tahini/app/src/main/java/com/tahini/lang/Environment.java +++ b/tahini/app/src/main/java/com/tahini/lang/Environment.java @@ -18,10 +18,16 @@ public Environment(Environment enclosing) { private final Map values = new HashMap<>(); + final Map namespaces = new HashMap<>(); + public void define(String name, Object value) { values.put(name, value); } + public void defineNamespace(String name, Environment namespace) { + namespaces.put(name, namespace); + } + public Object getValue(Token name) { if (values.containsKey(name.lexeme)) { return values.get(name.lexeme); @@ -34,6 +40,18 @@ public Object getValue(Token name) { throw new RuntimeError(name, "Undefined variable '" + name.lexeme + "'.", new ArrayList<>()); } + public Environment getNamespace(Token name) { + if (namespaces.containsKey(name.lexeme)) { + return namespaces.get(name.lexeme); + } + + if (enclosing != null) { + return enclosing.getNamespace(name); + } + + throw new RuntimeError(name, "Undefined namespace '" + name.lexeme + "'.", new ArrayList<>()); + } + void assign(Token name, Object value) { if (values.containsKey(name.lexeme)) { values.put(name.lexeme, value); diff --git a/tahini/app/src/main/java/com/tahini/lang/Expr.java b/tahini/app/src/main/java/com/tahini/lang/Expr.java index 6d02519..8b962d9 100644 --- a/tahini/app/src/main/java/com/tahini/lang/Expr.java +++ b/tahini/app/src/main/java/com/tahini/lang/Expr.java @@ -14,6 +14,7 @@ interface Visitor { R visitUnaryExpr(Unary expr); R visitTernaryExpr(Ternary expr); R visitVariableExpr(Variable expr); + R visitNamespacedVariableExpr(NamespacedVariable expr); R visitLogicalExpr(Logical expr); R visitTahiniListExpr(TahiniList expr); R visitTahiniMapExpr(TahiniMap expr); @@ -164,6 +165,18 @@ R accept(Visitor visitor) { final Token name; } + static class NamespacedVariable extends Expr { + NamespacedVariable(List nameParts) { + this.nameParts = nameParts; + } + + @Override + R accept(Visitor visitor) { + return visitor.visitNamespacedVariableExpr(this); + } + + final List nameParts; + } static class Logical extends Expr { Logical(Expr left, Token operator, Expr right) { this.left = left; diff --git a/tahini/app/src/main/java/com/tahini/lang/Interpreter.java b/tahini/app/src/main/java/com/tahini/lang/Interpreter.java index fbe90c8..df2ee61 100644 --- a/tahini/app/src/main/java/com/tahini/lang/Interpreter.java +++ b/tahini/app/src/main/java/com/tahini/lang/Interpreter.java @@ -24,7 +24,7 @@ class BreakException extends RuntimeException { private final Set scoopedFiles = new HashSet<>(); final Environment globals = new Environment(); - private Environment environment = globals; + public Environment environment = globals; public Interpreter(boolean repl) { this.repl = repl; @@ -211,31 +211,43 @@ public Void visitExpressionStmt(Stmt.Expression stmt) { @Override public Void visitImportStmt(Stmt.Import stmt) { + List importedDeclarations; + try { + importedDeclarations = loadAndParseFile(stmt.path); + } catch (IOException e) { + throw new RuntimeError(stmt.path, "Error importing file " + stmt.path.lexeme + ".", new ArrayList<>()); + } + if (stmt.name != null) { - // environment.define(stmt.name.lexeme, new TahiniModule(stmt.path.lexeme)); - return null; - } else { - List importedDeclarations; + Environment previous = this.environment; + Environment importedEnv = new Environment(); + try { - importedDeclarations = loadAndParseFile(stmt.path); - } catch (IOException e) { - throw new RuntimeError(stmt.path, "Error importing file " + stmt.path.lexeme + ".", new ArrayList<>()); + this.environment = importedEnv; + + for (Stmt statement : importedDeclarations) { + if (statement instanceof Stmt.Function || statement instanceof Stmt.Var || statement instanceof Stmt.Import) { + execute(statement); + } + } + } finally { + this.environment = previous; } - // Register only declarations (variables, functions) in the module environment + environment.defineNamespace(stmt.name.lexeme, importedEnv); + } else { for (Stmt statement : importedDeclarations) { if (statement instanceof Stmt.Function || statement instanceof Stmt.Var || statement instanceof Stmt.Import) { execute(statement); } } - scoopedFiles.remove(Paths.get((String) stmt.path.literal).toAbsolutePath()); - return null; } + scoopedFiles.remove(Paths.get((String) stmt.path.literal).toAbsolutePath()); + + return null; } private List loadAndParseFile(Token path) throws IOException { - // use some file getting system which doesnt depend on absolute path - // of where you are calling the interpreter from Path filePath = Paths.get((String) path.literal).toAbsolutePath(); if (scoopedFiles.contains(filePath)) { throw new RuntimeError(path, "Circular import detected.", new ArrayList<>()); @@ -318,6 +330,16 @@ public Object visitVariableExpr(Expr.Variable expr) { return environment.getValue(expr.name); } + @Override + public Object visitNamespacedVariableExpr(Expr.NamespacedVariable expr) { + List nameParts = expr.nameParts; + Environment env = environment; + for (int i = 0; i < nameParts.size() - 1; i++) { + env = env.getNamespace(nameParts.get(i)); + } + return env.getValue(nameParts.get(nameParts.size() - 1)); + } + private void checkNumberOperand(Token operator, Object operand) { if (operand instanceof Double) { return; @@ -424,7 +446,19 @@ public Object visitCallExpr(Expr.Call expr) { Object result; try { - result = function.call(this, arguments); + if (expr.callee instanceof Expr.NamespacedVariable namespacedVariable) { + List nameParts = namespacedVariable.nameParts; + Environment env = environment; + Environment previousEnv = environment; + for (int i = 0; i < nameParts.size() - 1; i++) { + env = env.getNamespace(nameParts.get(i)); + } + this.environment = env; + result = function.call(this, arguments); + this.environment = previousEnv; + } else { + result = function.call(this, arguments); + } } catch (RuntimeError error) { if (error.token == null) { throw new RuntimeError(expr.paren, error.getMessage(), new ArrayList<>(callStack)); diff --git a/tahini/app/src/main/java/com/tahini/lang/Parser.java b/tahini/app/src/main/java/com/tahini/lang/Parser.java index 44a1a45..2fb079a 100644 --- a/tahini/app/src/main/java/com/tahini/lang/Parser.java +++ b/tahini/app/src/main/java/com/tahini/lang/Parser.java @@ -559,7 +559,17 @@ private Expr primary() { return new Expr.Literal(previous().literal); } if (match(TokenType.IDENTIFIER)) { - return new Expr.Variable(previous()); + Token name = previous(); + if (peek().type != TokenType.NAMESPACE_SEPARATOR) { + return new Expr.Variable(name); + } + List nameParts = new ArrayList<>(); + nameParts.add(name); + while (match(TokenType.NAMESPACE_SEPARATOR)) { + Token member = consume(TokenType.IDENTIFIER, "Expect member name after '::'."); + nameParts.add(member); + } + return new Expr.NamespacedVariable(nameParts); } if (match(TokenType.LEFT_PAREN)) { Expr expr = expression(); diff --git a/tahini/app/src/main/java/com/tahini/lang/Scanner.java b/tahini/app/src/main/java/com/tahini/lang/Scanner.java index b383461..35bd55b 100644 --- a/tahini/app/src/main/java/com/tahini/lang/Scanner.java +++ b/tahini/app/src/main/java/com/tahini/lang/Scanner.java @@ -120,8 +120,13 @@ private void scanToken() { string(); case '?' -> addToken(TokenType.QUESTION_MARK); - case ':' -> - addToken(TokenType.COLON); + case ':' -> { + if (match(':')) { + addToken(TokenType.NAMESPACE_SEPARATOR); + } else { + addToken(TokenType.COLON); + } + } default -> { if (isDigit(c)) { number(); diff --git a/tahini/app/src/main/java/com/tahini/lang/TahiniFunction.java b/tahini/app/src/main/java/com/tahini/lang/TahiniFunction.java index ee2b698..a4c1d3a 100644 --- a/tahini/app/src/main/java/com/tahini/lang/TahiniFunction.java +++ b/tahini/app/src/main/java/com/tahini/lang/TahiniFunction.java @@ -19,7 +19,7 @@ public int arity() { @Override public Object call(Interpreter interpreter, List arguments) { - Environment environment = new Environment(interpreter.globals); + Environment environment = new Environment(interpreter.environment); for (int i = 0; i < declaration.params.size(); i++) { environment.define(declaration.params.get(i).lexeme, arguments.get(i)); diff --git a/tahini/app/src/main/java/com/tahini/lang/TokenType.java b/tahini/app/src/main/java/com/tahini/lang/TokenType.java index a9a77d3..bb5e84f 100644 --- a/tahini/app/src/main/java/com/tahini/lang/TokenType.java +++ b/tahini/app/src/main/java/com/tahini/lang/TokenType.java @@ -18,5 +18,5 @@ enum TokenType { // Annotations. PRECONDITION, POSTCONDITION, ASSERTION, TEST, // Import - SCOOP, INTO + SCOOP, INTO, NAMESPACE_SEPARATOR } diff --git a/tahini/app/src/main/java/com/tahini/tool/GenerateAst.java b/tahini/app/src/main/java/com/tahini/tool/GenerateAst.java index 9622506..d9c897a 100644 --- a/tahini/app/src/main/java/com/tahini/tool/GenerateAst.java +++ b/tahini/app/src/main/java/com/tahini/tool/GenerateAst.java @@ -15,19 +15,20 @@ public static void main(String[] args) throws IOException { String outputDir = args[0]; defineAst(outputDir, "Expr", Arrays.asList( - "Assign : Token name, Expr value", - "Binary : Expr left, Token operator, Expr right", - "Call : Expr callee, Token paren, List arguments", - "ListAccess : Expr list, Token paren, Expr index", - "ListSlice : Expr list, Token paren, Expr start, Expr end", - "Grouping : Expr expression", - "Literal : Object value", - "Unary : Token operator, Expr right", - "Ternary : Expr condition, Expr left, Expr right", - "Variable : Token name", - "Logical : Expr left, Token operator, Expr right", - "TahiniList : List elements", - "TahiniMap : List keys, List values" + "Assign : Token name, Expr value", + "Binary : Expr left, Token operator, Expr right", + "Call : Expr callee, Token paren, List arguments", + "ListAccess : Expr list, Token paren, Expr index", + "ListSlice : Expr list, Token paren, Expr start, Expr end", + "Grouping : Expr expression", + "Literal : Object value", + "Unary : Token operator, Expr right", + "Ternary : Expr condition, Expr left, Expr right", + "Variable : Token name", + "NamespacedVariable : List nameParts", + "Logical : Expr left, Token operator, Expr right", + "TahiniList : List elements", + "TahiniMap : List keys, List values" )); defineAst(outputDir, "Stmt", Arrays.asList( @@ -42,7 +43,7 @@ public static void main(String[] args) throws IOException { "Break", "Return : Token keyword, Expr value", "Contract : Token type, List conditions, Object msg", - "Import : Token path, Token name" + "Import : Token path, Token name" )); } diff --git a/tahini/tests/namescoop.tah b/tahini/tests/namescoop.tah new file mode 100644 index 0000000..72b5b07 --- /dev/null +++ b/tahini/tests/namescoop.tah @@ -0,0 +1,18 @@ +scoop "./tests/namescoop2.tah" into test2; + +fun hello() { + return "Hello, World!"; +} + +print test2::hello2(); +print test2::hello3(); +print test2::test4::from4; +print test2::test4::test5::from5; + +// Hello, World! From 3. +// test4.tah +// test5.tah +// Hello, World! From 2. +// Hello, World! From 3. +// test4.tah +// test5.tah diff --git a/tahini/tests/namescoop2.tah b/tahini/tests/namescoop2.tah new file mode 100644 index 0000000..d62154e --- /dev/null +++ b/tahini/tests/namescoop2.tah @@ -0,0 +1,16 @@ +scoop "./tests/namescoop3.tah"; + +var checkfromlol = 15; + +fun hello2() { + print hello3(); + print test4::from4; + print test4::test5::from5; + return "Hello, World! From 2."; +} + +hello2(); + +// Hello, World! From 3. +// test4.tah +// test5.tah diff --git a/tahini/tests/namescoop3.tah b/tahini/tests/namescoop3.tah new file mode 100644 index 0000000..81c6790 --- /dev/null +++ b/tahini/tests/namescoop3.tah @@ -0,0 +1,7 @@ +scoop "./tests/namescoop4.tah" into test4; + +fun hello3() { + return "Hello, World! From 3."; +} + +var test3var = "test3var"; diff --git a/tahini/tests/namescoop4.tah b/tahini/tests/namescoop4.tah new file mode 100644 index 0000000..86feaff --- /dev/null +++ b/tahini/tests/namescoop4.tah @@ -0,0 +1,3 @@ +scoop "./tests/namescoop5.tah" into test5; + +var from4 = "test4.tah"; diff --git a/tahini/tests/namescoop5.tah b/tahini/tests/namescoop5.tah new file mode 100644 index 0000000..0fc1625 --- /dev/null +++ b/tahini/tests/namescoop5.tah @@ -0,0 +1 @@ +var from5 = "test5.tah"; diff --git a/tahini/tests/namescooperror.tah b/tahini/tests/namescooperror.tah new file mode 100644 index 0000000..fe04bad --- /dev/null +++ b/tahini/tests/namescooperror.tah @@ -0,0 +1,17 @@ +scoop "./tests/namescoop2.tah" into test2; + +fun hello() { + return "Hello, World!"; +} + +print test2::hello2(); +print hello3(); +print test2::test4::from4; +print test2::test4::test5::from5; + +// Hello, World! From 3. +// test4.tah +// test5.tah +// Hello, World! From 2. +// RuntimeError: Undefined variable 'hello3'. +// [at line 8 in namescooperror.tah] diff --git a/test.tah b/test.tah deleted file mode 100644 index 35d1204..0000000 --- a/test.tah +++ /dev/null @@ -1,91 +0,0 @@ -scoop "../test2.tah"; - -fun isEven(n) { - var check =4; - while (n >= 2) { - n = n - 2; - } - return n == 0; -} - -print checkfromlol; - -print testList(); - -var dictwithfunc = { "key1": 1, "key2": 2, "key3": 3, true: false, nil:5, "fib": isEven, isEven: isEven }; -print dictwithfunc["key1"]; -print dictwithfunc[true]; -print dictwithfunc[nil]; -print dictwithfunc["fib"](4); -print dictwithfunc["fib"]; -print dictwithfunc[isEven]; -print dictwithfunc[isEven](5); -print dictwithfunc["isEven"](4); - -print len("Hello"); -var str = "hello"; -print len(str); -print str[1]; - -fun filterEven(arr) { - var oddArr = []; - for (var i = 0; i < len(arr); i = i + 1) { - if (isEven(arr[i])) { - oddArr = oddArr + [arr[i]]; - } - } - return oddArr; -} - -print filterEven([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - - -var ddd = 4.4; -print ddd; - -print 4.3+5.7; - -// Fibonacci function -fun fib(n) -precondition: n >= 0 -{ - if (n <= 1) return n; - return fib(n - 2) + fib(n - 1); -} - -// list of functions -var list = [fib]; - -// call the function in the list -print "T"; -print list[0](7); -print list[0]; - -// are funcitons first class citizens? -// let's find out by passing a function as an argument -fun testFunction(func) { - return func(6); -} - -print testFunction(fib); - -// assign a function to a variable -var fibVar = fib; -print fibVar(6); -print fib(testFunction(fib)); - -print "h"+"d"; - -// Regular code block -var x = 10; -print "Fib(x): " + fib(x); - -// Test block to check Fibonacci function -test "checking this out" { - assertion: fib(0) == 0; - assertion: fib(1) == 1; - assertion: fib(2) == 1; - assertion: fib(3) == 2; - assertion: fib(4) == 3; - assertion: fib(5) == 5; -}