diff --git a/.vscode/settings.json b/.vscode/settings.json index bd1fc383..8068d23c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -49,5 +49,6 @@ }, "[void]": { "editor.tabSize": 2 - } + }, + "vitest.nodeExecutable": "" } diff --git a/src/__tests__/compiler.test.ts b/src/__tests__/compiler.test.ts index 9cd0dbfd..01647d86 100644 --- a/src/__tests__/compiler.test.ts +++ b/src/__tests__/compiler.test.ts @@ -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"; @@ -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); diff --git a/src/__tests__/fixtures/e2e-file.ts b/src/__tests__/fixtures/e2e-file.ts index d8ee53a0..619b511c 100644 --- a/src/__tests__/fixtures/e2e-file.ts +++ b/src/__tests__/fixtures/e2e-file.ts @@ -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) +`; diff --git a/src/assembler.ts b/src/assembler.ts index 91b530c2..98caa6b4 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -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(); @@ -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 }); @@ -69,6 +70,27 @@ const compileExpression = (opts: CompileExprOpts): number => { ); }; +const compileInt = (opts: CompileExprOpts) => { + 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) => { + 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) => { const type = opts.expr; @@ -157,7 +179,7 @@ const compileIdentifier = (opts: CompileExprOpts) => { const compileCall = (opts: CompileExprOpts): 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); @@ -182,7 +204,7 @@ const compileCall = (opts: CompileExprOpts): 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); } @@ -222,7 +244,11 @@ const compileExport = (opts: CompileExprOpts) => { const compileAssign = (opts: CompileExprOpts): 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`); @@ -321,7 +347,11 @@ const compileIf = (opts: CompileExprOpts) => { 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 diff --git a/src/parser/lexer.ts b/src/parser/lexer.ts index 861a9da1..388a3682 100644 --- a/src/parser/lexer.ts +++ b/src/parser/lexer.ts @@ -56,6 +56,7 @@ export const lexer = (chars: CharStream): Token => { } token.location.endIndex = chars.position; + token.location.endColumn = chars.column; return token; }; @@ -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) || diff --git a/src/parser/reader-macros/float.ts b/src/parser/reader-macros/float.ts index 96fa3966..33e2be06 100644 --- a/src/parser/reader-macros/float.ts +++ b/src/parser/reader-macros/float.ts @@ -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 }); + }, }; diff --git a/src/parser/reader-macros/int.ts b/src/parser/reader-macros/int.ts index c2dc410b..7f99fab3 100644 --- a/src/parser/reader-macros/int.ts +++ b/src/parser/reader-macros/int.ts @@ -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 }); + }, }; diff --git a/src/parser/syntax-macros/primary.ts b/src/parser/syntax-macros/primary.ts index db5c9add..d878c39d 100644 --- a/src/parser/syntax-macros/primary.ts +++ b/src/parser/syntax-macros/primary.ts @@ -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 diff --git a/src/parser/utils/parse-std.ts b/src/parser/utils/parse-std.ts index c0ace0c1..92e31c15 100644 --- a/src/parser/utils/parse-std.ts +++ b/src/parser/utils/parse-std.ts @@ -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( @@ -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 diff --git a/src/semantics/check-types.ts b/src/semantics/check-types.ts index ec4c5db0..c1f2df4d 100644 --- a/src/semantics/check-types.ts +++ b/src/semantics/check-types.ts @@ -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; @@ -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) { @@ -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}` ); } diff --git a/src/semantics/init-entities.ts b/src/semantics/init-entities.ts index 3d9079ac..150f408c 100644 --- a/src/semantics/init-entities.ts +++ b/src/semantics/init-entities.ts @@ -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) => { diff --git a/src/semantics/resolution/get-call-fn.ts b/src/semantics/resolution/get-call-fn.ts index 41e3702d..32d79082 100644 --- a/src/semantics/resolution/get-call-fn.ts +++ b/src/semantics/resolution/get-call-fn.ts @@ -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); diff --git a/src/semantics/resolution/get-expr-type.ts b/src/semantics/resolution/get-expr-type.ts index dff7db52..05a1a821 100644 --- a/src/semantics/resolution/get-expr-type.ts +++ b/src/semantics/resolution/get-expr-type.ts @@ -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()) { diff --git a/src/semantics/resolution/resolve-fn-type.ts b/src/semantics/resolution/resolve-fn-type.ts index 6a52381a..136d157c 100644 --- a/src/semantics/resolution/resolve-fn-type.ts +++ b/src/semantics/resolution/resolve-fn-type.ts @@ -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; } @@ -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; @@ -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}`); } diff --git a/src/syntax-objects/call.ts b/src/syntax-objects/call.ts index 9e367fd5..49a095d4 100644 --- a/src/syntax-objects/call.ts +++ b/src/syntax-objects/call.ts @@ -13,7 +13,7 @@ export class Call extends ScopedSyntax { fn?: Fn | ObjectType; fnName: Identifier; args: List; - type?: Type; + _type?: Type; lexicon: LexicalContext; constructor( @@ -29,11 +29,27 @@ export class Call extends ScopedSyntax { this.fnName = opts.fnName; this.fn = opts.fn; this.args = opts.args; - this.type = opts.type; + this._type = opts.type; this.lexicon = opts.lexicon ?? new LexicalContext(); opts.args.parent = this; } + set type(type: Type | undefined) { + this._type = type; + } + + get type() { + if (!this._type && this.fn?.isFn()) { + this._type = this.fn.returnType; + } + + if (!this._type && this.fn?.isObjectType()) { + this._type = this.fn; + } + + return this._type; + } + eachArg(fn: (expr: Expr) => void) { this.args.each(fn); return this; diff --git a/src/syntax-objects/float.ts b/src/syntax-objects/float.ts index 10ae1583..fea75f0b 100644 --- a/src/syntax-objects/float.ts +++ b/src/syntax-objects/float.ts @@ -1,17 +1,26 @@ import { Expr } from "./expr.js"; import { Syntax, SyntaxMetadata } from "./syntax.js"; +export type FloatOpts = SyntaxMetadata & { + value: FloatValue; +}; + +export type FloatValue = number | { type: "f64"; value: number }; + export class Float extends Syntax { readonly syntaxType = "float"; - value: number; + value: FloatValue; - constructor(opts: SyntaxMetadata & { value: number }) { + constructor(opts: FloatOpts) { super(opts); this.value = opts.value; } clone(parent?: Expr): Float { - return new Float({ ...super.getCloneOpts(parent), value: this.value }); + return new Float({ + ...super.getCloneOpts(parent), + value: this.value, + }); } toJSON() { diff --git a/src/syntax-objects/fn.ts b/src/syntax-objects/fn.ts index ee48bd62..1f30f8bf 100644 --- a/src/syntax-objects/fn.ts +++ b/src/syntax-objects/fn.ts @@ -12,6 +12,7 @@ export class Fn extends ScopedNamedEntity { returnTypeExpr?: Expr; inferredReturnType?: Type; annotatedReturnType?: Type; + resolved?: boolean; private _body?: Expr; constructor( diff --git a/src/syntax-objects/int.ts b/src/syntax-objects/int.ts index 3565940a..f6b2a2be 100644 --- a/src/syntax-objects/int.ts +++ b/src/syntax-objects/int.ts @@ -1,20 +1,33 @@ import { Expr } from "./expr.js"; import { Syntax, SyntaxMetadata } from "./syntax.js"; +export type IntOpts = SyntaxMetadata & { + value: IntValue; +}; + +export type IntValue = number | { type: "i64"; value: bigint }; + export class Int extends Syntax { readonly syntaxType = "int"; - value: number; + value: IntValue; - constructor(opts: SyntaxMetadata & { value: number }) { + constructor(opts: IntOpts) { super(opts); this.value = opts.value; } clone(parent?: Expr): Int { - return new Int({ ...super.getCloneOpts(parent), value: this.value }); + return new Int({ + ...super.getCloneOpts(parent), + value: this.value, + }); } toJSON() { - return this.value; + if (typeof this.value === "number") { + return this.value; + } + + return this.value.value.toString() + "i64"; } } diff --git a/src/syntax-objects/syntax.ts b/src/syntax-objects/syntax.ts index 0f719867..436ef531 100644 --- a/src/syntax-objects/syntax.ts +++ b/src/syntax-objects/syntax.ts @@ -249,6 +249,8 @@ export class SourceLocation { line: number; /** The column within the line the syntax begins */ column: number; + /** The column index in the line where the syntax ends */ + endColumn?: number; filePath: string; @@ -267,6 +269,8 @@ export class SourceLocation { } toString() { - return `${this.filePath}:${this.line}:${this.column}`; + return `${this.filePath}:${this.line}:${this.column + 1}${ + this.endColumn ? `-${this.endColumn + 1}` : "" + }`; } } diff --git a/src/syntax-objects/variable.ts b/src/syntax-objects/variable.ts index 6670c7c8..1669e2c5 100644 --- a/src/syntax-objects/variable.ts +++ b/src/syntax-objects/variable.ts @@ -6,6 +6,7 @@ export class Variable extends NamedEntity { readonly syntaxType = "variable"; isMutable: boolean; type?: Type; + /** Set before the type was narrowed by the type checker */ originalType?: Type; inferredType?: Type; annotatedType?: Type; diff --git a/std/operators.void b/std/operators.void index 5d5785af..c3c57cfb 100644 --- a/std/operators.void +++ b/std/operators.void @@ -5,15 +5,29 @@ pub def_wasm_operator('>', gt_s, i32, bool) pub def_wasm_operator('<=', le_s, i32, bool) pub def_wasm_operator('>=', ge_s, i32, bool) pub def_wasm_operator('==', eq, i32, bool) -pub def_wasm_operator('and', 'and', i32, bool) -pub def_wasm_operator('or', 'or', i32, bool) -pub def_wasm_operator('xor', 'xor', i32, bool) +pub def_wasm_operator('and', 'and', i32, i32) +pub def_wasm_operator('or', 'or', i32, i32) +pub def_wasm_operator('xor', 'xor', i32, i32) pub def_wasm_operator('not', ne, i32, bool) pub def_wasm_operator('+', add, i32, i32) pub def_wasm_operator('-', sub, i32, i32) pub def_wasm_operator('*', mul, i32, i32) pub def_wasm_operator('/', div_s, i32, i32) +pub def_wasm_operator('<', lt_s, i64, bool) +pub def_wasm_operator('>', gt_s, i64, bool) +pub def_wasm_operator('<=', le_s, i64, bool) +pub def_wasm_operator('>=', ge_s, i64, bool) +pub def_wasm_operator('==', eq, i64, bool) +pub def_wasm_operator('and', 'and', i64, i64) +pub def_wasm_operator('or', 'or', i64, i64) +pub def_wasm_operator('xor', 'xor', i64, i64) +pub def_wasm_operator('not', ne, i64, bool) +pub def_wasm_operator('+', add, i64, i64) +pub def_wasm_operator('-', sub, i64, i64) +pub def_wasm_operator('*', mul, i64, i64) +pub def_wasm_operator('/', div_s, i64, i64) + pub def_wasm_operator('<', lt, f32, bool) pub def_wasm_operator('>', gt, f32, bool) pub def_wasm_operator('<=', le, f32, bool) diff --git a/tsconfig.json b/tsconfig.json index a0c27f6a..e62b3e2a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, "module": "NodeNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */