Skip to content
Permalink
Browse files

Add tail call elimination and basic syscalls (#3)

* TCO and syswrite

* Add comment support and sys_exit

* Latest

* Tail calls eliminated
  • Loading branch information...
eatonphil committed May 15, 2019
1 parent 9b56dba commit 213b83b8e952c210ba408bf38e59ae677d19e643
Showing with 216 additions and 76 deletions.
  1. +8 −0 lib/kernel.lisp
  2. +124 −61 src/backend/llvm.js
  3. +17 −0 src/backend/utility/Context.js
  4. +5 −2 src/backend/utility/Scope.js
  5. +4 −0 src/parser.js
  6. +21 −12 src/ulisp.js
  7. +7 −0 tests/digits.lisp
  8. +1 −1 tests/fib.lisp
  9. +10 −0 tests/print.lisp
  10. +9 −0 tests/sys-write.lisp
  11. +10 −0 tests/tail-fib.lisp
@@ -0,0 +1,8 @@
(def print-char (c)
(syscall/sys_write 1 &c 1))

(def print (n)
(if (> n 9)
(print (/ n 10)))

(print-char (+ 48 (% n 10))))
@@ -1,7 +1,18 @@
const cp = require('child_process');
const fs = require('fs');

const { Scope } = require('./utility/Scope');
const { Context } = require('./utility/Context');

const SYSCALL_TABLE = {
darwin: {
sys_write: 0x2000004,
sys_exit: 0x2000001,
},
linux: {
sys_write: 1,
sys_exit: 60,
},
}[process.platform];

class Compiler {
constructor() {
@@ -13,8 +24,13 @@ class Compiler {
'+': this.compileOp('add'),
'-': this.compileOp('sub'),
'*': this.compileOp('mul'),
'/': this.compileOp('udiv'),
'%': this.compileOp('urem'),
'<': this.compileOp('icmp slt'),
'>': this.compileOp('icmp sgt'),
'=': this.compileOp('icmp eq'),
'syscall/sys_write': this.compileSyscall(SYSCALL_TABLE.sys_write),
'syscall/sys_exit': this.compileSyscall(SYSCALL_TABLE.sys_exit),
};
}

@@ -23,31 +39,56 @@ class Compiler {
this.outBuffer.push(indent + args);
}

compileSyscall(id) {
return (args, destination, context) => {
const argTmps = args.map((arg) => {
const tmp = context.scope.symbol();
this.compileExpression(arg, tmp, context);
return tmp.type + ' %' + tmp.value;
}).join(', ');
const regs = ['rdi', 'rsi', 'rdx', 'r10', 'r8', 'r9'];
const params = args.map((arg, i) => `{${regs[i]}}`).join(',');
const idTmp = context.scope.symbol().value;
this.emit(1, `%${idTmp} = add i64 ${id}, 0`)
this.emit(1, `%${destination.value} = call ${destination.type} asm sideeffect "syscall", "=r,{rax},${params},~{dirflag},~{fpsr},~{flags}" (i64 %${idTmp}, ${argTmps})`);
}
}

compileOp(op) {
return ([a, b], destination, scope) => {
const arg1 = scope.symbol();
const arg2 = scope.symbol();
this.compileExpression(a, arg1, scope);
this.compileExpression(b, arg2, scope);
this.emit(1, `%${destination} = ${op} i32 %${arg1}, %${arg2}`);
return ([a, b], destination, context) => {
const arg1 = context.scope.symbol();
const arg2 = context.scope.symbol();
this.compileExpression(a, arg1, context);
this.compileExpression(b, arg2, context);
this.emit(1, `%${destination.value} = ${op} ${arg1.type} %${arg1.value}, %${arg2.value}`);
};
}

compileExpression(exp, destination, scope) {
compileExpression(exp, destination, context) {
// Is a nested function call, compile it
if (Array.isArray(exp)) {
this.compileCall(exp[0], exp.slice(1), destination, scope);
this.compileCall(exp[0], exp.slice(1), destination, context);
return;
}

const res = scope.get(exp);
if (Number.isInteger(exp)) {
this.emit(1, `%${destination} = add i32 ${exp}, 0`);
if (Number.isInteger(+exp)) {
this.emit(1, `%${destination.value} = add i64 ${exp}, 0`);
return;
}

if (exp.startsWith('&')) {
const symbol = exp.substring(1);
const tmp = context.scope.symbol();
this.compileExpression(symbol, tmp, context);
this.emit(1, `%${destination.value} = alloca ${tmp.type}, align 4`);
destination.type = tmp.type + '*';
this.emit(1, `store ${tmp.type} %${tmp.value}, ${destination.type} %${destination.value}, align 4`);
return;
}

const res = context.scope.get(exp);
if (res) {
this.emit(1, `%${destination} = add i32 %${res}, 0`);
this.emit(1, `%${destination.value} = add ${res.type} %${res.value}, 0`);
} else {
throw new Error(
'Attempt to reference undefined variable or unsupported literal: ' +
@@ -56,91 +97,111 @@ class Compiler {
}
}

compileBegin(body, destination, scope) {
body.forEach((expression, i) =>
compileBegin(body, destination, context) {
body.forEach((expression, i) => {
const isLast = body.length - 1 === i;

// Clone just to reset tailCallTree
const contextClone = context.copy();
contextClone.scope = context.scope;
if (!isLast) {
contextClone.tailCallTree = [];
}

this.compileExpression(
expression,
i === body.length - 1 ? destination : scope.symbol(),
scope,
),
);
isLast ? destination : context.scope.symbol(),
contextClone,
);
});
}

compileIf([test, thenBlock, elseBlock], destination, scope) {
const testVariable = scope.symbol();
const result = scope.symbol('ifresult');
compileIf([test, thenBlock, elseBlock], destination, context) {
const testVariable = context.scope.symbol();
const result = context.scope.symbol('ifresult');
// Space for result
this.emit(1, `%${result} = alloca i32, align 4`);
result.type = 'i64*';
this.emit(1, `%${result.value} = alloca i64, align 4`);

// Compile expression and branch
this.compileExpression(test, testVariable, scope);
const trueLabel = scope.symbol('iftrue');
const falseLabel = scope.symbol('iffalse');
this.emit(1, `br i1 %${testVariable}, label %${trueLabel}, label %${falseLabel}`);
this.compileExpression(test, testVariable, context);
const trueLabel = context.scope.symbol('iftrue').value;
const falseLabel = context.scope.symbol('iffalse').value;
this.emit(1, `br i1 %${testVariable.value}, label %${trueLabel}, label %${falseLabel}`);

// Compile true section
this.emit(0, trueLabel + ':');
const tmp1 = scope.symbol();
this.compileExpression(thenBlock, tmp1, scope);
this.emit(1, `store i32 %${tmp1}, i32* %${result}, align 4`);
const endLabel = scope.symbol('ifend');
const tmp1 = context.scope.symbol();
this.compileExpression(thenBlock, tmp1, context);
this.emit(1, `store ${tmp1.type} %${tmp1.value}, ${result.type} %${result.value}, align 4`);
const endLabel = context.scope.symbol('ifend').value;
this.emit(1, 'br label %' + endLabel);
this.emit(0, falseLabel + ':');

// Compile false section
const tmp2 = scope.symbol();
this.compileExpression(elseBlock, tmp2, scope);
this.emit(1, `store i32 %${tmp2}, i32* %${result}, align 4`);
// Compile optional false section
this.emit(0, falseLabel + ':');
if (elseBlock) {
const tmp2 = context.scope.symbol();
this.compileExpression(elseBlock, tmp2, context);
this.emit(1, `store ${tmp2.type} %${tmp2.value}, ${result.type} %${result.value}, align 4`);
}
this.emit(1, 'br label %' + endLabel);

// Compile cleanup
this.emit(0, endLabel + ':');
this.emit(1, `%${destination} = load i32, i32* %${result}, align 4`);
this.emit(1, `%${destination.value} = load ${destination.type}, ${result.type} %${result.value}, align 4`);
}

compileDefine([name, params, ...body], destination, scope) {
// Add this function to outer scope
const safeName = scope.register(name);
compileDefine([name, params, ...body], destination, context) {
// Add this function to outer context.scope
const fn = context.scope.register(name);

// Copy outer scope so parameter mappings aren't exposed in outer scope.
const childScope = scope.copy();
// Copy outer context.scope so parameter mappings aren't exposed in outer context.scope.
const childContext = context.copy();
childContext.tailCallTree.push(fn.value);

const safeParams = params.map((param) =>
// Store parameter mapped to associated local
childScope.register(param),
childContext.scope.register(param),
);

this.emit(
0,
`define i32 @${safeName}(${safeParams
.map((p) => `i32 %${p}`)
`define i64 @${fn.value}(${safeParams
.map((p) => `${p.type} %${p.value}`)
.join(', ')}) {`,
);

// Pass childScope in for reference when body is compiled.
const ret = childScope.symbol();
this.compileExpression(body[0], ret, childScope);
// Pass childContext in for reference when body is compiled.
const ret = childContext.scope.symbol();
this.compileBegin(body, ret, childContext);

this.emit(1, `ret i32 %${ret}`);
this.emit(1, `ret ${ret.type} %${ret.value}`);
this.emit(0, '}\n');
}

compileCall(fun, args, destination, scope) {
compileCall(fun, args, destination, context) {
if (this.primitiveFunctions[fun]) {
this.primitiveFunctions[fun](args, destination, scope);
this.primitiveFunctions[fun](args, destination, context);
return;
}

const validFunction = scope.get(fun);
const validFunction = context.scope.get(fun);
if (validFunction) {
const safeArgs = args
.map((a) => {
const res = scope.symbol();
this.compileExpression(a, res, scope);
return 'i32 %' + res;
const res = context.scope.symbol();
this.compileExpression(a, res, context);
return res.type + ' %' + res.value;
})
.join(', ');
this.emit(1, `%${destination} = call i32 @${validFunction}(${safeArgs})`);
.join(', ');

const isTailCall = module.exports.TAIL_CALL_ENABLED &&
context.tailCallTree.includes(validFunction.value);
const maybeTail = isTailCall ? 'tail ' : '';
this.emit(1, `%${destination.value} = ${maybeTail}call ${validFunction.type} @${validFunction.value}(${safeArgs})`);
if (isTailCall) {
this.emit(1, `ret ${destination.type} %${destination.value}`);
}
} else {
throw new Error('Attempt to call undefined function: ' + fun);
}
@@ -153,14 +214,16 @@ class Compiler {

module.exports.compile = function(ast) {
const c = new Compiler();
const scope = new Scope();
c.compileCall('begin', ast, scope.symbol(), scope);
const context = new Context();
c.compileCall('begin', ast, context.scope.symbol(), context);
return c.getOutput();
};

module.exports.build = function(buildDir, program) {
const prog = 'prog';
fs.writeFileSync(buildDir + `/${prog}.ll`, program);
cp.execSync(`llc -o ${buildDir}/${prog}.s ${buildDir}/${prog}.ll`);
cp.execSync(`gcc -o ${buildDir}/${prog} ${buildDir}/${prog}.s`);
cp.execSync(`llc --x86-asm-syntax=intel -o ${buildDir}/${prog}.s ${buildDir}/${prog}.ll`);
cp.execSync(`gcc -o ${buildDir}/${prog} -masm=intel ${buildDir}/${prog}.s`);
};

module.exports.TAIL_CALL_ENABLED = true;
@@ -0,0 +1,17 @@
const { Scope } = require('./Scope');

class Context {
constructor() {
this.scope = new Scope();
this.tailCallTree = [];
}

copy() {
const c = new Context();
c.tailCallTree = [...this.tailCallTree];
c.scope = this.scope.copy();
return c;
}
}

module.exports.Context = Context;
@@ -19,8 +19,11 @@ class Scope {
copy = local + n++;
}

this.locals[local] = copy;
return copy;
this.locals[local] = {
value: copy,
type: 'i64',
};
return this.locals[local];
}

copy() {
@@ -18,6 +18,10 @@ module.exports.parse = function parse(program = '') {
}

return [tokens, program.substring(i + 1)];
} else if (char === ';') {
while (program.charAt(i) !== '\n') {
i++;
}
} else if (WHITESPACE.includes(char)) {
if (currentToken.length) {
tokens.push(+currentToken || currentToken);
@@ -5,19 +5,28 @@ const { parse } = require('./parser');
const backends = require('./backend');

function main(args) {
const input = fs.readFileSync(args[2]).toString();
const kernel = fs.readFileSync(__dirname + '/../lib/kernel.lisp').toString();
const input = kernel + '\n' + fs.readFileSync(args[2]).toString();

let backend;
switch (args[3]) {
case 'llvm':
case undefined:
backend = backends.llvm;
break;
case 'x86':
backend = backends.x86;
break;
default:
console.log('Unsupported backend ' + args[3]);
let backend = backends.llvm;

const restArgs = args.slice(2);
for (let i = 0; i < restArgs.length; i++) {
switch (restArgs[i]) {
case '--backend':
case '-b':
backend = backends[args[i + 1]];
if (!backend) {
console.log('Unsupported backend ' + args[i+1]);
process.exit(1);
}
i++;
break;
case '--no-tail-call':
case '-n':
backend.TAIL_CALL_ENABLED = false;
break;
}
}

const [ast] = parse(input);
@@ -0,0 +1,7 @@
(def digits (n c)
(if (< n 10)
(+ c 1)
(digits (/ n 10) (+ c 1))))

(def main ()
(digits 10234 0))
@@ -4,4 +4,4 @@
(+ (fib (- n 1)) (fib (- n 2)))))

(def main ()
(fib 8))
(print (fib 45)))
@@ -0,0 +1,10 @@
(def print-char1 (c)
(syscall/sys_write 1 &c 1))

(def print1 (n)
(if (> n 9)
(print1 (/ n 10)))
(print-char1 (+ 48 (% n 10))))

(def main ()
(print1 123))

0 comments on commit 213b83b

Please sign in to comment.
You can’t perform that action at this time.