From c39c6406a1258350306c92f9a8d80396b6c6760c Mon Sep 17 00:00:00 2001 From: Drew Youngwerth Date: Tue, 3 Sep 2024 23:25:27 -0700 Subject: [PATCH 1/4] Support TCO --- src/assembler.ts | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/assembler.ts b/src/assembler.ts index 2cc57630..481c92c9 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -36,17 +36,21 @@ interface CompileExprOpts { expr: T; mod: binaryen.Module; extensionHelpers: ReturnType; + isReturnExpr?: boolean; } const compileExpression = (opts: CompileExprOpts): number => { - const { expr, mod } = opts; - if (expr.isCall()) return compileCall({ ...opts, expr }); + const { expr, mod, isReturnExpr } = opts; + opts.isReturnExpr = false; + // These can take isReturnExpr + if (expr.isCall()) return compileCall({ ...opts, expr, isReturnExpr }); + if (expr.isBlock()) return compileBlock({ ...opts, expr, isReturnExpr }); + if (expr.isMatch()) return compileMatch({ ...opts, expr, isReturnExpr }); if (expr.isInt()) return mod.i32.const(expr.value); if (expr.isFloat()) return mod.f32.const(expr.value); if (expr.isIdentifier()) return compileIdentifier({ ...opts, expr }); if (expr.isFn()) return compileFunction({ ...opts, expr }); if (expr.isVariable()) return compileVariable({ ...opts, expr }); - if (expr.isBlock()) return compileBlock({ ...opts, expr }); if (expr.isDeclaration()) return compileDeclaration({ ...opts, expr }); if (expr.isModule()) return compileModule({ ...opts, expr }); if (expr.isObjectLiteral()) return compileObjectLiteral({ ...opts, expr }); @@ -54,7 +58,6 @@ const compileExpression = (opts: CompileExprOpts): number => { if (expr.isUse()) return mod.nop(); if (expr.isMacro()) return mod.nop(); if (expr.isMacroVariable()) return mod.nop(); - if (expr.isMatch()) return compileMatch({ ...opts, expr }); if (expr.isBool()) { return expr.value ? mod.i32.const(1) : mod.i32.const(0); @@ -86,7 +89,13 @@ const compileModule = (opts: CompileExprOpts) => { const compileBlock = (opts: CompileExprOpts) => { return opts.mod.block( null, - opts.expr.body.toArray().map((expr) => compileExpression({ ...opts, expr })) + opts.expr.body.toArray().map((expr, index, array) => { + if (index === array.length - 1) { + return compileExpression({ ...opts, expr, isReturnExpr: true }); + } + + return compileExpression({ ...opts, expr, isReturnExpr: false }); + }) ); }; @@ -146,7 +155,7 @@ const compileIdentifier = (opts: CompileExprOpts) => { }; const compileCall = (opts: CompileExprOpts): number => { - const { expr, mod } = opts; + const { expr, mod, isReturnExpr } = opts; if (expr.calls("quote")) return (expr.argAt(0) as Int).value; // TODO: This is an ugly hack to get constants that the compiler needs to know at compile time for ex bnr calls; if (expr.calls("=")) return compileAssign(opts); if (expr.calls("if")) return compileIf(opts); @@ -167,13 +176,16 @@ const compileCall = (opts: CompileExprOpts): number => { const args = expr.args .toArray() - .map((expr) => compileExpression({ ...opts, expr })); + .map((expr) => compileExpression({ ...opts, expr, isReturnExpr: false })); - return mod.call( - expr.fn!.id, - args, - mapBinaryenType(opts, expr.fn!.returnType!) - ); + const id = expr.fn!.id; + const returnType = mapBinaryenType(opts, expr.fn!.returnType!); + + if (isReturnExpr && id === expr.parentFn?.id) { + return mod.return_call(id, args, returnType); + } + + return mod.call(id, args, returnType); }; const compileObjectInit = (opts: CompileExprOpts) => { @@ -250,7 +262,13 @@ const compileFunction = (opts: CompileExprOpts): number => { const { expr: fn, mod } = opts; const parameterTypes = getFunctionParameterTypes(opts, fn); const returnType = mapBinaryenType(opts, fn.getReturnType()); - const body = compileExpression({ ...opts, expr: fn.body! }); + + const body = compileExpression({ + ...opts, + expr: fn.body!, + isReturnExpr: true, + }); + const variableTypes = getFunctionVarTypes(opts, fn); // TODO: Vars should probably be registered with the function type rather than body (for consistency). mod.addFunction(fn.id, parameterTypes, returnType, variableTypes, body); From bccccacaa2dcee47fcf1c2a401c42a3761a8ddbe Mon Sep 17 00:00:00 2001 From: Drew Youngwerth Date: Tue, 3 Sep 2024 23:32:14 -0700 Subject: [PATCH 2/4] Add test --- .../__snapshots__/compiler.test.ts.snap | 32 +++++++++++++++++++ src/__tests__/compiler.test.ts | 10 ++++-- src/__tests__/fixtures/e2e-file.ts | 11 +++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/__snapshots__/compiler.test.ts.snap diff --git a/src/__tests__/__snapshots__/compiler.test.ts.snap b/src/__tests__/__snapshots__/compiler.test.ts.snap new file mode 100644 index 00000000..824ecb42 --- /dev/null +++ b/src/__tests__/__snapshots__/compiler.test.ts.snap @@ -0,0 +1,32 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`E2E Compiler Pipeline > Compiler can do tco 1`] = ` +"(module + (type $0 (func (param i32 i32 i32) (result i32))) + (memory $0 1 150) + (export "buffer" (memory $0)) + (export "fib" (func $fib#15065)) + (func $fib#15065 (type $0) (param $0 i32) (param $1 i32) (param $2 i32) (result i32) + (if (result i32) + (local.get $0) + (then + (return_call $fib#15065 + (i32.sub + (local.get $0) + (i32.const 1) + ) + (local.get $2) + (i32.add + (local.get $1) + (local.get $2) + ) + ) + ) + (else + (local.get $1) + ) + ) + ) +) +" +`; diff --git a/src/__tests__/compiler.test.ts b/src/__tests__/compiler.test.ts index dd600ec3..33b27106 100644 --- a/src/__tests__/compiler.test.ts +++ b/src/__tests__/compiler.test.ts @@ -1,6 +1,6 @@ -import { e2eVoidText, gcVoidText } from "./fixtures/e2e-file.js"; +import { e2eVoidText, gcVoidText, tcoText } from "./fixtures/e2e-file.js"; import { compile } from "../compiler.js"; -import { describe, test } from "vitest"; +import { describe, expect, test } from "vitest"; import assert from "node:assert"; import { getWasmFn, getWasmInstance } from "../lib/wasm.js"; @@ -35,4 +35,10 @@ describe("E2E Compiler Pipeline", () => { t.expect(test5(), "test 5 returns correct value").toEqual(21); t.expect(test6(), "test 6 returns correct value").toEqual(-1); }); + + test("Compiler can do tco", async (t) => { + const mod = await compile(tcoText); + mod.optimize(); + t.expect(mod.emitText()).toMatchSnapshot(); + }); }); diff --git a/src/__tests__/fixtures/e2e-file.ts b/src/__tests__/fixtures/e2e-file.ts index 2fc0a517..d8ee53a0 100644 --- a/src/__tests__/fixtures/e2e-file.ts +++ b/src/__tests__/fixtures/e2e-file.ts @@ -86,3 +86,14 @@ pub fn test6() let vec = Bitly { x: 52, y: 2, z: 21 } get_num_from_vec_sub_obj(vec) `; + +export const tcoText = ` +use std::all + +// Tail call fib +pub fn fib(n: i32, a: i32, b: i32) -> i32 + if n == 0 then: + a + else: + fib(n - 1, b, a + b) +`; From 4bffc8d88632d9a62e60c34cbb4527420e3cb169 Mon Sep 17 00:00:00 2001 From: Drew Youngwerth Date: Tue, 3 Sep 2024 23:37:40 -0700 Subject: [PATCH 3/4] Set test workflow timeout --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 037620df..789cd742 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,7 +5,7 @@ on: [pull_request] jobs: test: runs-on: ubuntu-latest - + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - name: Use Node.js From fac1431634b759f26ced54ccbed1995663768436 Mon Sep 17 00:00:00 2001 From: Drew Youngwerth Date: Tue, 3 Sep 2024 23:54:16 -0700 Subject: [PATCH 4/4] Less fragile test --- .../__snapshots__/compiler.test.ts.snap | 32 ------------------- src/__tests__/compiler.test.ts | 9 +++--- src/assembler.ts | 3 +- src/assembler/return-call.ts | 11 +++++++ 4 files changed, 18 insertions(+), 37 deletions(-) delete mode 100644 src/__tests__/__snapshots__/compiler.test.ts.snap create mode 100644 src/assembler/return-call.ts diff --git a/src/__tests__/__snapshots__/compiler.test.ts.snap b/src/__tests__/__snapshots__/compiler.test.ts.snap deleted file mode 100644 index 824ecb42..00000000 --- a/src/__tests__/__snapshots__/compiler.test.ts.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`E2E Compiler Pipeline > Compiler can do tco 1`] = ` -"(module - (type $0 (func (param i32 i32 i32) (result i32))) - (memory $0 1 150) - (export "buffer" (memory $0)) - (export "fib" (func $fib#15065)) - (func $fib#15065 (type $0) (param $0 i32) (param $1 i32) (param $2 i32) (result i32) - (if (result i32) - (local.get $0) - (then - (return_call $fib#15065 - (i32.sub - (local.get $0) - (i32.const 1) - ) - (local.get $2) - (i32.add - (local.get $1) - (local.get $2) - ) - ) - ) - (else - (local.get $1) - ) - ) - ) -) -" -`; diff --git a/src/__tests__/compiler.test.ts b/src/__tests__/compiler.test.ts index 33b27106..9cd0dbfd 100644 --- a/src/__tests__/compiler.test.ts +++ b/src/__tests__/compiler.test.ts @@ -1,8 +1,9 @@ import { e2eVoidText, gcVoidText, tcoText } from "./fixtures/e2e-file.js"; import { compile } from "../compiler.js"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import assert from "node:assert"; import { getWasmFn, getWasmInstance } from "../lib/wasm.js"; +import * as rCallUtil from "../assembler/return-call.js"; describe("E2E Compiler Pipeline", () => { test("Compiler can compile and run a basic void program", async (t) => { @@ -37,8 +38,8 @@ describe("E2E Compiler Pipeline", () => { }); test("Compiler can do tco", async (t) => { - const mod = await compile(tcoText); - mod.optimize(); - t.expect(mod.emitText()).toMatchSnapshot(); + const spy = vi.spyOn(rCallUtil, "returnCall"); + await compile(tcoText); + t.expect(spy).toHaveBeenCalledTimes(1); }); }); diff --git a/src/assembler.ts b/src/assembler.ts index 481c92c9..91b530c2 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -21,6 +21,7 @@ import { HeapTypeRef } from "./lib/binaryen-gc/types.js"; import { getExprType } from "./semantics/resolution/get-expr-type.js"; import { Match, MatchCase } from "./syntax-objects/match.js"; import { initExtensionHelpers } from "./assembler/extension-helpers.js"; +import { returnCall } from "./assembler/return-call.js"; export const assemble = (ast: Expr) => { const mod = new binaryen.Module(); @@ -182,7 +183,7 @@ const compileCall = (opts: CompileExprOpts): number => { const returnType = mapBinaryenType(opts, expr.fn!.returnType!); if (isReturnExpr && id === expr.parentFn?.id) { - return mod.return_call(id, args, returnType); + return returnCall(mod, id, args, returnType); } return mod.call(id, args, returnType); diff --git a/src/assembler/return-call.ts b/src/assembler/return-call.ts new file mode 100644 index 00000000..820402a7 --- /dev/null +++ b/src/assembler/return-call.ts @@ -0,0 +1,11 @@ +import binaryen from "binaryen"; +import { ExpressionRef, TypeRef } from "../lib/binaryen-gc/types.js"; + +export const returnCall = ( + mod: binaryen.Module, + fnId: string, + args: ExpressionRef[], + returnType: TypeRef +) => { + return mod.return_call(fnId, args, returnType); +};