Skip to content

Commit

Permalink
[VOI-95] Support f64 and i64 literals (#34)
Browse files Browse the repository at this point in the history
* Working i64

There is an issue with if cond typecheck, where it says must resolve to a bool, but the actual issue was the fn couldn't be resolved.

* Checking bugfixes

* Fix bugs and use i64 / f32 extensions

Default integers to i32 and floats to f64

* TCO Improvements

* Improved type inference

* Test speedup?
  • Loading branch information
drew-y authored Sep 5, 2024
1 parent 233e093 commit 230633d
Show file tree
Hide file tree
Showing 22 changed files with 212 additions and 52 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
},
"[void]": {
"editor.tabSize": 2
}
},
"vitest.nodeExecutable": ""
}
17 changes: 15 additions & 2 deletions src/__tests__/compiler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { e2eVoidText, gcVoidText, tcoText } from "./fixtures/e2e-file.js";
import {
e2eVoidText,
gcVoidText,
goodTypeInferenceText,
tcoText,
} from "./fixtures/e2e-file.js";
import { compile } from "../compiler.js";
import { describe, expect, test, vi } from "vitest";
import { describe, test, vi } from "vitest";
import assert from "node:assert";
import { getWasmFn, getWasmInstance } from "../lib/wasm.js";
import * as rCallUtil from "../assembler/return-call.js";
Expand All @@ -14,6 +19,14 @@ describe("E2E Compiler Pipeline", () => {
t.expect(fn(), "Main function returns correct value").toEqual(55);
});

test("Compiler has good inference", async (t) => {
const mod = await compile(goodTypeInferenceText);
const instance = getWasmInstance(mod);
const fn = getWasmFn("main", instance);
assert(fn, "Function exists");
t.expect(fn(), "Main function returns correct value").toEqual(55n);
});

