Skip to content

Commit

Permalink
Merge pull request #7 from anirudhgray/refactor-contracts
Browse files Browse the repository at this point in the history
Refactor contracts
  • Loading branch information
anirudhgray authored Nov 2, 2024
2 parents 9f2f9a4 + d68ed96 commit 11da21a
Show file tree
Hide file tree
Showing 17 changed files with 204 additions and 23 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@
- Database support and SQL queries
- Cross-language import of Java classes
- Mocking features
- Run file by disabling/enabling contracts

### Changed
- Enhanced HTTP Request/Response support for methods (POST, PUT, DELETE, etc.)

---

## [0.4.0] - 2024-11-02

### Added
- `typeOf` function to determine the type of a variable.

### Fixed
- Add custom error messages for failing contracts (like for assertions).
- Fixed function calls in the REPL.

### Changed
- Critical (runtimeError) vs warning/check (stderr) assertions

## [0.3.2] - 2024-10-31

### Fixed
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,17 @@ Precondition failed.
[line 15]
```

`assertion` works in a similar way, except it can be used anywhere in the function body, and outside of functions as well.
`assertion` works in a similar way, except it can be used anywhere in the function body, and outside of functions as well. A non-critical form of `assertion` is also available — `check`. This will log a warning to stderr, but will not interrupt the program execution by throwing an error.

```
var check = false;
assertion: check, "Check should be true!";
var recipeName = "Muffins";
var sugar = 50;
assertion: recipeName != nil, "Recipe name cannot be nil!";
check: sugar < 40, "Sugar might be too much.";
```

In the above example, if `recipeName` is `nil`, a critical error will be thrown, but if `sugar` is more than/equal to 40, a warning will be logged to stderr.

#### Unit Tests

Inspired by https://ziglang.org/documentation/master/#Zig-Test and https://dlang.org/spec/unittest.html
Expand Down Expand Up @@ -434,6 +438,7 @@ Apart from its standard library (`larder`), Tahini provides a set of built-in fu
- `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.
- `typeOf(value)` - Get the type of a value as a string.

## Standard Library

