Skip to content

Commit

Permalink
Merge pull request #3 from anirudhgray/implement-namespace-and-imports
Browse files Browse the repository at this point in the history
Implement namespace and imports
  • Loading branch information
anirudhgray authored Oct 28, 2024
2 parents 2bc2886 + 09d50fc commit be2bfda
Show file tree
Hide file tree
Showing 25 changed files with 238 additions and 66 deletions.
78 changes: 47 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,38 @@
**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.

```
var check = 10;
fun percentage(part, total)
precondition: total > 0, part >= 0
postcondition: result >= 0, result <= 100
// import the kitchen file to get the bake function and ovenTemperature variable
scoop "./kitchen.tah";
fun totalIngredients(ingredientQuantities)
// contract
postcondition: total >= 0
{
var result = (part / total) * 100;
assertion: check == 10, "Unwanted side effects!";
return result;
var total = 0;
for (var i = 0; i < len(ingredientQuantities); i = i + 1) {
total = total + ingredientQuantities[i];
}
return total;
}
test "percentage test" {
assertion: percentage(20, 50) == 40;
assertion: percentage(10, 100) == 10;
fun prepareDish() {
return bake(100, ovenTemperature);
}
print percentage(20, 28);
test "totalIngredients test" {
// Test case: summing 3 ingredients
assertion: totalIngredients([1, 2, 3]) == 6, "Should be 6!";
// Test case: summing 0 ingredients
assertion: totalIngredients([]) == 0, "Should be 0!";
}
var flour = 2;
var sugar = 1;
var eggs = 3;
var ingredientsList = [flour, sugar, eggs];
print "Total ingredients needed: " + totalIngredients(ingredientsList);
print prepareDish();
```

