Skip to content

Commit

Permalink
Merge pull request #4 from anirudhgray/implement-namespaced-imports
Browse files Browse the repository at this point in the history
Implement namespaced imports
  • Loading branch information
anirudhgray authored Oct 29, 2024
2 parents 0a533b4 + ca01526 commit 0efd3ef
Show file tree
Hide file tree
Showing 16 changed files with 216 additions and 131 deletions.
47 changes: 40 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,7 +23,7 @@ fun totalIngredients(ingredientQuantities)
}
fun prepareDish() {
return bake(100, ovenTemperature);
return kitchen::bake(100, kitchen::ovenTemperature);
}
test "totalIngredients test" {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions tahini/app/src/main/java/com/tahini/lang/Environment.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ public Environment(Environment enclosing) {

private final Map<String, Object> values = new HashMap<>();

final Map<String, Environment> 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);
Expand All @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions tahini/app/src/main/java/com/tahini/lang/Expr.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Visitor<R> {
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);
Expand Down Expand Up @@ -164,6 +165,18 @@ <R> R accept(Visitor<R> visitor) {

final Token name;
}
static class NamespacedVariable extends Expr {
NamespacedVariable(List<Token> nameParts) {
this.nameParts = nameParts;
}

@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitNamespacedVariableExpr(this);
}

final List<Token> nameParts;
}
static class Logical extends Expr {
Logical(Expr left, Token operator, Expr right) {
this.left = left;
Expand Down
62 changes: 48 additions & 14 deletions tahini/app/src/main/java/com/tahini/lang/Interpreter.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class BreakException extends RuntimeException {
private final Set<Path> scoopedFiles = new HashSet<>();

final Environment globals = new Environment();
private Environment environment = globals;
public Environment environment = globals;

public Interpreter(boolean repl) {
this.repl = repl;
Expand Down Expand Up @@ -211,31 +211,43 @@ public Void visitExpressionStmt(Stmt.Expression stmt) {

@Override
public Void visitImportStmt(Stmt.Import stmt) {
List<Stmt> 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<Stmt> 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<Stmt> 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<>());
Expand Down Expand Up @@ -318,6 +330,16 @@ public Object visitVariableExpr(Expr.Variable expr) {
return environment.getValue(expr.name);
}

@Override
public Object visitNamespacedVariableExpr(Expr.NamespacedVariable expr) {
List<Token> 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;
Expand Down Expand Up @@ -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<Token> 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));
Expand Down
12 changes: 11 additions & 1 deletion tahini/app/src/main/java/com/tahini/lang/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Token> 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();
Expand Down
9 changes: 7 additions & 2 deletions tahini/app/src/main/java/com/tahini/lang/Scanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public int arity() {
@Override
public Object call(Interpreter interpreter,
List<Object> 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));
Expand Down
2 changes: 1 addition & 1 deletion tahini/app/src/main/java/com/tahini/lang/TokenType.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ enum TokenType {
// Annotations.
PRECONDITION, POSTCONDITION, ASSERTION, TEST,
// Import
SCOOP, INTO
SCOOP, INTO, NAMESPACE_SEPARATOR
}
29 changes: 15 additions & 14 deletions tahini/app/src/main/java/com/tahini/tool/GenerateAst.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Expr> 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<Expr> elements",
"TahiniMap : List<Expr> keys, List<Expr> values"
"Assign : Token name, Expr value",
"Binary : Expr left, Token operator, Expr right",
"Call : Expr callee, Token paren, List<Expr> 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<Token> nameParts",
"Logical : Expr left, Token operator, Expr right",
"TahiniList : List<Expr> elements",
"TahiniMap : List<Expr> keys, List<Expr> values"
));

defineAst(outputDir, "Stmt", Arrays.asList(
Expand All @@ -42,7 +43,7 @@ public static void main(String[] args) throws IOException {
"Break",
"Return : Token keyword, Expr value",
"Contract : Token type, List<Expr> conditions, Object msg",
"Import : Token path, Token name"
"Import : Token path, Token name"
));
}

Expand Down
18 changes: 18 additions & 0 deletions tahini/tests/namescoop.tah
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions tahini/tests/namescoop2.tah
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions tahini/tests/namescoop3.tah
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
scoop "./tests/namescoop4.tah" into test4;

fun hello3() {
return "Hello, World! From 3.";
}

var test3var = "test3var";
3 changes: 3 additions & 0 deletions tahini/tests/namescoop4.tah
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
scoop "./tests/namescoop5.tah" into test5;

var from4 = "test4.tah";
1 change: 1 addition & 0 deletions tahini/tests/namescoop5.tah
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
var from5 = "test5.tah";
Loading

0 comments on commit 0efd3ef

Please sign in to comment.