From 2da011caf4e7f48cc9ee3b614e8e82cc71972e98 Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Sun, 30 Jul 2023 23:59:43 +0200 Subject: [PATCH] implement field accesses --- src/ast.ts | 18 ++++++++ src/index.ts | 67 ++-------------------------- src/lexer.ts | 4 +- src/lower.ts | 119 +++++++++++++++++++++++++++++++++++++++---------- src/parser.ts | 39 ++++++++++++---- src/printer.ts | 3 ++ src/typeck.ts | 70 +++++++++++++++++++++++++++++ 7 files changed, 224 insertions(+), 96 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index e0933d5..0c5a07c 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -120,6 +120,16 @@ export type ExprCall = { args: Expr[]; }; +export type ExprFieldAccess = { + kind: "fieldAccess"; + lhs: Expr; + field: { + value: string | number; + span: Span; + fieldIdx?: number; + }; +}; + export type ExprIf = { kind: "if"; cond: Expr; @@ -161,6 +171,7 @@ export type ExprKind = | ExprBinary | ExprUnary | ExprCall + | ExprFieldAccess | ExprIf | ExprLoop | ExprBreak @@ -546,6 +557,13 @@ export function superFoldExpr(expr: Expr, folder: Folder): Expr { args: expr.args.map((expr) => folder.expr(expr)), }; } + case "fieldAccess": { + return { + ...expr, + kind: "fieldAccess", + lhs: folder.expr(expr.lhs), + }; + } case "if": { return { ...expr, diff --git a/src/index.ts b/src/index.ts index 045eb1f..2870578 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,71 +11,12 @@ import { exec } from "child_process"; const INPUT = ` function main() = ( - prIntln(0); - prIntln(1); - prIntln(9); - prIntln(2352353); - prIntln(100); + printTuples(("hello, ", "world\\n")); ); -function prIntln(x: Int) = ( - prInt(x); - print("\n"); -); - -function stringForDigit(x: Int): String = - if x == 0 then "0" - else if x == 1 then "1" - else if x == 2 then "2" - else if x == 3 then "3" - else if x == 4 then "4" - else if x == 5 then "5" - else if x == 6 then "6" - else if x == 7 then "7" - else if x == 8 then "8" - else if x == 9 then "9" - else trap(); - -function log10(x: Int): Int = ( - let i = 0; - loop ( - if x < 10 then break; - i = i + 1; - x = x / 10; - ); - i -); - -function pow(base: Int, exp: Int): Int = ( - let acc = 1; - loop ( - if exp == 0 then break; - acc = acc * base; - exp = exp - 1; - ); - acc -); - -function prInt(x: Int) = ( - let mag = log10(x); - - loop ( - if mag == 0 then break; - let base = pow(10, mag); - - let digit = x / base; - print(stringForDigit(digit)); - - x = x % base; - mag = mag - 1; - ); - - print(stringForDigit(x % 10)); -); - -function println(s: String) = ( - print(s); - print("\n"); +function printTuples(a: (String, String)) = ( + print(a.0); + print(a.1); ); `; diff --git a/src/lexer.ts b/src/lexer.ts index 3de6b7c..40513b1 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -18,6 +18,7 @@ export type DatalessToken = | "]" | ";" | ":" + | "." | "," | "=" | "+" @@ -70,6 +71,7 @@ const SINGLE_PUNCT: string[] = [ "]", ";", ":", + ".", ",", "+", "-", @@ -245,7 +247,7 @@ export function tokenize(input: string): Token[] { } else if (isWhitespace(next)) { // ignore } else { - throw new CompilerError(`Invalid character: \`${next}\``, span); + throw new CompilerError(`invalid character: \`${next}\``, span); } } } diff --git a/src/lower.ts b/src/lower.ts index e8d1e4a..950a36c 100644 --- a/src/lower.ts +++ b/src/lower.ts @@ -9,6 +9,7 @@ import { Resolution, Ty, TyFn, + TyTuple, varUnreachable, } from "./ast"; import { encodeUtf8, unwrap } from "./utils"; @@ -558,6 +559,68 @@ function lowerExpr(fcx: FuncContext, instrs: wasm.Instr[], expr: Expr) { instrs.push(callInstr); break; } + case "fieldAccess": { + // We could just naively always evaluate the LHS normally, but that's kinda + // stupid as it would cause way too much code for `let a = (0, 0, 0); a.0` + // as that operation would first load the entire tuple onto the stack! + // Therefore, we are a little clever be peeking into the LHS and doing + // something smarter if it's another field access or ident (in the future, + // we should be able to generalize this to all "places"/"lvalues"). + + // TODO: Actually do this instead of being naive. + + const isPlace = (expr: Expr) => + expr.kind === "ident" || expr.kind === "fieldAccess"; + + function project() {} + + lowerExpr(fcx, instrs, expr.lhs); + + switch (expr.lhs.ty!.kind) { + case "tuple": { + // Tuples have a by-value ABI, so we can simply index. + const lhsSize = argRetAbi(expr.lhs.ty!).length; + const resultAbi = argRetAbi(expr.ty!); + const resultSize = resultAbi.length; + const wasmIdx = wasmTypeIdxForTupleField( + expr.lhs.ty!, + expr.field.fieldIdx! + ); + + // lhsSize=5, resultSize=2, wasmIdx=2 + // I I Y Y I + // drop, 2xlocal.set, drop, drop, 2xlocal.get + + // TODO: Establish some way of having reusable "scratch locals". + const localIdx = fcx.wasm.locals.length + fcx.wasmType.params.length; + fcx.wasm.locals.push(...resultAbi); + + Array(lhsSize - wasmIdx - resultSize) + .fill(0) + .forEach(() => instrs.push({ kind: "drop" })); + + if (expr.field.fieldIdx! > 0) { + // Keep the result in scratch space. + storeVariable(instrs, { localIdx, types: resultAbi }); + + Array(wasmIdx) + .fill(0) + .forEach(() => instrs.push({ kind: "drop" })); + + loadVariable(instrs, { localIdx, types: resultAbi }); + } + + break; + } + case "struct": { + todo("struct field accesses"); + } + default: + throw new Error("invalid field access lhs"); + } + + break; + } case "if": { lowerExpr(fcx, instrs, expr.cond!); @@ -690,32 +753,32 @@ function storeVariable(instrs: wasm.Instr[], loc: VarLocation) { }); } -function computeAbi(ty: TyFn): FnAbi { - function argRetAbi(param: Ty): ArgRetAbi { - switch (param.kind) { - case "string": - return STRING_ABI; - case "fn": - todo("fn abi"); - case "int": - return ["i64"]; - case "i32": - return ["i32"]; - case "bool": - return ["i32"]; - case "list": - todo("list abi"); - case "tuple": - return param.elems.flatMap(argRetAbi); - case "struct": - todo("struct ABI"); - case "never": - return []; - case "var": - varUnreachable(); - } +function argRetAbi(param: Ty): ArgRetAbi { + switch (param.kind) { + case "string": + return STRING_ABI; + case "fn": + todo("fn abi"); + case "int": + return ["i64"]; + case "i32": + return ["i32"]; + case "bool": + return ["i32"]; + case "list": + todo("list abi"); + case "tuple": + return param.elems.flatMap(argRetAbi); + case "struct": + todo("struct ABI"); + case "never": + return []; + case "var": + varUnreachable(); } +} +function computeAbi(ty: TyFn): FnAbi { const params = ty.params.map(argRetAbi); const ret = argRetAbi(ty.returnTy); @@ -773,6 +836,14 @@ function blockTypeForBody(cx: Context, ty: Ty): wasm.Blocktype { return { kind: "typeidx", idx: typeIdx }; } +function wasmTypeIdxForTupleField(ty: TyTuple, idx: number): number { + // Tuples are all flattened by value, so we just count the values in + // the flattened representation. + const layout = ty.elems.map(argRetAbi); + const head = layout.slice(0, idx); + return head.reduce((a, b) => a + b.length, 0); +} + function todo(msg: string): never { throw new Error(`TODO: ${msg}`); } diff --git a/src/parser.ts b/src/parser.ts index 0b0b157..f42d643 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -188,7 +188,7 @@ function parseExpr(t: Token[]): [Token[], Expr] { UNARY = { "!" | "-" } CALL - CALL = ATOM { "(" EXPR_LIST ")" } + CALL = ATOM { ( "(" EXPR_LIST ")" ) | ( "." ( IDENT | NUMBER ) ) } ATOM = "(" { EXPR ";" | "," } EXPR ")" | IDENT { STRUCT_INIT } | LITERAL | EMPTY | LET | IF | LOOP | BREAK EMPTY = @@ -271,14 +271,34 @@ function parseExprCall(t: Token[]): [Token[], Expr] { let lhs: Expr; [t, lhs] = parseExprAtom(t); - while (next(t)[1].kind === "(") { - let popen; - [t, popen] = next(t); + while (next(t)[1].kind === "(" || next(t)[1].kind === ".") { + let tok; + [t, tok] = next(t); - let args; - [t, args] = parseCommaSeparatedList(t, ")", parseExpr); + if (tok.kind === "(") { + let args; + [t, args] = parseCommaSeparatedList(t, ")", parseExpr); - lhs = { kind: "call", span: popen.span, lhs, args }; + lhs = { kind: "call", span: tok.span, lhs, args }; + } else if (tok.kind === ".") { + let access; + [t, access] = next(t); + let value; + if (access.kind === "identifier") { + value = access.ident; + } else if (access.kind === "lit_int") { + value = access.value; + } else { + unexpectedToken(access, "identifier or integer"); + } + + lhs = { + kind: "fieldAccess", + lhs, + field: { span: access.span, value }, + span: spanMerge(lhs.span, access.span), + }; + } } return [t, lhs]; @@ -565,7 +585,10 @@ function expectNext( let tok; [t, tok] = next(t); if (tok.kind !== kind) { - throw new CompilerError(`expected \`${kind}\`, found \`${tok.kind}\``, tok.span); + throw new CompilerError( + `expected \`${kind}\`, found \`${tok.kind}\``, + tok.span + ); } return [t, tok as unknown as T & Token]; } diff --git a/src/printer.ts b/src/printer.ts index 7aee2b2..2036022 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -140,6 +140,9 @@ function printExpr(expr: Expr, indent: number): string { ); } } + case "fieldAccess": { + return `${printExpr(expr.lhs, indent)}.${expr.field.value}`; + } case "if": { const elsePart = expr.else ? ` else ${printExpr(expr.else, indent + 1)}` diff --git a/src/typeck.ts b/src/typeck.ts index cdedebc..6e6df80 100644 --- a/src/typeck.ts +++ b/src/typeck.ts @@ -677,6 +677,76 @@ export function checkBody( return { ...expr, lhs, args, ty: lhsTy.returnTy }; } + case "fieldAccess": { + const lhs = this.expr(expr.lhs); + lhs.ty = infcx.resolveIfPossible(lhs.ty!); + + const { field } = expr; + let ty: Ty; + let fieldIdx: number; + switch (lhs.ty.kind) { + case "tuple": { + const { elems } = lhs.ty; + if (typeof field.value === "number") { + if (elems.length > field.value) { + ty = elems[field.value]; + fieldIdx = field.value; + } else { + throw new CompilerError( + `tuple with ${elems.length} elements cannot be indexed with ${field.value}`, + field.span + ); + } + } else { + throw new CompilerError( + "tuple fields must be accessed with numbers", + field.span + ); + } + break; + } + case "struct": { + if (typeof field.value === "string") { + const idx = lhs.ty.fields.findIndex( + ([name]) => name === field.value + ); + if (idx === -1) { + throw new CompilerError( + `field \`${field.value}\` does not exist on ${printTy( + lhs.ty + )}`, + field.span + ); + } + + ty = lhs.ty.fields[idx][1]; + fieldIdx = idx; + } else { + throw new CompilerError( + "struct fields must be accessed with their name", + field.span + ); + } + break; + } + default: { + throw new CompilerError( + "only tuples and structs have fields", + expr.span + ); + } + } + + return { + ...expr, + lhs, + field: { + ...expr.field, + fieldIdx, + }, + ty, + }; + } case "if": { const cond = this.expr(expr.cond); const then = this.expr(expr.then);