Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: packages
run: sudo add-apt-repository universe && sudo apt-get update && sudo apt-get install -y clang-14 llvm-14

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
node-version: '25'

- name: Install dependencies
run: npm ci
Expand Down
7 changes: 7 additions & 0 deletions exec-babel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#! /bin/bash
if [ -z "$1" ]; then
echo "Usage: $0 <test-name>"
exit 1
fi
complect -b babel -f fixtures/${1} -o output/${1}.js
node output/${1}.js
Comment thread
jarrodconnolly marked this conversation as resolved.
File renamed without changes.
29 changes: 29 additions & 0 deletions fixtures/function-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
func double n
make m n * 2
return m
end

func say word
print word
end

func add a b
make sum a + b
return sum
end

make x 5
make result 0
call double x into result
print result

call say 'Hello, World!'

make newWord 'Temporary string'
call say newWord
free newWord

make total 0
make a 10
call add a 5 into total
print total
43 changes: 42 additions & 1 deletion lib/ast/ast-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { TokenType } from '../tokenizer/token-type.js';
import { Program, VariableDeclaration, AssignmentExpression, BinaryExpression, IfStatement, WhileStatement, PrintStatement, FreeStatement, Identifier, NumericLiteral, StringLiteral } from './ir-nodes.js';
import { Program, VariableDeclaration, AssignmentExpression, BinaryExpression, IfStatement, WhileStatement, PrintStatement, FreeStatement, FunctionDeclaration, ReturnStatement, CallStatement, Identifier, NumericLiteral, StringLiteral } from './ir-nodes.js';

// ASTBuilder builds an IR AST from tokens using a state machine
export class ASTBuilder {
Expand Down Expand Up @@ -71,6 +71,43 @@ export class ASTBuilder {
const test = this.parseFullExpression();
const body = this.parseBlock('repeat');
return new WhileStatement(test, body, { start: { line: token.line, column: token.column }, end: { line: token.line, column: token.column } });
} else if (token.value === 'func') {
const nameToken = this.tokens[this.index++];
if (nameToken.type !== TokenType.identifier) throw new Error(`Expected identifier after 'func'`);
const params = [];
while (this.index < this.tokens.length && this.tokens[this.index].type === TokenType.identifier) {
params.push(this.tokens[this.index++].value);
}
const body = this.parseBlock('end');
return new FunctionDeclaration(nameToken.value, params, body, { start: { line: token.line, column: token.column }, end: { line: token.line, column: token.column } });
} else if (token.value === 'return') {
const varToken = this.tokens[this.index++];
if (varToken.type !== TokenType.identifier) throw new Error(`Expected identifier after 'return'`);
return new ReturnStatement(varToken.value, { start: { line: token.line, column: token.column }, end: { line: token.line, column: token.column } });
} else if (token.value === 'call') {
const calleeToken = this.tokens[this.index++];
if (calleeToken.type !== TokenType.identifier) throw new Error(`Expected identifier after 'call'`);
const args = [];
while (this.index < this.tokens.length) {
const next = this.tokens[this.index];
if (next.type === TokenType.identifier || next.type === TokenType.number || next.type === TokenType.string) {
const expr = this.parseExpression(next);
args.push(expr);
this.index++;
} else {
break;
}
}
let result = null;
if (this.index < this.tokens.length && this.tokens[this.index].type === TokenType.keyword && this.tokens[this.index].value === 'into') {
this.index++; // consume 'into'
if (this.index < this.tokens.length && this.tokens[this.index].type === TokenType.identifier) {
result = this.tokens[this.index++].value;
} else {
throw new Error(`Expected identifier after 'into'`);
}
}
return new CallStatement(calleeToken.value, args, result, { start: { line: token.line, column: token.column }, end: { line: token.line, column: token.column } });
} else {
throw new Error(`Unexpected keyword: ${token.value}`);
}
Expand Down Expand Up @@ -110,4 +147,8 @@ export class ASTBuilder {
throw new Error(`Unexpected expression token: ${token.type} ${token.value}`);
}
}

canStartExpression(token) {
return token.type === TokenType.identifier || token.type === TokenType.number || token.type === TokenType.string;
}
}
108 changes: 107 additions & 1 deletion lib/ast/ast-builder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import assert from "node:assert";

import { ASTBuilder } from "./ast-builder.js";
import { TokenType } from "../tokenizer/token-type.js";
import { Program, VariableDeclaration, AssignmentExpression, BinaryExpression, IfStatement, WhileStatement, PrintStatement, Identifier, NumericLiteral, StringLiteral } from "./ir-nodes.js";
import { Program, VariableDeclaration, AssignmentExpression, BinaryExpression, IfStatement, WhileStatement, PrintStatement, FunctionDeclaration, ReturnStatement, CallStatement, Identifier, NumericLiteral, StringLiteral } from "./ir-nodes.js";