## Table of Contents
Expand All @@ -45,8 +61,8 @@ print percentage(20, 28);
- [Maps](#maps)
- [Conditionals](#conditionals)
- [Loops](#loops)
- [Imports](#imports)
- [Built-in Functions](#built-in-functions)
- [Planned Features](#planned-features)
- [Stretch Goals](#stretch-goals)
- [The Theory Behind This Implementation of Tahini](#the-theory-behind-this-implementation-of-tahini)
- [What is a Tree-Walk Interpreter?](#what-is-a-tree-walk-interpreter)
Expand All @@ -65,8 +81,9 @@ Tahini currently implements:
- [ ] **Error Handling**: Support for user-defined exceptions and error handling (in progress).
- [x] **Stack Traces**: Detailed error messages with line numbers and function names.
- [x] **Unit Tests**: Write test blocks directly in the source file to validate code correctness.
- [ ] **Import System**: Import other Tahini files to reuse code and create modular applications (in progress).

Planned features include an import system, standard library, and cross-language support.
Planned features include a standard library and cross-language support.

## Getting Started

Expand Down Expand Up @@ -356,31 +373,30 @@ while (i < 5) {
}
```

### Built-in Functions
### Imports

Currently, Tahini does not support importing external modules or libraries. 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:
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.

- `input()` - Read a line of string input from the user.
- `clock()` - Get the current time in seconds since the Unix epoch.
- `len(arr)` - Get the length of an array.
```tahini
scoop "../kitchen.tah";
```

## Planned Features
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.

Tahini is under active development, and we plan to introduce several new features to enhance its functionality:
To avoid polluting the global environment, future versions of Tahini will support **namespaced imports**.

1. **Import System** (TODO)
- Add support for importing external modules and scripts, enabling code modularity and reuse.
Example (tentative):
```tahini
// future support
scoop "../kitchen.tah" into kitchen;
```

2. **Standard Library** (TODO)
- A set of basic utility functions for common operations such as user input, pattern matching, and file handling.

Example (tenative std lib features):
### Built-in Functions

3. **Auto Mocking Functions** (TODO)

Example (tentative):
Currently, Tahini does not support importing external modules or libraries. 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:

- `input()` - Read a line of string input from the user.
- `clock()` - Get the current time in seconds since the Unix epoch.
- `len(arr)` - Get the length of an array.

## Stretch Goals

Expand Down
4 changes: 3 additions & 1 deletion tahini/app/src/main/java/com/tahini/lang/CallFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ class CallFrame {

final TahiniCallable function;
final int returnToLine; // line number where the function was called
final String returnToFilename; // filename where the function was called

CallFrame(TahiniCallable function, int returnToLine) {
CallFrame(TahiniCallable function, int returnToLine, String returnToFilename) {
this.function = function;
this.returnToLine = returnToLine;
this.returnToFilename = returnToFilename;
}
}
55 changes: 54 additions & 1 deletion tahini/app/src/main/java/com/tahini/lang/Interpreter.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.tahini.lang;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;

class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {

Expand All @@ -13,6 +21,8 @@ class BreakException extends RuntimeException {

final boolean repl;

private final Set<Path> scoopedFiles = new HashSet<>();

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

Expand Down Expand Up @@ -199,6 +209,49 @@ public Void visitExpressionStmt(Stmt.Expression stmt) {
return null;
}

@Override
public Void visitImportStmt(Stmt.Import stmt) {
if (stmt.name != null) {
// environment.define(stmt.name.lexeme, new TahiniModule(stmt.path.lexeme));
return null;
} else {
List<Stmt> importedDeclarations;
try {
importedDeclarations = loadAndParseFile(stmt.path);
} catch (IOException e) {
throw new RuntimeError(stmt.path, "Error importing file " + stmt.path.lexeme + ".", new ArrayList<>());
}

// Register only declarations (variables, functions) in the module environment
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;
}
}

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<>());
}
scoopedFiles.add(filePath);
byte[] bytes = Files.readAllBytes(filePath);
String source = new String(bytes, Charset.defaultCharset());
Parser parser = new Parser(new Scanner(source, filePath.normalize().toString()).scanTokens(), false);

// Parse into declarations only
List<Stmt> allStatements = parser.parse();
return allStatements.stream()
.filter(stmt -> stmt instanceof Stmt.Function || stmt instanceof Stmt.Var || stmt instanceof Stmt.Import)
.collect(Collectors.toList());
}

@Override
public Void visitTestStmt(Stmt.Test stmt) {
try {
Expand Down Expand Up @@ -365,7 +418,7 @@ public Object visitCallExpr(Expr.Call expr) {
+ arguments.size() + ".", new ArrayList<>());
}

CallFrame frame = new CallFrame(function, expr.paren.line);
CallFrame frame = new CallFrame(function, expr.paren.line, expr.paren.filename);

callStack.push(frame);

Expand Down
13 changes: 13 additions & 0 deletions tahini/app/src/main/java/com/tahini/lang/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ private Stmt declaration() {
return null;
}
}
if (match(TokenType.SCOOP)) {
return scoop();
}
return statement();
} catch (ParseError error) {
synchronize();
Expand Down Expand Up @@ -220,6 +223,16 @@ private Stmt printStatement() {
return new Stmt.Print(value);
}

private Stmt scoop() {
Token path = consume(TokenType.STRING, "Expect string path to scoop from.");
Token name = null;
if (match(TokenType.INTO)) {
name = consume(TokenType.IDENTIFIER, "Expect variable name to scoop into.");
}
consume(TokenType.SEMICOLON, "Expect ';' after scoop statement.");
return new Stmt.Import(path, name);
}

private Stmt varDeclaration() {
Token name = consume(TokenType.IDENTIFIER, "Expect variable name.");

Expand Down
14 changes: 9 additions & 5 deletions tahini/app/src/main/java/com/tahini/lang/Scanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
class Scanner {

private final String source;
private final String filename;
private final List<Token> tokens = new ArrayList<>();
private int start = 0;
private int current = 0;
Expand Down Expand Up @@ -41,10 +42,13 @@ class Scanner {
keywords.put("postcondition", TokenType.POSTCONDITION);
keywords.put("assertion", TokenType.ASSERTION);
keywords.put("test", TokenType.TEST);
keywords.put("scoop", TokenType.SCOOP);
keywords.put("into", TokenType.INTO);
}

Scanner(String source) {
Scanner(String source, String filename) {
this.source = source;
this.filename = filename;
}

List<Token> scanTokens() {
Expand All @@ -54,7 +58,7 @@ List<Token> scanTokens() {
scanToken();
}

tokens.add(new Token(TokenType.EOF, "", null, line));
tokens.add(new Token(TokenType.EOF, "", null, line, filename));
return tokens;
}

Expand Down Expand Up @@ -124,7 +128,7 @@ private void scanToken() {
} else if (isAlpha(c)) {
identifier();
} else {
Tahini.error(line, "Unexpected character.");
Tahini.error(filename, line, "Unexpected character.");
}
}
}
Expand Down Expand Up @@ -175,7 +179,7 @@ private void string() {
}

if (isAtEnd()) {
Tahini.error(line, "Unterminated string.");
Tahini.error(filename, line, "Unterminated string.");
return;
}

Expand Down Expand Up @@ -237,6 +241,6 @@ private void addToken(TokenType type) {

private void addToken(TokenType type, Object literal) {
String text = source.substring(start, current);
tokens.add(new Token(type, text, literal, line));
tokens.add(new Token(type, text, literal, line, filename));
}
}
15 changes: 15 additions & 0 deletions tahini/app/src/main/java/com/tahini/lang/Stmt.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface Visitor<R> {
R visitBreakStmt(Break stmt);
R visitReturnStmt(Return stmt);
R visitContractStmt(Contract stmt);
R visitImportStmt(Import stmt);
}
static class Expression extends Stmt {
Expression(Expr expression) {
Expand Down Expand Up @@ -170,6 +171,20 @@ <R> R accept(Visitor<R> visitor) {
final List<Expr> conditions;
final Object msg;
}
static class Import extends Stmt {
Import(Token path, Token name) {
this.path = path;
this.name = name;
}

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

final Token path;
final Token name;
}

abstract <R> R accept(Visitor<R> visitor);
}
Loading

0 comments on commit be2bfda

Please sign in to comment.