test("Compiler can compile gc objects and map correct fns", async (t) => {
const mod = await compile(gcVoidText);
const instance = getWasmInstance(mod);
Expand Down
17 changes: 17 additions & 0 deletions src/__tests__/fixtures/e2e-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,20 @@ pub fn fib(n: i32, a: i32, b: i32) -> i32
else:
fib(n - 1, b, a + b)
`;

export const goodTypeInferenceText = `
use std::all
// Should infer return type from fib_alias
fn fib(n: i32, a: i64, b: i64)
if n == 0 then:
a
else:
fib_alias(n - 1, b, a + b)
fn fib_alias(n: i32, a: i64, b: i64) -> i64
fib(n - 1, b, a + b)
pub fn main() -> i64
fib(10, 0i64, 1i64)
`;
42 changes: 36 additions & 6 deletions src/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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";
import { Float } from "./syntax-objects/float.js";

export const assemble = (ast: Expr) => {
const mod = new binaryen.Module();
Expand All @@ -47,8 +48,8 @@ const compileExpression = (opts: CompileExprOpts): number => {
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.isInt()) return compileInt({ ...opts, expr });
if (expr.isFloat()) return compileFloat({ ...opts, expr });
if (expr.isIdentifier()) return compileIdentifier({ ...opts, expr });
if (expr.isFn()) return compileFunction({ ...opts, expr });
if (expr.isVariable()) return compileVariable({ ...opts, expr });
Expand All @@ -69,6 +70,27 @@ const compileExpression = (opts: CompileExprOpts): number => {
);
};

const compileInt = (opts: CompileExprOpts<Int>) => {
const val = opts.expr.value;
if (typeof val === "number") {
return opts.mod.i32.const(val);
}

const i64Int = val.value;
const low = Number(i64Int & BigInt(0xffffffff)); // Extract lower 32 bits
const high = Number((i64Int >> BigInt(32)) & BigInt(0xffffffff)); // Extract higher 32 bits
return opts.mod.i64.const(low, high);
};

const compileFloat = (opts: CompileExprOpts<Float>) => {
const val = opts.expr.value;
if (typeof val === "number") {
return opts.mod.f32.const(val);
}

return opts.mod.f64.const(val.value);
};

const compileType = (opts: CompileExprOpts<Type>) => {
const type = opts.expr;

Expand Down Expand Up @@ -157,7 +179,7 @@ const compileIdentifier = (opts: CompileExprOpts<Identifier>) => {

const compileCall = (opts: CompileExprOpts<Call>): number => {
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("quote")) return (expr.argAt(0) as { value: number }).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);
if (expr.calls("export")) return compileExport(opts);
Expand All @@ -182,7 +204,7 @@ const compileCall = (opts: CompileExprOpts<Call>): number => {
const id = expr.fn!.id;
const returnType = mapBinaryenType(opts, expr.fn!.returnType!);

if (isReturnExpr && id === expr.parentFn?.id) {
if (isReturnExpr) {
return returnCall(mod, id, args, returnType);
}

Expand Down Expand Up @@ -222,7 +244,11 @@ const compileExport = (opts: CompileExprOpts<Call>) => {
const compileAssign = (opts: CompileExprOpts<Call>): number => {
const { expr, mod } = opts;
const identifier = expr.argAt(0) as Identifier;
const value = compileExpression({ ...opts, expr: expr.argAt(1)! });
const value = compileExpression({
...opts,
expr: expr.argAt(1)!,
isReturnExpr: false,
});
const entity = identifier.resolve();
if (!entity) {
throw new Error(`${identifier} not found in scope`);
Expand Down Expand Up @@ -321,7 +347,11 @@ const compileIf = (opts: CompileExprOpts<Call>) => {
const conditionNode = expr.exprArgAt(0);
const ifTrueNode = expr.labeledArgAt(1);
const ifFalseNode = expr.optionalLabeledArgAt(2);
const condition = compileExpression({ ...opts, expr: conditionNode });
const condition = compileExpression({
...opts,
expr: conditionNode,
isReturnExpr: false,
});
const ifTrue = compileExpression({ ...opts, expr: ifTrueNode });
const ifFalse =
ifFalseNode !== undefined
Expand Down
3 changes: 2 additions & 1 deletion src/parser/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const lexer = (chars: CharStream): Token => {
}

token.location.endIndex = chars.position;
token.location.endColumn = chars.column;
return token;
};

Expand All @@ -67,7 +68,7 @@ const consumeOperator = (chars: CharStream, token: Token) => {

const consumeNumber = (chars: CharStream, token: Token) => {
const isValidNumber = (str: string) =>
/^[+-]?\d*(\.\d+)?[Ee]?[+-]?\d*$/.test(str);
/^[+-]?\d+(?:\.\d+)?([Ee]?[+-]?\d+|(?:i|f)(?:|3|6|32|64))?$/.test(str);
const stillConsumingNumber = () =>
chars.next &&
(isValidNumber(token.value + chars.next) ||
Expand Down
17 changes: 11 additions & 6 deletions src/parser/reader-macros/float.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { Float } from "../../syntax-objects/index.js";
import { ReaderMacro } from "./types.js";

export const floatMacro: ReaderMacro = {
match: (t) => /^[+-]?\d+\.\d+$/.test(t.value),
macro: (_, { token }) =>
new Float({
value: Number(token.value),
location: token.location,
}),
match: (t) => /^[+-]?\d+\.\d+(?:f64|f32)?$/.test(t.value),
macro: (_, { token }) => {
const value =
token.value.at(-3) === "f"
? token.value.endsWith("f64")
? ({ type: "f64", value: Number(token.value.slice(0, -3)) } as const)
: Number(token.value.slice(0, -3))
: ({ type: "f64", value: Number(token.value) } as const); // Default to f64

return new Float({ value, location: token.location });
},
};
20 changes: 14 additions & 6 deletions src/parser/reader-macros/int.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import { Int } from "../../syntax-objects/index.js";
import { ReaderMacro } from "./types.js";

export const intMacro: ReaderMacro = {
match: (t) => /^[+-]?\d+$/.test(t.value),
macro: (_, { token }) =>
new Int({
value: Number(token.value),
location: token.location,
}),
match: (t) => /^[+-]?\d+(?:i64|i32)?$/.test(t.value),
macro: (_, { token }) => {
const value =
token.value.at(-3) === "i"
? token.value.endsWith("i64")
? ({
type: "i64",
value: BigInt(token.value.slice(0, -3)),
} as const)
: Number(token.value.slice(0, -3))
: Number(token.value); // Default to i32

return new Int({ value, location: token.location });
},
};
2 changes: 1 addition & 1 deletion src/parser/syntax-macros/primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const parseBinaryCall = (left: Expr, list: List): List => {
// Dot handling should maybe be moved to a macro?
const result = isDotOp(op)
? parseDot(right, left)
: new List({ value: [op, left, right] });
: new List({ ...op.metadata, value: [op, left, right] });

// Remove "tuple" from the list of parameters of a lambda
// Functional notation macro isn't smart enough to identify lambda parameters
Expand Down
21 changes: 19 additions & 2 deletions src/parser/utils/parse-std.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from "path";
import { parseDirectory } from "./parse-directory.js";
import { ParsedFiles, parseDirectory } from "./parse-directory.js";
import { fileURLToPath } from "url";

export const stdPath = path.resolve(
Expand All @@ -11,4 +11,21 @@ export const stdPath = path.resolve(
"std"
);

export const parseStd = async () => parseDirectory(stdPath);
let cache: ParsedFiles | undefined = undefined;
export const parseStd = async () => {
if (cache) {
return cloneParsedFiles(cache);
}

const parsed = await parseDirectory(stdPath);
cache = cloneParsedFiles(parsed);
return parsed;
};

const cloneParsedFiles = (parsed: ParsedFiles) =>
Object.entries(parsed).reduce(
(acc, [key, value]) => ({ ...acc, [key]: value.clone() }),
{} as ParsedFiles
);

// Convert the object
18 changes: 12 additions & 6 deletions src/semantics/check-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ const checkCallTypes = (call: Call): Call | ObjectLiteral => {
}

if (!call.type) {
throw new Error(`Could not resolve type for call ${call.fnName}`);
throw new Error(
`Could not resolve type for call ${call.fnName} at ${call.location}`
);
}

return call;
Expand Down Expand Up @@ -119,12 +121,16 @@ const checkIdentifier = (id: Identifier) => {
};

export const checkIf = (call: Call) => {
const condType = getExprType(call.argAt(0));
const cond = checkTypes(call.argAt(0));
const condType = getExprType(cond);
if (!condType || !typesAreEquivalent(condType, bool)) {
throw new Error("If conditions must resolve to a boolean");
throw new Error(
`If conditions must resolve to a boolean at ${cond.location}`
);
}
const thenExpr = call.argAt(1);
const elseExpr = call.argAt(2);

const thenExpr = checkTypes(call.argAt(1));
const elseExpr = call.argAt(2) ? checkTypes(call.argAt(2)) : undefined;

// Until unions are supported, return void if no else
if (!elseExpr) {
Expand Down Expand Up @@ -335,7 +341,7 @@ const checkMatch = (match: Match) => {

if (!mCase.matchType.extends(match.baseType)) {
throw new Error(
`Match case type ${mCase.matchType.name} does not extend ${match.baseType.name}`
`Match case type ${mCase.matchType.name} does not extend ${match.baseType.name} at ${mCase.expr.location}`
);
}

Expand Down
3 changes: 0 additions & 3 deletions src/semantics/init-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ import {
TypeAlias,
ObjectType,
ObjectLiteral,
Identifier,
} from "../syntax-objects/index.js";
import { Match, MatchCase } from "../syntax-objects/match.js";
import { getExprType } from "./resolution/get-expr-type.js";
import { resolveTypes } from "./resolution/resolve-types.js";
import { SemanticProcessor } from "./types.js";

export const initEntities: SemanticProcessor = (expr) => {
Expand Down
2 changes: 2 additions & 0 deletions src/semantics/resolution/get-call-fn.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Call, Expr, Fn } from "../../syntax-objects/index.js";
import { getExprType } from "./get-expr-type.js";
import { typesAreEquivalent } from "./types-are-equivalent.js";
import { resolveFnTypes } from "./resolve-fn-type.js";

export const getCallFn = (call: Call): Fn | undefined => {
if (isPrimitiveFnCall(call)) return undefined;

const candidates = call.resolveFns(call.fnName).filter((candidate) => {
resolveFnTypes(candidate);
const params = candidate.parameters;
return params.every((p, index) => {
const arg = call.argAt(index);
Expand Down
6 changes: 3 additions & 3 deletions src/semantics/resolution/get-expr-type.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Expr } from "../../syntax-objects/expr.js";
import { Call, Identifier } from "../../syntax-objects/index.js";
import { Type, i32, f32, bool } from "../../syntax-objects/types.js";
import { Type, i32, f32, bool, i64, f64 } from "../../syntax-objects/types.js";
import { resolveCallTypes } from "./resolve-call-types.js";

export const getExprType = (expr?: Expr): Type | undefined => {
if (!expr) return;
if (expr.isInt()) return i32;
if (expr.isFloat()) return f32;
if (expr.isInt()) return typeof expr.value === "number" ? i32 : i64;
if (expr.isFloat()) return typeof expr.value === "number" ? f32 : f64;
if (expr.isBool()) return bool;
if (expr.isIdentifier()) return getIdentifierType(expr);
if (expr.isCall()) {
Expand Down
7 changes: 6 additions & 1 deletion src/semantics/resolution/resolve-fn-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getExprType } from "./get-expr-type.js";
import { resolveTypes } from "./resolve-types.js";

export const resolveFnTypes = (fn: Fn): Fn => {
if (fn.returnType) {
if (fn.resolved) {
// Already resolved
return fn;
}
Expand All @@ -15,6 +15,7 @@ export const resolveFnTypes = (fn: Fn): Fn => {
fn.returnType = fn.annotatedReturnType;
}

fn.resolved = true;
fn.body = resolveTypes(fn.body);
fn.inferredReturnType = getExprType(fn.body);
fn.returnType = fn.annotatedReturnType ?? fn.inferredReturnType;
Expand All @@ -24,6 +25,10 @@ export const resolveFnTypes = (fn: Fn): Fn => {

const resolveParameters = (params: Parameter[]) => {
params.forEach((p) => {
if (p.type) {
return;
}

if (!p.typeExpr) {
throw new Error(`Unable to determine type for ${p}`);
}
Expand Down
Loading

0 comments on commit 230633d

Please sign in to comment.