async function* tokenGenerator(tokens) {
for (const token of tokens) {
Expand Down Expand Up @@ -160,4 +160,110 @@ describe('ASTBuilder', () => {
assert(stmt.body.length === 1);
assert(stmt.body[0] instanceof AssignmentExpression);
});

it('function declaration', async () => {
const tokens = [
{ type: TokenType.keyword, value: 'func', line: 1, column: 1 },
{ type: TokenType.identifier, value: 'add', line: 1, column: 10 },
{ type: TokenType.identifier, value: 'a', line: 1, column: 14 },
{ type: TokenType.identifier, value: 'b', line: 1, column: 16 },
{ type: TokenType.keyword, value: 'return', line: 2, column: 3 },
{ type: TokenType.identifier, value: 'result', line: 2, column: 10 },
{ type: TokenType.keyword, value: 'end', line: 3, column: 1 }
];

const builder = new ASTBuilder();
const ir = await builder.build(tokenGenerator(tokens));

assert(ir.statements.length === 1);
const stmt = ir.statements[0];
assert(stmt instanceof FunctionDeclaration);
assert(stmt.name === 'add');
assert.deepEqual(stmt.params, ['a', 'b']);
assert(stmt.body.length === 1);
assert(stmt.body[0] instanceof ReturnStatement);
assert(stmt.body[0].argument === 'result');
});

it('return statement', async () => {
const tokens = [
{ type: TokenType.keyword, value: 'return', line: 1, column: 1 },
{ type: TokenType.identifier, value: 'x', line: 1, column: 8 }
];

const builder = new ASTBuilder();
const ir = await builder.build(tokenGenerator(tokens));

assert(ir.statements.length === 1);
const stmt = ir.statements[0];
assert(stmt instanceof ReturnStatement);
assert(stmt.argument === 'x');
});

it('call statement with result', async () => {
const tokens = [
{ type: TokenType.keyword, value: 'call', line: 1, column: 1 },
{ type: TokenType.identifier, value: 'double', line: 1, column: 6 },
{ type: TokenType.identifier, value: 'x', line: 1, column: 13 },
{ type: TokenType.keyword, value: 'into', line: 1, column: 15 },
{ type: TokenType.identifier, value: 'result', line: 1, column: 20 }
];

const builder = new ASTBuilder();
const ir = await builder.build(tokenGenerator(tokens));

assert(ir.statements.length === 1);
const stmt = ir.statements[0];
assert(stmt instanceof CallStatement);
assert(stmt.callee === 'double');
assert(stmt.arguments.length === 1);
assert(stmt.arguments[0] instanceof Identifier);
assert(stmt.arguments[0].name === 'x');
assert(stmt.result === 'result');
});

it('call statement void', async () => {
const tokens = [
{ type: TokenType.keyword, value: 'call', line: 1, column: 1 },
{ type: TokenType.identifier, value: 'print', line: 1, column: 6 },
{ type: TokenType.string, value: 'hello', line: 1, column: 12 }
];

const builder = new ASTBuilder();
const ir = await builder.build(tokenGenerator(tokens));

assert(ir.statements.length === 1);
const stmt = ir.statements[0];
assert(stmt instanceof CallStatement);
assert(stmt.callee === 'print');
assert(stmt.arguments.length === 1);
assert(stmt.arguments[0] instanceof StringLiteral);
assert(stmt.arguments[0].value === 'hello');
assert(stmt.result === null);
});

it('call statement with multiple args and result', async () => {
const tokens = [
{ type: TokenType.keyword, value: 'call', line: 1, column: 1 },
{ type: TokenType.identifier, value: 'add', line: 1, column: 6 },
{ type: TokenType.identifier, value: 'a', line: 1, column: 10 },
{ type: TokenType.number, value: 5, line: 1, column: 12 },
{ type: TokenType.keyword, value: 'into', line: 1, column: 14 },
{ type: TokenType.identifier, value: 'total', line: 1, column: 19 }
];

const builder = new ASTBuilder();
const ir = await builder.build(tokenGenerator(tokens));

assert(ir.statements.length === 1);
const stmt = ir.statements[0];
assert(stmt instanceof CallStatement);
assert(stmt.callee === 'add');
assert(stmt.arguments.length === 2);
assert(stmt.arguments[0] instanceof Identifier);
assert(stmt.arguments[0].name === 'a');
assert(stmt.arguments[1] instanceof NumericLiteral);
assert(stmt.arguments[1].value === 5);
assert(stmt.result === 'total');
});
});
28 changes: 28 additions & 0 deletions lib/ast/ir-nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,32 @@ export class StringLiteral extends Expression {
super(loc);
this.value = value; // string (without quotes)
}
}

// Function declaration (e.g., function name param1 param2 ... end)
export class FunctionDeclaration extends Statement {
constructor(name, params, body, loc) {
super(loc);
this.name = name; // string (function name)
this.params = params; // array of strings (parameter names)
this.body = body; // array of Statement
}
}

// Return statement (e.g., return variable)
export class ReturnStatement extends Statement {
constructor(argument, loc) {
super(loc);
this.argument = argument; // string (variable name to return)
}
}

// Call statement (e.g., call func arg1 arg2 result)
export class CallStatement extends Statement {
constructor(callee, args, result, loc) {
super(loc);
this.callee = callee; // string (function name)
this.arguments = args; // array of Expression
this.result = result; // string or null (result variable, null for void calls)
}
}
41 changes: 40 additions & 1 deletion lib/babel-ast/babel-translator.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CodeGenerator } from '@babel/generator';
import _traverse from "@babel/traverse";
const traverse = _traverse.default;

import { VariableDeclaration, AssignmentExpression, BinaryExpression, IfStatement, WhileStatement, PrintStatement, FreeStatement, Identifier, NumericLiteral, StringLiteral } from '../ast/ir-nodes.js';
import { VariableDeclaration, AssignmentExpression, BinaryExpression, IfStatement, WhileStatement, PrintStatement, FreeStatement, FunctionDeclaration, ReturnStatement, CallStatement, Identifier, NumericLiteral, StringLiteral } from '../ast/ir-nodes.js';

export class BabelTranslator {
translate(ir) {
Expand Down Expand Up @@ -83,6 +83,45 @@ export class BabelTranslator {
const exprStmt = t.expressionStatement(assign);
exprStmt.loc = stmt.loc;
return exprStmt;
} else if (stmt instanceof FunctionDeclaration) {
const id = t.identifier(stmt.name);
id.loc = stmt.loc;
const params = stmt.params.map(p => {
const param = t.identifier(p);
param.loc = stmt.loc;
return param;
});
const body = t.blockStatement(stmt.body.map(s => this.translateStatement(s)));
const func = t.functionDeclaration(id, params, body);
func.loc = stmt.loc;
return func;
} else if (stmt instanceof ReturnStatement) {
const arg = t.identifier(stmt.argument);
arg.loc = stmt.loc;
const ret = t.returnStatement(arg);
ret.loc = stmt.loc;
return ret;
} else if (stmt instanceof CallStatement) {
const callee = t.identifier(stmt.callee);
callee.loc = stmt.loc;
const args = stmt.arguments.map(a => this.translateExpression(a));
const call = t.callExpression(callee, args);
call.loc = stmt.loc;

if (stmt.result) {
// Assign result to variable
const resultId = t.identifier(stmt.result);
resultId.loc = stmt.loc;
const assign = t.assignmentExpression('=', resultId, call);
const exprStmt = t.expressionStatement(assign);
exprStmt.loc = stmt.loc;
return exprStmt;
} else {
// Void call
const exprStmt = t.expressionStatement(call);
exprStmt.loc = stmt.loc;
return exprStmt;
}
} else {
throw new Error(`Unknown statement type: ${stmt.constructor.name}`);
}
Expand Down
44 changes: 43 additions & 1 deletion lib/babel-ast/babel-translator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { describe, it } from "node:test";
import assert from "node:assert";

import { BabelTranslator } from "./babel-translator.js";
import { Program, VariableDeclaration, AssignmentExpression, BinaryExpression, IfStatement, WhileStatement, PrintStatement, Identifier, NumericLiteral, StringLiteral } from "../ast/ir-nodes.js";
import { Program, VariableDeclaration, AssignmentExpression, BinaryExpression, IfStatement, WhileStatement, PrintStatement, FunctionDeclaration, ReturnStatement, CallStatement, Identifier, NumericLiteral, StringLiteral } from "../ast/ir-nodes.js";

describe('BabelTranslator', () => {
it('translates variable declaration with number', () => {
Expand Down Expand Up @@ -154,4 +154,46 @@ while (a < 10) {
}`;
assert(result.code.trim() === expected);
});

it('translates function declaration', () => {
const ir = new Program([
new FunctionDeclaration('add', ['a', 'b'], [
new ReturnStatement('result', { start: { line: 2, column: 3 }, end: { line: 2, column: 10 } })
], { start: { line: 1, column: 1 }, end: { line: 3, column: 1 } })
], null);

const translator = new BabelTranslator();
const result = translator.translate(ir);

const expected = `function add(a, b) {
return result;
}`;
assert(result.code.trim() === expected);
});

it('translates call statement with result', () => {
const ir = new Program([
new VariableDeclaration('result', new NumericLiteral(0, { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }), { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }),
new CallStatement('double', [new Identifier('x', { start: { line: 1, column: 13 }, end: { line: 1, column: 13 } })], 'result', { start: { line: 1, column: 1 }, end: { line: 1, column: 15 } })
], null);

const translator = new BabelTranslator();
const result = translator.translate(ir);

const expected = `let result = 0;
result = double(x);`;
assert(result.code.trim() === expected);
});

it('translates call statement void', () => {
const ir = new Program([
new CallStatement('print', [new StringLiteral('hello', { start: { line: 1, column: 12 }, end: { line: 1, column: 18 } })], null, { start: { line: 1, column: 1 }, end: { line: 1, column: 18 } })
], null);

const translator = new BabelTranslator();
const result = translator.translate(ir);

const expected = `print("hello");`;
assert(result.code.trim() === expected);
});
});
Loading