diff --git a/src/ast.ts b/src/ast.ts index 85cbbbb..6253dda 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -54,15 +54,18 @@ export type ExprEmpty = { kind: "empty" }; export type ExprLet = { kind: "let"; - name: string; + name: Identifier; type?: Type; rhs: Expr; - after: Expr; + // IMPORTANT: This is (sadly) shared with ExprBlock. + local?: LocalInfo, }; export type ExprBlock = { kind: "block"; exprs: Expr[]; + // IMPORTANT: This is (sadly) shared with ExprLet. + locals?: LocalInfo[]; }; export type ExprLiteral = { @@ -206,13 +209,15 @@ export type Type = TypeKind & { ty?: Ty; }; +// name resolution stuff + export type Resolution = | { kind: "local"; /** * The index of the local variable, from inside out. * ``` - * let a in let b in (a, b); + * let a = 0; let b; (a, b); * ^ ^ * 1 0 * ``` @@ -246,6 +251,14 @@ export const BUILTINS = [ export type BuiltinName = (typeof BUILTINS)[number]; +export type LocalInfo = { + name: string; + span: Span; + ty?: Ty; +}; + +// types + export type TyString = { kind: "string"; }; @@ -397,7 +410,6 @@ export function superFoldExpr(expr: Expr, folder: Folder): Expr { name: expr.name, type: expr.type && folder.type(expr.type), rhs: folder.expr(expr.rhs), - after: folder.expr(expr.after), }; } case "block": { diff --git a/src/error.ts b/src/error.ts index 432bf57..fce66b5 100644 --- a/src/error.ts +++ b/src/error.ts @@ -52,8 +52,13 @@ function renderError(input: string, e: CompilerError) { console.error(`${lineIdx} | ${spanToSnippet(input, line)}`); const startRelLine = e.span.start === Number.MAX_SAFE_INTEGER ? 0 : e.span.start - line.start; + + const spanLength = min(e.span.end, line.end) - e.span.start; + console.error( - `${" ".repeat(String(lineIdx).length)} ${" ".repeat(startRelLine)}^` + `${" ".repeat(String(lineIdx).length)} ${" ".repeat( + startRelLine + )}${"^".repeat(spanLength)}` ); } @@ -81,3 +86,7 @@ export function lines(input: string): Span[] { export function todo(msg: string): never { throw new CompilerError(`TODO: ${msg}`, { start: 0, end: 0 }); } + +function min(a: number, b: number): number { + return a < b ? a : b; +} diff --git a/src/index.ts b/src/index.ts index c87a943..92b5dc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,14 +10,13 @@ import fs from "fs"; import { exec } from "child_process"; const input = ` -type Uwu = ( - meow: String, - oops: Int, - aaa: (), -); +function printInt(a: Int) = ; -function aa(a: Int, b: Uwu): Uwu = Uwu {meow: "",oops:0,aaa:()}; -function main() = (); +function main() = ( + let a = 0; + let b = 0; + printInt(a + b); +); `; function main() { @@ -44,8 +43,8 @@ function main() { console.log("-----AST typecked------"); const typecked = typeck(resolved); - - return; + console.dir(typecked, {depth: 8}); + console.log("-----wasm--------------"); const wasmModule = lowerToWasm(typecked); diff --git a/src/lexer.ts b/src/lexer.ts index a8adbbd..d93d122 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -3,7 +3,6 @@ import { CompilerError, Span } from "./error"; export type DatalessToken = | "function" | "let" - | "in" | "if" | "then" | "else" @@ -79,6 +78,14 @@ export function tokenize(input: string): Token[] { const next = input[i]; const span: Span = { start: i, end: i + 1 }; + if (next === "/" && input[i + 1] === "/") { + while (input[i] !== "\n") { + i++; + } + + continue; + } + if (SINGLE_PUNCT.includes(next)) { tokens.push({ kind: next as DatalessToken, span }); } else { @@ -207,15 +214,16 @@ function isWhitespace(char: string): boolean { return char === " " || char === "\t" || char === "\n" || char === "\r"; } -const keywords = new Set([ +const KEYOWRDS: DatalessToken[] = [ "function", "let", - "in", "if", "then", "else", "type", -]); +]; + +const KEYWORD_SET = new Set(KEYOWRDS); function isKeyword(kw: string): DatalessToken | undefined { - return keywords.has(kw) ? (kw as DatalessToken) : undefined; + return KEYWORD_SET.has(kw) ? (kw as DatalessToken) : undefined; } diff --git a/src/lower.ts b/src/lower.ts index ec9e655..0b76f4f 100644 --- a/src/lower.ts +++ b/src/lower.ts @@ -121,6 +121,8 @@ function lowerExpr(fcx: FuncContext, instrs: wasm.Instr[], expr: Expr) { case "block": if (expr.exprs.length === 1) { lowerExpr(fcx, instrs, expr.exprs[0]); + } else { + todo("complex blocks"); } break; case "literal": diff --git a/src/parser.ts b/src/parser.ts index b0ac386..4f501ad 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -4,9 +4,11 @@ import { Ast, BinaryKind, COMPARISON_KINDS, + DEFAULT_FOLDER, Expr, ExprStructLiteral, FieldDef, + Folder, FunctionArg, FunctionDef, Identifier, @@ -16,9 +18,11 @@ import { TypeDef, UNARY_KINDS, UnaryKind, + foldAst, + superFoldExpr, } from "./ast"; -import { CompilerError, Span, todo } from "./error"; -import { BaseToken, DatalessToken, Token, TokenIdent } from "./lexer"; +import { CompilerError, Span, spanMerge } from "./error"; +import { BaseToken, Token, TokenIdent } from "./lexer"; type Parser = (t: Token[]) => [Token[], T]; @@ -33,7 +37,11 @@ export function parse(t: Token[]): Ast { const withIds = items.map((item, i) => ({ ...item, id: i })); - return { items: withIds }; + const ast = { items: withIds }; + + validateAst(ast); + + return ast; } function parseItem(t: Token[]): [Token[], Item] { @@ -127,7 +135,7 @@ function parseItem(t: Token[]): [Token[], Item] { function parseExpr(t: Token[]): [Token[], Expr] { /* - EXPR = { LET | COMPARISON | IF } + EXPR = COMPARISON LET = "let" NAME { ":" TYPE } "=" EXPR "in" EXPR IF = "if" EXPR "then" EXPR { "else" EXPR } @@ -145,56 +153,11 @@ function parseExpr(t: Token[]): [Token[], Expr] { CALL = ATOM { "(" EXPR_LIST ")" } - ATOM = "(" { EXPR ";" } EXPR ")" | IDENT { STRUCT_INIT } | LITERAL | EMPTY + ATOM = "(" { EXPR ";" } EXPR ")" | IDENT { STRUCT_INIT } | LITERAL | EMPTY | LET | IF EMPTY = STRUCT_INIT = "{" { NAME ":" EXPR } { "," NAME ":" EXPR } { "," } "}" EXPR_LIST = { EXPR { "," EXPR } { "," } } */ - const [, peak] = next(t); - - if (peak.kind === "let") { - [t] = expectNext(t, "let"); - let name; - [t, name] = expectNext(t, "identifier"); - - let type = undefined; - let colon; - [t, colon] = eat(t, ":"); - if (colon) { - [t, type] = parseType(t); - } - - [t] = expectNext(t, "="); - let rhs; - [t, rhs] = parseExpr(t); - [t] = expectNext(t, "in"); - let after; - [t, after] = parseExpr(t); - - return [ - t, - { kind: "let", name: name.ident, type, rhs, after, span: name.span }, - ]; - } - - if (peak.kind === "if") { - [t] = expectNext(t, "if"); - let cond; - [t, cond] = parseExpr(t); - [t] = expectNext(t, "then"); - let then; - [t, then] = parseExpr(t); - - let elseTok; - [t, elseTok] = eat(t, "else"); - let elsePart = undefined; - if (elseTok) { - [t, elsePart] = parseExpr(t); - } - - return [t, { kind: "if", cond, then, else: elsePart, span: peak.span }]; - } - return parseExprComparison(t); } @@ -206,15 +169,15 @@ function mkParserExprBinary( let lhs; [t, lhs] = lower(t); - const [, peak] = next(t); - if (kinds.includes(peak.kind)) { + const [, peek] = next(t); + if (kinds.includes(peek.kind)) { [t] = next(t); let rhs; [t, rhs] = parser(t); - const span = peak.span; + const span = spanMerge(lhs.span, rhs.span); return [ t, - { kind: "binary", binaryKind: peak.kind as BinaryKind, lhs, rhs, span }, + { kind: "binary", binaryKind: peek.kind as BinaryKind, lhs, rhs, span }, ]; } @@ -318,8 +281,6 @@ function parseExprAtom(startT: Token[]): [Token[], Expr] { } if (tok.kind === "identifier") { - console.log(t); - if (maybeNextT(t)[1]?.kind === "{") { let fields; [t, fields] = parseStructInit(t); @@ -344,6 +305,52 @@ function parseExprAtom(startT: Token[]): [Token[], Expr] { ]; } + if (tok.kind === "let") { + let name; + [t, name] = expectNext(t, "identifier"); + + let type = undefined; + let colon; + [t, colon] = eat(t, ":"); + if (colon) { + [t, type] = parseType(t); + } + + [t] = expectNext(t, "="); + let rhs; + [t, rhs] = parseExpr(t); + + const nameIdent: Identifier = { name: name.ident, span: name.span }; + + return [ + t, + { + kind: "let", + name: nameIdent, + type, + rhs, + span: name.span, + }, + ]; + } + + if (tok.kind === "if") { + let cond; + [t, cond] = parseExpr(t); + [t] = expectNext(t, "then"); + let then; + [t, then] = parseExpr(t); + + let elseTok; + [t, elseTok] = eat(t, "else"); + let elsePart = undefined; + if (elseTok) { + [t, elsePart] = parseExpr(t); + } + + return [t, { kind: "if", cond, then, else: elsePart, span: tok.span }]; + } + // Parse nothing at all. return [startT, { kind: "empty", span }]; } @@ -496,3 +503,30 @@ function maybeNextT(t: Token[]): [Token[], Token | undefined] { function unexpectedToken(token: Token): never { throw new CompilerError("unexpected token", token.span); } + +function validateAst(ast: Ast) { + const validator: Folder = { + ...DEFAULT_FOLDER, + expr(value: Expr): Expr { + if (value.kind === "block") { + value.exprs.forEach((expr) => { + if (expr.kind === "let") { + this.expr(expr.rhs); + if (expr.type) { + this.type(expr.type); + } + } else { + this.expr(expr); + } + }); + return value; + } else if (value.kind === "let") { + throw new CompilerError("let is only allowed in blocks", value.span); + } else { + return superFoldExpr(value, this); + } + }, + }; + + foldAst(ast, validator); +} diff --git a/src/printer.ts b/src/printer.ts index d801d0b..90c6ebd 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -53,10 +53,10 @@ function printExpr(expr: Expr, indent: number): string { case "let": { const type = expr.type ? `: ${printType(expr.type)}` : ""; - return `let ${expr.name}${type} = ${printExpr( + return `let ${expr.name.name}${type} = ${printExpr( expr.rhs, indent + 1 - )} in${linebreak(indent)}${printExpr(expr.after, indent)}`; + )}`; } case "block": { const exprs = expr.exprs.map((expr) => printExpr(expr, indent + 1)); diff --git a/src/resolve.ts b/src/resolve.ts index d01b53a..a5b4423 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -3,8 +3,10 @@ import { BUILTINS, BuiltinName, DEFAULT_FOLDER, + Expr, Folder, Identifier, + LocalInfo, Resolution, foldAst, superFoldExpr, @@ -68,6 +70,8 @@ export function resolve(ast: Ast): Ast { throw new CompilerError(`cannot find ${ident.name}`, ident.span); }; + const blockLocals: LocalInfo[][] = []; + const resolver: Folder = { ...DEFAULT_FOLDER, item(item) { @@ -82,7 +86,7 @@ export function resolve(ast: Ast): Ast { item.node.returnType && this.type(item.node.returnType); item.node.params.forEach(({ name }) => scopes.push(name)); - const body = superFoldExpr(item.node.body, this); + const body = this.expr(item.node.body); const revParams = item.node.params.slice(); revParams.reverse(); revParams.forEach(({ name }) => popScope(name)); @@ -104,24 +108,39 @@ export function resolve(ast: Ast): Ast { return superFoldItem(item, this); }, expr(expr) { - if (expr.kind === "let") { - const rhs = this.expr(expr.rhs); - const type = expr.type && this.type(expr.type); - scopes.push(expr.name); - const after = this.expr(expr.after); - popScope(expr.name); + if (expr.kind === "block") { + const prevScopeLength = scopes.length; + blockLocals.push([]); + + const exprs = expr.exprs.map((inner) => this.expr(inner)); + + scopes.length = prevScopeLength; + const locals = blockLocals.pop(); return { - kind: "let", - name: expr.name, - rhs, - after, - type, + kind: "block", + exprs, + locals, span: expr.span, }; - } + } else if (expr.kind === "let") { + let rhs = this.expr(expr.rhs); + let type = expr.type && this.type(expr.type); - return superFoldExpr(expr, this); + scopes.push(expr.name.name); + const local = { name: expr.name.name, span: expr.name.span }; + blockLocals[blockLocals.length - 1].push(local); + + return { + ...expr, + name: expr.name, + local, + type, + rhs, + }; + } else { + return superFoldExpr(expr, this); + } }, ident(ident) { const res = resolveIdent(ident); diff --git a/src/typeck.ts b/src/typeck.ts index 26e1cbc..8dcdb91 100644 --- a/src/typeck.ts +++ b/src/typeck.ts @@ -425,9 +425,11 @@ export function checkBody( const rhs = this.expr(expr.rhs); infcx.assign(bindingTy, rhs.ty!, expr.span); + // AST validation ensures that lets can only be in blocks, where + // the types will be popped. localTys.push(bindingTy); - const after = this.expr(expr.after); - localTys.pop(); + + expr.local!.ty = bindingTy; const type: Type | undefined = loweredBindingTy && { ...expr.type!, @@ -439,20 +441,23 @@ export function checkBody( name: expr.name, type, rhs, - after, - ty: after.ty!, + ty: TY_UNIT, span: expr.span, }; } case "block": { + const prevLocalTysLen = localTys.length; + const exprs = expr.exprs.map((expr) => this.expr(expr)); + const ty = exprs.length > 0 ? exprs[exprs.length - 1].ty! : TY_UNIT; + localTys.length = prevLocalTysLen; + return { - kind: "block", + ...expr, exprs, ty, - span: expr.span, }; } case "literal": { @@ -592,13 +597,25 @@ export function checkBody( infcx.assign(fnTy.returnTy, checked.ty!, body.span); + const resolveTy = (ty: Ty, span: Span) => { + const resTy = infcx.resolveIfPossible(ty); + if (!resTy) { + throw new CompilerError("cannot infer type", span); + } + return resTy; + }; + const resolver: Folder = { ...DEFAULT_FOLDER, expr(expr) { - const ty = infcx.resolveIfPossible(expr.ty!); - if (!ty) { - throw new CompilerError("cannot infer type", expr.span); + const ty = resolveTy(expr.ty!, expr.span); + + if (expr.kind === "block") { + expr.locals!.forEach((local) => { + local.ty = resolveTy(local.ty!, local.span); + }); } + return { ...expr, ty }; }, };