Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VOI-95] Support f64 and i64 literals #34

Merged
merged 6 commits into from
Sep 5, 2024
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
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