From 2f9b0b23a411aa4d689a41c943306c01747d33b3 Mon Sep 17 00:00:00 2001 From: Jarrod Connolly Date: Fri, 26 Dec 2025 18:12:45 -0800 Subject: [PATCH 1/6] add functions to our ast and babel generator --- exec-babel.sh | 7 +++ exec.sh => exec-llvm.sh | 0 fixtures/function-test | 15 +++++ lib/ast/ast-builder.js | 29 ++++++++- lib/ast/ast-builder.test.js | 82 +++++++++++++++++++++++++- lib/ast/ir-nodes.js | 28 +++++++++ lib/babel-ast/babel-translator.js | 41 ++++++++++++- lib/babel-ast/babel-translator.test.js | 44 +++++++++++++- lib/tokenizer/keywords.js | 4 ++ todo.md | 4 +- 10 files changed, 248 insertions(+), 6 deletions(-) create mode 100755 exec-babel.sh rename exec.sh => exec-llvm.sh (100%) create mode 100644 fixtures/function-test diff --git a/exec-babel.sh b/exec-babel.sh new file mode 100755 index 0000000..abc50be --- /dev/null +++ b/exec-babel.sh @@ -0,0 +1,7 @@ +#! /bin/bash +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi +complect -b babel -f fixtures/${1} -o output/${1}.js +node output/${1}.js \ No newline at end of file diff --git a/exec.sh b/exec-llvm.sh similarity index 100% rename from exec.sh rename to exec-llvm.sh diff --git a/fixtures/function-test b/fixtures/function-test new file mode 100644 index 0000000..6ac6d8f --- /dev/null +++ b/fixtures/function-test @@ -0,0 +1,15 @@ +func double n + make m n * 2 + return m +end + +func say word + print word +end + +make x 5 +make result 0 +call double x result +print result + +call say 'Hello, World!' \ No newline at end of file diff --git a/lib/ast/ast-builder.js b/lib/ast/ast-builder.js index b9a1cf9..0882271 100644 --- a/lib/ast/ast-builder.js +++ b/lib/ast/ast-builder.js @@ -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 { @@ -71,6 +71,33 @@ 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 = []; + let result = null; + while (this.index < this.tokens.length) { + const expr = this.parseFullExpression(); + args.push(expr); + if (this.index < this.tokens.length && this.tokens[this.index].type === TokenType.identifier) { + result = this.tokens[this.index++].value; + break; + } + } + 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}`); } diff --git a/lib/ast/ast-builder.test.js b/lib/ast/ast-builder.test.js index df5a8f0..4f48350 100644 --- a/lib/ast/ast-builder.test.js +++ b/lib/ast/ast-builder.test.js @@ -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) { @@ -160,4 +160,84 @@ 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.identifier, value: 'result', line: 1, column: 15 } + ]; + + 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); + }); }); \ No newline at end of file diff --git a/lib/ast/ir-nodes.js b/lib/ast/ir-nodes.js index 37ced17..323546a 100644 --- a/lib/ast/ir-nodes.js +++ b/lib/ast/ir-nodes.js @@ -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) + } } \ No newline at end of file diff --git a/lib/babel-ast/babel-translator.js b/lib/babel-ast/babel-translator.js index bee25b2..1752477 100644 --- a/lib/babel-ast/babel-translator.js +++ b/lib/babel-ast/babel-translator.js @@ -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) { @@ -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}`); } diff --git a/lib/babel-ast/babel-translator.test.js b/lib/babel-ast/babel-translator.test.js index 69c7fd0..74fba01 100644 --- a/lib/babel-ast/babel-translator.test.js +++ b/lib/babel-ast/babel-translator.test.js @@ -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', () => { @@ -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); + }); }); \ No newline at end of file diff --git a/lib/tokenizer/keywords.js b/lib/tokenizer/keywords.js index c32357d..8c073e7 100644 --- a/lib/tokenizer/keywords.js +++ b/lib/tokenizer/keywords.js @@ -15,4 +15,8 @@ export const keywords = [ 'repeat', 'print', 'free', + 'func', + 'return', + 'call', + 'end', ]; diff --git a/todo.md b/todo.md index b48986b..300a0ec 100644 --- a/todo.md +++ b/todo.md @@ -1,8 +1,8 @@ -# Things To Do +# Things To Do (Never things completed) ## Immediate (Next Up Changes) +* **Functions (LLVM)**: Implement LLVM backend support for functions * **Language Grammar Enhancements**: - - **Functions** (highest priority): Implement function definitions and calls for code organization - Add array support (declaration, indexing, operations) - Add file I/O operations (read/write files) - Add break/continue statements for loop control From b60676372dc4db386b23d0c18011216c3fc6f3b7 Mon Sep 17 00:00:00 2001 From: Jarrod Connolly Date: Fri, 26 Dec 2025 22:02:50 -0800 Subject: [PATCH 2/6] functions with multi param functions working on llvm --- fixtures/function-test | 18 ++++- lib/ast/ast-builder.js | 22 +++++- lib/ast/ast-builder.test.js | 28 ++++++- lib/llvm/llvm-translator.js | 129 ++++++++++++++++++++++++++++++- lib/llvm/llvm-translator.test.js | 47 +++++++++++ lib/tokenizer/keywords.js | 1 + todo.md | 1 - 7 files changed, 237 insertions(+), 9 deletions(-) create mode 100644 lib/llvm/llvm-translator.test.js diff --git a/fixtures/function-test b/fixtures/function-test index 6ac6d8f..fc1c682 100644 --- a/fixtures/function-test +++ b/fixtures/function-test @@ -7,9 +7,23 @@ 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 result +call double x into result print result -call say 'Hello, World!' \ No newline at end of file +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 \ No newline at end of file diff --git a/lib/ast/ast-builder.js b/lib/ast/ast-builder.js index 0882271..9e54970 100644 --- a/lib/ast/ast-builder.js +++ b/lib/ast/ast-builder.js @@ -88,13 +88,23 @@ export class ASTBuilder { const calleeToken = this.tokens[this.index++]; if (calleeToken.type !== TokenType.identifier) throw new Error(`Expected identifier after 'call'`); const args = []; - let result = null; while (this.index < this.tokens.length) { - const expr = this.parseFullExpression(); - args.push(expr); + 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; - break; + } 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 } }); @@ -137,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; + } } \ No newline at end of file diff --git a/lib/ast/ast-builder.test.js b/lib/ast/ast-builder.test.js index 4f48350..c396f59 100644 --- a/lib/ast/ast-builder.test.js +++ b/lib/ast/ast-builder.test.js @@ -205,7 +205,8 @@ describe('ASTBuilder', () => { { 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.identifier, value: 'result', line: 1, column: 15 } + { type: TokenType.keyword, value: 'into', line: 1, column: 15 }, + { type: TokenType.identifier, value: 'result', line: 1, column: 20 } ]; const builder = new ASTBuilder(); @@ -240,4 +241,29 @@ describe('ASTBuilder', () => { 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'); + }); }); \ No newline at end of file diff --git a/lib/llvm/llvm-translator.js b/lib/llvm/llvm-translator.js index e1e92a8..1fbc3ba 100644 --- a/lib/llvm/llvm-translator.js +++ b/lib/llvm/llvm-translator.js @@ -5,7 +5,7 @@ */ import llvm from 'llvm-bindings'; -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 LLVMTranslator { constructor() { @@ -15,6 +15,7 @@ export class LLVMTranslator { this.variables = new Map(); // name -> { type: 'int'|'string', value: LLVM Value } this.function = null; this.stringLiterals = new Map(); // value -> global string constant + this.functionSignatures = new Map(); // name -> array of types } translate(ir) { @@ -25,6 +26,9 @@ export class LLVMTranslator { // Declare runtime functions this.declareRuntimeFunctions(); + // Collect function signatures from call sites + this.collectFunctionSignatures(ir.statements); + // Create main function const mainType = llvm.FunctionType.get(this.builder.getInt32Ty(), [], false); this.function = llvm.Function.Create(mainType, llvm.Function.LinkageTypes.ExternalLinkage, 'main', this.module); @@ -51,6 +55,36 @@ export class LLVMTranslator { return this.module.print(); } + collectFunctionSignatures(statements) { + for (const stmt of statements) { + if (stmt instanceof CallStatement) { + const signature = this.functionSignatures.get(stmt.callee) || []; + const argTypes = stmt.arguments.map(arg => this.getExpressionType(arg)); + + // Update signature with inferred types + for (let i = 0; i < argTypes.length; i++) { + if (i >= signature.length) { + signature.push(argTypes[i]); + } else if (signature[i] !== argTypes[i]) { + // Type conflict - for now, prefer string over int + if (signature[i] === 'int' && argTypes[i] === 'string') { + signature[i] = 'string'; + } + } + } + + this.functionSignatures.set(stmt.callee, signature); + } else if (stmt instanceof FunctionDeclaration) { + // Recursively collect signatures from function bodies + this.collectFunctionSignatures(stmt.body); + } else if (stmt instanceof IfStatement) { + this.collectFunctionSignatures(stmt.consequent); + } else if (stmt instanceof WhileStatement) { + this.collectFunctionSignatures(stmt.body); + } + } + } + declareRuntimeFunctions() { // Declare printf const printfType = llvm.FunctionType.get( @@ -158,6 +192,35 @@ export class LLVMTranslator { } else if (stmt instanceof WhileStatement) { this.translateWhile(stmt); + } else if (stmt instanceof FunctionDeclaration) { + this.translateFunctionDeclaration(stmt); + + } else if (stmt instanceof ReturnStatement) { + const varInfo = this.variables.get(stmt.argument); + if (!varInfo) { + throw new Error(`Undefined variable: ${stmt.argument}`); + } + const value = this.builder.CreateLoad(varInfo.type === 'int' ? this.builder.getInt32Ty() : this.builder.getInt8PtrTy(), varInfo.value); + this.builder.CreateRet(value); + + } else if (stmt instanceof CallStatement) { + const callee = this.module.getFunction(stmt.callee); + if (!callee) { + throw new Error(`Undefined function: ${stmt.callee}`); + } + + const args = stmt.arguments.map(arg => this.translateExpression(arg)); + const call = this.builder.CreateCall(callee, args, stmt.result ? 'call' : ''); + + if (stmt.result) { + // Store result in the pre-declared variable + const resultVar = this.variables.get(stmt.result); + if (!resultVar) { + throw new Error(`Undefined result variable: ${stmt.result}`); + } + this.builder.CreateStore(call, resultVar.value); + } + } else { throw new Error(`Unknown statement type: ${stmt.constructor.name}`); } @@ -400,4 +463,68 @@ export class LLVMTranslator { // Exit this.builder.SetInsertPoint(exitBB); } + + translateFunctionDeclaration(stmt) { + // For now, assume all functions return int (we can extend this later) + const returnType = this.builder.getInt32Ty(); + + // Get parameter types from collected signatures, default to int + const signature = this.functionSignatures.get(stmt.name) || []; + const paramTypes = stmt.params.map((param, i) => { + const inferredType = signature[i] || 'int'; + return inferredType === 'string' ? this.builder.getInt8PtrTy() : this.builder.getInt32Ty(); + }); + + // Create function type + const funcType = llvm.FunctionType.get(returnType, paramTypes, false); + + // Create function + const func = llvm.Function.Create(funcType, llvm.Function.LinkageTypes.InternalLinkage, stmt.name, this.module); + + // Create entry block + const entryBB = llvm.BasicBlock.Create(this.context, 'entry', func); + + // Save current builder position and variables + const savedInsertPoint = this.builder.GetInsertBlock(); + const savedVariables = new Map(this.variables); + + // Set up function scope + this.builder.SetInsertPoint(entryBB); + this.variables.clear(); // Start with fresh scope + + // Set up parameters + for (let i = 0; i < stmt.params.length; i++) { + const param = func.getArg(i); + param.name = stmt.params[i]; + + const paramType = signature[i] || 'int'; + + // Create alloca for parameter + const llvmType = paramType === 'string' ? this.builder.getInt8PtrTy() : this.builder.getInt32Ty(); + const alloca = this.builder.CreateAlloca(llvmType, null, stmt.params[i]); + this.builder.CreateStore(param, alloca); + + // Add to variables map + this.variables.set(stmt.params[i], { type: paramType, value: alloca }); + } + + // Translate function body + for (const bodyStmt of stmt.body) { + this.translateStatement(bodyStmt); + } + + // If no return statement, add a default return (though our parser requires returns) + if (!entryBB.getTerminator()) { + this.builder.CreateRet(this.builder.getInt32(0)); + } + + // Verify function + if (llvm.verifyFunction(func)) { + throw new Error(`Function verification failed for ${stmt.name}`); + } + + // Restore previous state + this.builder.SetInsertPoint(savedInsertPoint); + this.variables = savedVariables; + } } \ No newline at end of file diff --git a/lib/llvm/llvm-translator.test.js b/lib/llvm/llvm-translator.test.js new file mode 100644 index 0000000..1f41e33 --- /dev/null +++ b/lib/llvm/llvm-translator.test.js @@ -0,0 +1,47 @@ +/* Complect - Compiler for the Complect programming language + * + * Copyright © 2024 Jarrod Connolly + * MIT License +*/ +import { describe, it } from "node:test"; +import assert from "node:assert"; + +import { LLVMTranslator } from "./llvm-translator.js"; +import { Program, VariableDeclaration, FunctionDeclaration, ReturnStatement, CallStatement, Identifier, NumericLiteral } from "../ast/ir-nodes.js"; + +describe('LLVMTranslator', () => { + it('translates function declaration', () => { + const ir = new Program([ + new FunctionDeclaration('double', ['n'], [ + new VariableDeclaration('result', new NumericLiteral(42, { start: { line: 2, column: 5 }, end: { line: 2, column: 7 } }), { start: { line: 2, column: 1 }, end: { line: 2, column: 7 } }), + new ReturnStatement('result', { start: { line: 3, column: 3 }, end: { line: 3, column: 10 } }) + ], { start: { line: 1, column: 1 }, end: { line: 4, column: 1 } }) + ], null); + + const translator = new LLVMTranslator(); + const result = translator.translate(ir); + + // Check that the LLVM IR contains the function definition + assert(result.includes('define internal i32 @double')); + assert(result.includes('ret i32')); + }); + + it('translates call statement with result', () => { + const ir = new Program([ + new FunctionDeclaration('double', ['n'], [ + new VariableDeclaration('result', new NumericLiteral(42, { start: { line: 2, column: 5 }, end: { line: 2, column: 7 } }), { start: { line: 2, column: 1 }, end: { line: 2, column: 7 } }), + new ReturnStatement('result', { start: { line: 3, column: 3 }, end: { line: 3, column: 10 } }) + ], { start: { line: 1, column: 1 }, end: { line: 4, column: 1 } }), + new VariableDeclaration('x', new NumericLiteral(10, { start: { line: 5, column: 1 }, end: { line: 5, column: 1 } }), { start: { line: 5, column: 1 }, end: { line: 5, column: 1 } }), + new VariableDeclaration('result', new NumericLiteral(0, { start: { line: 6, column: 1 }, end: { line: 6, column: 1 } }), { start: { line: 6, column: 1 }, end: { line: 6, column: 1 } }), + new CallStatement('double', [new Identifier('x', { start: { line: 7, column: 13 }, end: { line: 7, column: 13 } })], 'result', { start: { line: 7, column: 1 }, end: { line: 7, column: 15 } }) + ], null); + + const translator = new LLVMTranslator(); + const result = translator.translate(ir); + + // Check that the LLVM IR contains a call instruction + assert(result.includes('call i32 @double')); + assert(result.includes('store i32 %call, i32* %result')); + }); +}); \ No newline at end of file diff --git a/lib/tokenizer/keywords.js b/lib/tokenizer/keywords.js index 8c073e7..dcc0a57 100644 --- a/lib/tokenizer/keywords.js +++ b/lib/tokenizer/keywords.js @@ -18,5 +18,6 @@ export const keywords = [ 'func', 'return', 'call', + 'into', 'end', ]; diff --git a/todo.md b/todo.md index 300a0ec..f23e2a3 100644 --- a/todo.md +++ b/todo.md @@ -1,7 +1,6 @@ # Things To Do (Never things completed) ## Immediate (Next Up Changes) -* **Functions (LLVM)**: Implement LLVM backend support for functions * **Language Grammar Enhancements**: - Add array support (declaration, indexing, operations) - Add file I/O operations (read/write files) From 129b23e9fb6734fbd6dc6602ff77d0366a7274ad Mon Sep 17 00:00:00 2001 From: Jarrod Connolly Date: Fri, 26 Dec 2025 22:18:37 -0800 Subject: [PATCH 3/6] add llvm-14 to github action os --- .github/workflows/test.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6952f33..a189cb6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,10 +7,13 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: packages + run: add-apt-repository universe && apt-get update && apt-get install -y llvm-toolchain-14 + - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '25' - name: Install dependencies run: npm ci From 8fec6b84317f5fb7b5a8e6d8683c254e95984ab7 Mon Sep 17 00:00:00 2001 From: Jarrod Connolly Date: Fri, 26 Dec 2025 22:19:37 -0800 Subject: [PATCH 4/6] sudo --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a189cb6..bd2e08e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,7 +8,7 @@ jobs: uses: actions/checkout@v4 - name: packages - run: add-apt-repository universe && apt-get update && apt-get install -y llvm-toolchain-14 + run: sudo add-apt-repository universe && sudo apt-get update && sudo apt-get install -y llvm-toolchain-14 - name: Install Node.js uses: actions/setup-node@v4 From dcb7e7adfa8ec66bdfbb738d46f45981d8eeccf5 Mon Sep 17 00:00:00 2001 From: Jarrod Connolly Date: Fri, 26 Dec 2025 22:21:02 -0800 Subject: [PATCH 5/6] change install --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bd2e08e..66242a6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,7 +8,7 @@ jobs: uses: actions/checkout@v4 - name: packages - run: sudo add-apt-repository universe && sudo apt-get update && sudo apt-get install -y llvm-toolchain-14 + 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 From 52bc49c582652982638c8ff3b3300716d3547c50 Mon Sep 17 00:00:00 2001 From: Jarrod Connolly Date: Fri, 26 Dec 2025 22:32:08 -0800 Subject: [PATCH 6/6] address copilot comments --- lib/llvm/llvm-translator.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/llvm/llvm-translator.js b/lib/llvm/llvm-translator.js index 1fbc3ba..dca4fd9 100644 --- a/lib/llvm/llvm-translator.js +++ b/lib/llvm/llvm-translator.js @@ -26,7 +26,7 @@ export class LLVMTranslator { // Declare runtime functions this.declareRuntimeFunctions(); - // Collect function signatures from call sites + // Collect function signatures by traversing all statements (infer parameter types from usage) this.collectFunctionSignatures(ir.statements); // Create main function @@ -75,7 +75,7 @@ export class LLVMTranslator { this.functionSignatures.set(stmt.callee, signature); } else if (stmt instanceof FunctionDeclaration) { - // Recursively collect signatures from function bodies + // Collect function call signatures from nested function and control-flow bodies this.collectFunctionSignatures(stmt.body); } else if (stmt instanceof IfStatement) { this.collectFunctionSignatures(stmt.consequent); @@ -513,8 +513,9 @@ export class LLVMTranslator { this.translateStatement(bodyStmt); } - // If no return statement, add a default return (though our parser requires returns) - if (!entryBB.getTerminator()) { + // If no return statement in the current insertion block, add a default return + const currentBB = this.builder.GetInsertBlock(); + if (currentBB && !currentBB.getTerminator()) { this.builder.CreateRet(this.builder.getInt32(0)); }