Skip to content

Commit

Permalink
Merge pull request #5 from anirudhgray/add-stdlib
Browse files Browse the repository at this point in the history
Add stdlib
  • Loading branch information
anirudhgray authored Oct 30, 2024
2 parents 31013cd + 866f102 commit d33a02e
Show file tree
Hide file tree
Showing 15 changed files with 478 additions and 26 deletions.
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ print prepareDish();
- [Flat Imports](#flat-imports)
- [Namespaced Imports](#namespaced-imports)
- [Built-in Functions](#built-in-functions)
- [Standard Library](#standard-library)
- [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 @@ -82,9 +83,10 @@ 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).
- [x] **Import System**: Import other Tahini files to reuse code and create modular applications.
- [ ] **Standard Library**: A growing set of built-in functions and utilities, called the `larder`.

Planned features include a standard library and cross-language support.
Planned features include cross-language support.

## Getting Started

Expand Down Expand Up @@ -427,12 +429,34 @@ See [tests/namescoop](./tahini/tests/namescoop1.tah) for an example of how impor

### 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:
Apart from its standard library (`larder`), Tahini provides a set of built-in functions in the default namespace 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.

## Standard Library

Tahini comes with a growing standard library, called [the `larder`](./tahini/app/src/main/resources/stdlib/), which provides a set of built-in functions and utilities to simplify common tasks. The `larder` includes functions for string manipulation, file I/O, math operations, and more.

Here are some of the modules and functions available in the `larder`:

- `larder/math` - Mathematical functions like `sqrt`, `pow`, `sin` etc.
- `larder/string` - String manipulation functions like `split`, `join` etc.
- `larder/io` - File I/O functions like `readFile`, `writeFile` etc.
- `larder/collections` - Collection functions like `values`, `keys`, `append`, `remove` etc.

You can import the `larder` modules in your Tahini code using the `scoop` keyword, similar to importing other Tahini files.

```tahini
scoop "larder/math" into math;
scoop "larder/io";
var x = math::sqrt(25);
print x;
writeFile("output.txt", "Hello, Tahini!");
```

## Stretch Goals

**Note:** we will do in-depth feasability analysis on these goals, and decide on attempting to accomplish them based on our provided timeline.
Expand Down
59 changes: 51 additions & 8 deletions tahini/app/src/main/java/com/tahini/lang/Interpreter.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.tahini.lang;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand Down Expand Up @@ -43,8 +47,14 @@ public Object call(Interpreter interpreter, List<Object> arguments) {
public String toString() {
return "<native fn>";
}

@Override
public boolean isInternal() {
return false;
}
});
StandardLibrary.addStandardFunctions(environment);
StandardLibrary.addInternalFunctions(environment);
}

private final Stack<CallFrame> callStack = new Stack<>();
Expand Down Expand Up @@ -224,6 +234,8 @@ public Void visitImportStmt(Stmt.Import stmt) {

try {
this.environment = importedEnv;
StandardLibrary.addStandardFunctions(environment);
StandardLibrary.addInternalFunctions(environment);

for (Stmt statement : importedDeclarations) {
if (statement instanceof Stmt.Function || statement instanceof Stmt.Var || statement instanceof Stmt.Import) {
Expand All @@ -248,17 +260,43 @@ public Void visitImportStmt(Stmt.Import stmt) {
}

private List<Stmt> loadAndParseFile(Token path) throws IOException {
Path filePath = Paths.get((String) path.literal).toAbsolutePath();
if (scoopedFiles.contains(filePath)) {
throw new RuntimeError(path, "Circular import detected.", new ArrayList<>());
String importPath = (String) path.literal;
List<Stmt> parsedStatements = new ArrayList<>();

if (importPath.startsWith("larder/")) {
String stdlibFilePath = "/stdlib" + importPath.substring("larder".length()) + ".tah";
parsedStatements.addAll(loadSingleStdlibModule(stdlibFilePath, path));
} else {
Path filePath = Paths.get(importPath).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());
parsedStatements.addAll(parseSource(source, importPath));
}

return parsedStatements;
}

private List<Stmt> loadSingleStdlibModule(String stdlibFilePath, Token path) throws IOException {
InputStream stdlibStream = getClass().getResourceAsStream(stdlibFilePath);
if (stdlibStream == null) {
throw new RuntimeError(path, "File " + stdlibFilePath + " not found in the larder.", 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
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stdlibStream, StandardCharsets.UTF_8))) {
String source = reader.lines().collect(Collectors.joining("\n"));
return parseSource(source, stdlibFilePath);
}
}

private List<Stmt> parseSource(String source, String sourcePath) {
Parser parser = new Parser(new Scanner(source, sourcePath).scanTokens(), false);
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());
Expand Down Expand Up @@ -379,6 +417,11 @@ public Object visitBinaryExpr(Expr.Binary expr) {
checkNumberOperands(expr.operator, left, right);
yield (double) left * (double) right;
}
case MODULO -> {
checkNumberOperands(expr.operator, left, right);
checkZDE(expr.operator, right);
yield (double) left % (double) right;
}
case PLUS -> {
if (left instanceof Double && right instanceof Double) {
yield (double) left + (double) right;
Expand Down
4 changes: 2 additions & 2 deletions tahini/app/src/main/java/com/tahini/lang/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ private Expr term() {
private Expr factor() {
Expr expr = unary();

while (match(TokenType.SLASH, TokenType.STAR)) {
while (match(TokenType.SLASH, TokenType.STAR, TokenType.MODULO)) {
Token operator = previous();
Expr right = unary();
expr = new Expr.Binary(expr, operator, right);
Expand Down Expand Up @@ -604,7 +604,7 @@ private Expr primary() {
}

// Error production for binary operator without left-hand operand
if (match(TokenType.PLUS, TokenType.MINUS, TokenType.STAR, TokenType.SLASH, TokenType.EQUAL_EQUAL, TokenType.BANG_EQUAL, TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {
if (match(TokenType.PLUS, TokenType.MINUS, TokenType.STAR, TokenType.SLASH, TokenType.EQUAL_EQUAL, TokenType.BANG_EQUAL, TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL, TokenType.MODULO)) {
Token operator = previous();
throw error(operator, "Missing left hand operand.");
}
Expand Down
2 changes: 2 additions & 0 deletions tahini/app/src/main/java/com/tahini/lang/Scanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ private void scanToken() {
addToken(TokenType.SEMICOLON);
case '*' ->
addToken(TokenType.STAR);
case '%' ->
addToken(TokenType.MODULO);
case '!' ->
addToken(match('=') ? TokenType.BANG_EQUAL : TokenType.BANG);
case '=' ->
Expand Down
161 changes: 161 additions & 0 deletions tahini/app/src/main/java/com/tahini/lang/StandardLibrary.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.tahini.lang;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

class StandardLibrary {
Expand All @@ -9,6 +15,151 @@ public static void addStandardFunctions(Environment globalEnv) {
globalEnv.define("input", new InputFunction());
globalEnv.define("len", new ArrayLengthFunction());
}

public static void addInternalFunctions(Environment globalEnv) {
globalEnv.define("_keys", new HashmapKeysFunction());
globalEnv.define("_values", new HashmapValuesFunction());
globalEnv.define("_read", new FileReadFunction());
globalEnv.define("_write", new FileWriteFunction());
}
}

class FileWriteFunction implements TahiniCallable {

@Override
public Object call(Interpreter interpreter, List<Object> args) {
if (args.size() != 2) {
throw new RuntimeError(null, "Expected 2 arguments (file path and content) but got " + args.size() + ".", null);
}
Object pathArg = args.get(0);
Object contentArg = args.get(1);

if (!(pathArg instanceof String path)) {
throw new RuntimeError(null, "Expected a string (file path) but got " + pathArg + ".", null);
}
if (!(contentArg instanceof String content)) {
throw new RuntimeError(null, "Expected a string (content) but got " + contentArg + ".", null);
}

try {
Files.write(Path.of(path), content.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new RuntimeError(null, "Error writing file: " + e.getMessage(), null);
}
return null;
}

@Override
public String toString() {
return "<native fn>";
}

@Override
public int arity() {
return 2;
}

@Override
public boolean isInternal() {
return true;
}
}

class FileReadFunction implements TahiniCallable {

@Override
public Object call(Interpreter interpreter, List<Object> args) {
if (args.size() != 1) {
throw new RuntimeError(null, "Expected 1 argument (file path) but got " + args.size() + ".", null);
}
Object arg = args.get(0);

if (!(arg instanceof String path)) {
throw new RuntimeError(null, "Expected a string (file path) but got " + arg + ".", null);
}

try {
byte[] fileBytes = Files.readAllBytes(Path.of(path));
return new String(fileBytes, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeError(null, "Error reading file: " + e.getMessage(), null);
}
}

@Override
public String toString() {
return "<native fn>";
}

@Override
public int arity() {
return 1;
}

@Override
public boolean isInternal() {
return true;
}
}

class HashmapValuesFunction implements TahiniCallable {

@Override
public Object call(Interpreter interpreter, List<Object> args) {
if (args.size() != 1) {
throw new RuntimeError(null, "Expected 1 argument but got " + args.size() + ".", null);
}
Object arg = args.get(0);
if (!(arg instanceof Map)) {
throw new RuntimeError(null, "Expected a hashmap but got " + arg + ".", null);
}
return new ArrayList<>(((Map) arg).values());
}

@Override
public String toString() {
return "<native fn>";
}

@Override
public int arity() {
return 1;
}

@Override
public boolean isInternal() {
return true;
}
}

class HashmapKeysFunction implements TahiniCallable {

@Override
public Object call(Interpreter interpreter, List<Object> args) {
if (args.size() != 1) {
throw new RuntimeError(null, "Expected 1 argument but got " + args.size() + ".", null);
}
Object arg = args.get(0);
if (!(arg instanceof Map)) {
throw new RuntimeError(null, "Expected a hashmap but got " + arg + ".", null);
}
return new ArrayList<>(((Map) arg).keySet());
}

@Override
public String toString() {
return "<native fn>";
}

@Override
public int arity() {
return 1;
}

@Override
public boolean isInternal() {
return true;
}
}

class InputFunction implements TahiniCallable {
Expand All @@ -30,6 +181,11 @@ public String toString() {
public int arity() {
return 0;
}

@Override
public boolean isInternal() {
return false;
}
}

class ArrayLengthFunction implements TahiniCallable {
Expand Down Expand Up @@ -59,4 +215,9 @@ public String toString() {
public int arity() {
return 1;
}

@Override
public boolean isInternal() {
return false;
}
}
Loading

0 comments on commit d33a02e

Please sign in to comment.