implement field accesses

This commit is contained in:
nora 2023-07-30 23:59:43 +02:00
parent 7f65dc0277
commit 2da011caf4
7 changed files with 224 additions and 96 deletions

View file

@ -120,6 +120,16 @@ export type ExprCall = {
args: Expr[]; args: Expr[];
}; };
export type ExprFieldAccess = {
kind: "fieldAccess";
lhs: Expr;
field: {
value: string | number;
span: Span;
fieldIdx?: number;
};
};
export type ExprIf = { export type ExprIf = {
kind: "if"; kind: "if";
cond: Expr; cond: Expr;
@ -161,6 +171,7 @@ export type ExprKind =
| ExprBinary | ExprBinary
| ExprUnary | ExprUnary
| ExprCall | ExprCall
| ExprFieldAccess
| ExprIf | ExprIf
| ExprLoop | ExprLoop
| ExprBreak | ExprBreak
@ -546,6 +557,13 @@ export function superFoldExpr(expr: Expr, folder: Folder): Expr {
args: expr.args.map((expr) => folder.expr(expr)), args: expr.args.map((expr) => folder.expr(expr)),
}; };
} }
case "fieldAccess": {
return {
...expr,
kind: "fieldAccess",
lhs: folder.expr(expr.lhs),
};
}
case "if": { case "if": {
return { return {
...expr, ...expr,

View file

@ -11,71 +11,12 @@ import { exec } from "child_process";
const INPUT = ` const INPUT = `
function main() = ( function main() = (
prIntln(0); printTuples(("hello, ", "world\\n"));
prIntln(1);
prIntln(9);
prIntln(2352353);
prIntln(100);
); );
function prIntln(x: Int) = ( function printTuples(a: (String, String)) = (
prInt(x); print(a.0);
print("\n"); print(a.1);
);
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");
); );
`; `;

View file

@ -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)) { } else if (isWhitespace(next)) {
// ignore // ignore
} else { } else {
throw new CompilerError(`Invalid character: \`${next}\``, span); throw new CompilerError(`invalid character: \`${next}\``, span);
} }
} }
} }

View file

@ -9,6 +9,7 @@ import {
Resolution, Resolution,
Ty, Ty,
TyFn, TyFn,
TyTuple,
varUnreachable, varUnreachable,
} from "./ast"; } from "./ast";
import { encodeUtf8, unwrap } from "./utils"; import { encodeUtf8, unwrap } from "./utils";
@ -558,6 +559,68 @@ function lowerExpr(fcx: FuncContext, instrs: wasm.Instr[], expr: Expr) {
instrs.push(callInstr); instrs.push(callInstr);
break; 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": { case "if": {
lowerExpr(fcx, instrs, expr.cond!); 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 {
function argRetAbi(param: Ty): ArgRetAbi { switch (param.kind) {
switch (param.kind) { case "string":
case "string": return STRING_ABI;
return STRING_ABI; case "fn":
case "fn": todo("fn abi");
todo("fn abi"); case "int":
case "int": return ["i64"];
return ["i64"]; case "i32":
case "i32": return ["i32"];
return ["i32"]; case "bool":
case "bool": return ["i32"];
return ["i32"]; case "list":
case "list": todo("list abi");
todo("list abi"); case "tuple":
case "tuple": return param.elems.flatMap(argRetAbi);
return param.elems.flatMap(argRetAbi); case "struct":
case "struct": todo("struct ABI");
todo("struct ABI"); case "never":
case "never": return [];
return []; case "var":
case "var": varUnreachable();
varUnreachable();
}
} }
}
function computeAbi(ty: TyFn): FnAbi {
const params = ty.params.map(argRetAbi); const params = ty.params.map(argRetAbi);
const ret = argRetAbi(ty.returnTy); const ret = argRetAbi(ty.returnTy);
@ -773,6 +836,14 @@ function blockTypeForBody(cx: Context, ty: Ty): wasm.Blocktype {
return { kind: "typeidx", idx: typeIdx }; 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 { function todo(msg: string): never {
throw new Error(`TODO: ${msg}`); throw new Error(`TODO: ${msg}`);
} }

View file

@ -188,7 +188,7 @@ function parseExpr(t: Token[]): [Token[], Expr] {
UNARY = { "!" | "-" } CALL UNARY = { "!" | "-" } CALL
CALL = ATOM { "(" EXPR_LIST ")" } CALL = ATOM { ( "(" EXPR_LIST ")" ) | ( "." ( IDENT | NUMBER ) ) }
ATOM = "(" { EXPR ";" | "," } EXPR ")" | IDENT { STRUCT_INIT } | LITERAL | EMPTY | LET | IF | LOOP | BREAK ATOM = "(" { EXPR ";" | "," } EXPR ")" | IDENT { STRUCT_INIT } | LITERAL | EMPTY | LET | IF | LOOP | BREAK
EMPTY = EMPTY =
@ -271,14 +271,34 @@ function parseExprCall(t: Token[]): [Token[], Expr] {
let lhs: Expr; let lhs: Expr;
[t, lhs] = parseExprAtom(t); [t, lhs] = parseExprAtom(t);
while (next(t)[1].kind === "(") { while (next(t)[1].kind === "(" || next(t)[1].kind === ".") {
let popen; let tok;
[t, popen] = next(t); [t, tok] = next(t);
let args; if (tok.kind === "(") {
[t, args] = parseCommaSeparatedList(t, ")", parseExpr); 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]; return [t, lhs];
@ -565,7 +585,10 @@ function expectNext<T extends BaseToken>(
let tok; let tok;
[t, tok] = next(t); [t, tok] = next(t);
if (tok.kind !== kind) { 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]; return [t, tok as unknown as T & Token];
} }

View file

@ -140,6 +140,9 @@ function printExpr(expr: Expr, indent: number): string {
); );
} }
} }
case "fieldAccess": {
return `${printExpr(expr.lhs, indent)}.${expr.field.value}`;
}
case "if": { case "if": {
const elsePart = expr.else const elsePart = expr.else
? ` else ${printExpr(expr.else, indent + 1)}` ? ` else ${printExpr(expr.else, indent + 1)}`

View file

@ -677,6 +677,76 @@ export function checkBody(
return { ...expr, lhs, args, ty: lhsTy.returnTy }; 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": { case "if": {
const cond = this.expr(expr.cond); const cond = this.expr(expr.cond);
const then = this.expr(expr.then); const then = this.expr(expr.then);