Expand All @@ -458,6 +463,7 @@ scoop "larder/io";
var x = math::sqrt(25);
print x;
writeFile("output.txt", "Hello, Tahini!");
print typeOf(x); // "number"
```

## Stretch Goals
Expand Down
5 changes: 4 additions & 1 deletion tahini/app/src/main/java/com/tahini/lang/Interpreter.java
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,11 @@ public Expr evaluateContractConditions(List<Expr> conditions, Environment env) {
@Override
public Void visitContractStmt(Stmt.Contract stmt) {
Object condition = evaluateContractConditions(stmt.conditions, environment);
if (condition != null) {
if (condition != null && stmt.type.type == TokenType.ASSERTION) {
throw new RuntimeError(stmt.type, stmt.type.lexeme + " contract failed (" + stmt.msg + ")", new ArrayList<>());
} else if (condition != null && stmt.type.type == TokenType.WARNING) {
// stderr
System.err.println("Warning (" + stmt.msg + ") [" + stmt.type.filename + ":" + stmt.type.line + "]");
}
return null;
}
Expand Down
22 changes: 16 additions & 6 deletions tahini/app/src/main/java/com/tahini/lang/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ private Stmt statement() {
if (match(TokenType.RETURN)) {
return returnStatement();
}
if (match(TokenType.ASSERTION)) {
return assertationStatement();
if (match(TokenType.ASSERTION, TokenType.WARNING)) {
return assertionStatement();
}
return expressionStatement();
}
Expand Down Expand Up @@ -279,28 +279,38 @@ private Stmt function() {
// TODO add support for return check in postcondition
List<Expr> preconditions = new ArrayList<>();
List<Expr> postconditions = new ArrayList<>();
Object premsg = null;
Object postmsg = null;
if (match(TokenType.PRECONDITION)) {
consume(TokenType.COLON, "Expect ':' after 'precondition'.");
do {
if (match(TokenType.STRING)) {
premsg = previous().literal;
break;
}
preconditions.add(expression());
} while (match(TokenType.COMMA));
}
if (match(TokenType.POSTCONDITION)) {
consume(TokenType.COLON, "Expect ':' after 'postcondition'.");
do {
if (match(TokenType.STRING)) {
postmsg = previous().literal;
break;
}
postconditions.add(expression());
} while (match(TokenType.COMMA));
}

consume(TokenType.LEFT_BRACE, "Expect '{' before function body.");
List<Stmt> body = block();
endFunction();
return new Stmt.Function(name, parameters, body, preconditions, postconditions);
return new Stmt.Function(name, parameters, body, preconditions, postconditions, premsg, postmsg);
}

private Stmt assertationStatement() {
private Stmt assertionStatement() {
Token type = previous();
consume(TokenType.COLON, "Expect ':' after 'assertation'.");
consume(TokenType.COLON, "Expect ':' after assertion type.");
List<Expr> conditions = new ArrayList<>();
Object msg = null;
do {
Expand All @@ -310,7 +320,7 @@ private Stmt assertationStatement() {
}
conditions.add(expression());
} while (match(TokenType.COMMA));
consume(TokenType.SEMICOLON, "Expect ';' after assertation.");
consume(TokenType.SEMICOLON, "Expect ';' after an assertion.");
return new Stmt.Contract(type, conditions, msg);
}

Expand Down
1 change: 1 addition & 0 deletions tahini/app/src/main/java/com/tahini/lang/Scanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Scanner {
keywords.put("precondition", TokenType.PRECONDITION);
keywords.put("postcondition", TokenType.POSTCONDITION);
keywords.put("assertion", TokenType.ASSERTION);
keywords.put("check", TokenType.WARNING);
keywords.put("test", TokenType.TEST);
keywords.put("scoop", TokenType.SCOOP);
keywords.put("into", TokenType.INTO);
Expand Down
47 changes: 47 additions & 0 deletions tahini/app/src/main/java/com/tahini/lang/StandardLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static void addStandardFunctions(Environment globalEnv) {
globalEnv.define("input", new InputFunction());
globalEnv.define("len", new ArrayLengthFunction());
globalEnv.define("clock", new UnixEpochSecondsFunction());
globalEnv.define("typeOf", new TypeOfFunction());
}

public static void addInternalFunctions(Environment globalEnv) {
Expand All @@ -31,6 +32,52 @@ public static void addInternalFunctions(Environment globalEnv) {
}
}

class TypeOfFunction implements TahiniCallable {

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

@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 == null) {
return "nil";
}
return switch (arg) {
case String s ->
"string";
case Double d ->
"number";
case List<?> l ->
"array";
case Map<?, ?> m ->
"hashmap";
case Boolean b ->
"boolean";
// TahiniFunction or TahiniCallable
case TahiniCallable f ->
"function";
default ->
"unknown";
};
}

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

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

class HTTPRestFunction implements TahiniCallable {

@Override
Expand Down
6 changes: 5 additions & 1 deletion tahini/app/src/main/java/com/tahini/lang/Stmt.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ <R> R accept(Visitor<R> visitor) {
final Expr expression;
}
static class Function extends Stmt {
Function(Token name, List<Token> params, List<Stmt> body, List<Expr> preconditions, List<Expr> postconditions) {
Function(Token name, List<Token> params, List<Stmt> body, List<Expr> preconditions, List<Expr> postconditions, Object premsg, Object postmsg) {
this.name = name;
this.params = params;
this.body = body;
this.preconditions = preconditions;
this.postconditions = postconditions;
this.premsg = premsg;
this.postmsg = postmsg;
}

@Override
Expand All @@ -48,6 +50,8 @@ <R> R accept(Visitor<R> visitor) {
final List<Stmt> body;
final List<Expr> preconditions;
final List<Expr> postconditions;
final Object premsg;
final Object postmsg;
}
static class Test extends Stmt {
Test(Token name, Stmt body) {
Expand Down
2 changes: 1 addition & 1 deletion tahini/app/src/main/java/com/tahini/lang/Tahini.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ static void runtimeError(RuntimeError error) {
for (int i = callStack.size() - 1; i >= 0; i--) {
CallFrame frame = callStack.get(i);
System.err.println(" in " + frame.function);
System.err.print("[called at line " + frame.returnToLine + " in " + frame.returnToFilename + "]");
System.err.println("[called at line " + frame.returnToLine + " in " + frame.returnToFilename + "]");
}

hadRuntimeError = true;
Expand Down
6 changes: 4 additions & 2 deletions tahini/app/src/main/java/com/tahini/lang/TahiniFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ public Object call(Interpreter interpreter,

Expr failingPre = interpreter.evaluateContractConditions(declaration.preconditions, environment);
if (failingPre != null) {
String errormsg = declaration.premsg != null ? "Precondition failed: " + declaration.premsg : "Precondition failed.";
throw new RuntimeError(declaration.name,
"Precondition failed.", new ArrayList<>());
errormsg, new ArrayList<>());
}

Object returnValue = null;
Expand All @@ -45,8 +46,9 @@ public Object call(Interpreter interpreter,

Expr failingPost = interpreter.evaluateContractConditions(declaration.postconditions, environment);
if (failingPost != null) {
String errormsg = declaration.postmsg != null ? "Postcondition failed: " + declaration.postmsg : "Postcondition failed.";
throw new RuntimeError(declaration.name,
"Postcondition failed.", new ArrayList<>());
errormsg, new ArrayList<>());
}

return returnValue;
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 @@ -16,7 +16,7 @@ enum TokenType {
PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE,
EOF, BREAK, CONTINUE,
// Annotations.
PRECONDITION, POSTCONDITION, ASSERTION, TEST,
PRECONDITION, POSTCONDITION, ASSERTION, TEST, WARNING,
// Import
SCOOP, INTO, NAMESPACE_SEPARATOR
}
2 changes: 1 addition & 1 deletion tahini/app/src/main/java/com/tahini/tool/GenerateAst.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static void main(String[] args) throws IOException {

defineAst(outputDir, "Stmt", Arrays.asList(
"Expression : Expr expression",
"Function : Token name, List<Token> params, List<Stmt> body, List<Expr> preconditions, List<Expr> postconditions",
"Function : Token name, List<Token> params, List<Stmt> body, List<Expr> preconditions, List<Expr> postconditions, Object premsg, Object postmsg",
"Test : Token name, Stmt body",
"Print : Expr expression",
"If : Expr condition, Stmt thenBranch, Stmt elseBranch",
Expand Down
4 changes: 3 additions & 1 deletion tahini/app/src/main/resources/stdlib/math.tah
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// stdlib/math

fun min(a, b) {
fun min(a, b)
precondition: a != nil, b != nil, "Arguments must not be nil"
{
if (a < b) {
return a;
} else {
Expand Down
15 changes: 11 additions & 4 deletions tahini/app/src/main/resources/stdlib/random.tah
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
scoop "larder/math";

// Random float between 0 and 1
fun random() {
return _random();
fun random()
postcondition: 0 <= result, result <= 1
{
var result = _random();
return result;
}

// Random integer between min and max
fun randomInt(min, max) {
return floor(random() * (max - min + 1) + min);
fun randomInt(min, max)
precondition: min <= max
postcondition: min <= result, result <= max
{
var result = floor(random() * (max - min + 1) + min);
return result;
}
31 changes: 31 additions & 0 deletions tahini/tests/advcontract2.tah
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
fun safeSqrt(value)
precondition: value >= 0, "value must be greater than or equal to 0"
postcondition: x >= 0, "x must be greater than or equal to 0"
{
var x = value;
var tolerance = 0.00001; // Define a tolerance level for the approximation
var difference = x;

while (difference > tolerance) {
var newX = 0.5 * (x + value / x);
difference = x - newX;
if (difference < 0) {
difference = -difference;
}
x = newX;
}

return x;
}

var x = 25;
var y = safeSqrt(x); // y will be approximately 5
print y;

safeSqrt(-9); // This will trigger a precondition error since z < 0
print result;

// 5
// RuntimeError: Precondition failed: value must be greater than or equal to 0
// [at line 1 in advcontract2.tah] in <fn safeSqrt>
// [called at line 25 in advcontract2.tah]
4 changes: 2 additions & 2 deletions tahini/tests/assert_outside.tah
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var check = false;
assertion: check, "Check should be true!";
var checkvar = false;
assertion: checkvar, "Check should be true!";

// RuntimeError: assertion contract failed (Check should be true!)
// [at line 2 in assert_outside.tah]
9 changes: 9 additions & 0 deletions tahini/tests/check.tah
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
scoop "larder/math" into math;
check: math::max(4, 5) == 54;
var recipeName = "Muffins";
var sugar = 50;
assertion: recipeName != nil, "Recipe name cannot be nil!";
check: sugar < 40, "Sugar might be too much.";

// Warning (null) [check.tah:2]
// Warning (Sugar might be too much.) [check.tah:6]
Loading

0 comments on commit 11da21a

Please sign in to comment.