struct and array literal syntax

This commit is contained in:
sfja 2024-12-31 03:38:38 +01:00
parent 1bbb759981
commit eb2a6d90f3
18 changed files with 494 additions and 16 deletions

View File

@ -53,7 +53,9 @@ export const Builtins = {
ArrayPush: 0x22,
ArrayAt: 0x23,
ArrayLength: 0x24,
StructSet: 0x30,
StructNew: 0x30,
StructSet: 0x31,
StructAt: 0x32,
Print: 0x40,
FileOpen: 0x41,
FileClose: 0x42,

View File

@ -59,6 +59,8 @@ export type ExprKind =
sym: Sym;
}
| { type: "group"; expr: Expr }
| { type: "array"; exprs: Expr[] }
| { type: "struct"; fields: Field[] }
| { type: "field"; subject: Expr; ident: string }
| { type: "index"; subject: Expr; value: Expr }
| {
@ -101,6 +103,12 @@ export type BinaryType =
| "or"
| "and";
export type Field = {
ident: string;
expr: Expr;
pos: Pos;
};
export type Param = {
ident: string;
etype?: EType;
@ -141,7 +149,8 @@ export type ETypeKind =
sym: Sym;
}
| { type: "array"; inner: EType }
| { type: "struct"; fields: Param[] };
| { type: "struct"; fields: Param[] }
| { type: "type_of"; expr: Expr };
export type GenericParam = {
id: number;

View File

@ -1,4 +1,4 @@
import { EType, Expr, Param, Stmt } from "./ast.ts";
import { EType, Expr, Field, Param, Stmt } from "./ast.ts";
export type VisitRes = "stop" | void;
@ -21,6 +21,8 @@ export interface AstVisitor<Args extends unknown[] = []> {
visitStringExpr?(expr: Expr, ...args: Args): VisitRes;
visitIdentExpr?(expr: Expr, ...args: Args): VisitRes;
visitGroupExpr?(expr: Expr, ...args: Args): VisitRes;
visitArrayExpr?(expr: Expr, ...args: Args): VisitRes;
visitStructExpr?(expr: Expr, ...args: Args): VisitRes;
visitFieldExpr?(expr: Expr, ...args: Args): VisitRes;
visitIndexExpr?(expr: Expr, ...args: Args): VisitRes;
visitCallExpr?(expr: Expr, ...args: Args): VisitRes;
@ -38,6 +40,7 @@ export interface AstVisitor<Args extends unknown[] = []> {
visitBlockExpr?(expr: Expr, ...args: Args): VisitRes;
visitSymExpr?(expr: Expr, ...args: Args): VisitRes;
visitParam?(param: Param, ...args: Args): VisitRes;
visitField?(field: Field, ...args: Args): VisitRes;
visitEType?(etype: EType, ...args: Args): VisitRes;
visitErrorEType?(etype: EType, ...args: Args): VisitRes;
visitNullEType?(etype: EType, ...args: Args): VisitRes;
@ -48,6 +51,7 @@ export interface AstVisitor<Args extends unknown[] = []> {
visitSymEType?(etype: EType, ...args: Args): VisitRes;
visitArrayEType?(etype: EType, ...args: Args): VisitRes;
visitStructEType?(etype: EType, ...args: Args): VisitRes;
visitTypeOfEType?(etype: EType, ...args: Args): VisitRes;
visitAnno?(etype: EType, ...args: Args): VisitRes;
}
@ -175,6 +179,14 @@ export function visitExpr<Args extends unknown[] = []>(
visitExpr(expr.kind.left, v, ...args);
visitExpr(expr.kind.right, v, ...args);
break;
case "array":
if (v.visitArrayExpr?.(expr, ...args) == "stop") return;
expr.kind.exprs.map((expr) => visitExpr(expr, v, ...args));
break;
case "struct":
if (v.visitStructExpr?.(expr, ...args) == "stop") return;
expr.kind.fields.map((field) => visitField(field, v, ...args));
break;
case "if":
if (v.visitIfExpr?.(expr, ...args) == "stop") return;
visitExpr(expr.kind.cond, v, ...args);
@ -235,6 +247,15 @@ export function visitParam<Args extends unknown[] = []>(
if (param.etype) visitEType(param.etype, v, ...args);
}
export function visitField<Args extends unknown[] = []>(
field: Field,
v: AstVisitor<Args>,
...args: Args
) {
if (v.visitField?.(field, ...args) == "stop") return;
visitExpr(field.expr, v, ...args);
}
export function visitEType<Args extends unknown[] = []>(
etype: EType,
v: AstVisitor<Args>,
@ -271,6 +292,10 @@ export function visitEType<Args extends unknown[] = []>(
if (v.visitStructEType?.(etype, ...args) == "stop") return;
etype.kind.fields.map((field) => visitParam(field, v, ...args));
break;
case "type_of":
if (v.visitTypeOfEType?.(etype, ...args) == "stop") return;
visitExpr(etype.kind.expr, v, ...args);
break;
default:
throw new Error(
`etype '${

View File

@ -326,6 +326,10 @@ export class Checker {
return { type: "string" };
case "group":
return this.checkExpr(expr.kind.expr);
case "array":
throw new Error("should have been desugared");
case "struct":
return this.checkStructExpr(expr);
case "field":
return this.checkFieldExpr(expr);
case "index":
@ -393,6 +397,15 @@ export class Checker {
}
}
public checkStructExpr(expr: Expr): VType {
if (expr.kind.type !== "struct") {
throw new Error();
}
const fields: VTypeParam[] = expr.kind.fields
.map(({ ident, expr }) => ({ ident, vtype: this.checkExpr(expr) }));
return { type: "struct", fields };
}
public checkFieldExpr(expr: Expr): VType {
if (expr.kind.type !== "field") {
throw new Error();
@ -446,8 +459,8 @@ export class Checker {
if (subject.type === "fn") {
if (expr.kind.args.length !== subject.params.length) {
this.report(
`incorrect number of arguments` +
`, expected ${subject.params.length}`,
`expected ${subject.params.length} arguments` +
`, got ${expr.kind.args.length}`,
pos,
);
}
@ -657,8 +670,8 @@ export class Checker {
const args = expr.kind.args.map((arg) => this.checkExpr(arg));
if (args.length !== params.length) {
this.report(
`incorrect number of arguments` +
`, expected ${params.length}`,
`expected ${params.length} arguments` +
`, got ${args.length}`,
pos,
);
}
@ -989,7 +1002,11 @@ export class Checker {
}));
return { type: "struct", fields };
}
throw new Error(`unknown explicit type ${etype.kind.type}`);
if (etype.kind.type === "type_of") {
const exprVType = this.checkExpr(etype.kind.expr);
return exprVType;
}
throw new Error(`unknown explicit type '${etype.kind.type}'`);
}
private report(msg: string, pos: Pos) {

View File

@ -1,6 +1,7 @@
import { AstCreator, Mod, Stmt } from "./ast.ts";
import { Checker } from "./checker.ts";
import { CompoundAssignDesugarer } from "./desugar/compound_assign.ts";
import { StructLiteralDesugarer } from "./desugar/struct_literal.ts";
import { SpecialLoopDesugarer } from "./desugar/special_loop.ts";
import { Reporter } from "./info.ts";
import { Lexer } from "./lexer.ts";
@ -12,6 +13,7 @@ import { AstVisitor, VisitRes, visitStmts } from "./ast_visitor.ts";
import * as path from "jsr:@std/path";
import { Pos } from "./token.ts";
import { ArrayLiteralDesugarer } from "./desugar/array_literal.ts";
export type CompileResult = {
program: number[];
@ -34,6 +36,8 @@ export class Compiler {
).resolve();
new SpecialLoopDesugarer(this.astCreator).desugar(mod.ast);
new ArrayLiteralDesugarer(this.astCreator).desugar(mod.ast);
new StructLiteralDesugarer(this.astCreator).desugar(mod.ast);
new Resolver(this.reporter).resolve(mod.ast);

View File

@ -0,0 +1,93 @@
import {
AstCreator,
ETypeKind,
Expr,
ExprKind,
Stmt,
StmtKind,
} from "../ast.ts";
import { AstVisitor, visitExpr, VisitRes, visitStmts } from "../ast_visitor.ts";
import { Pos } from "../token.ts";
export class ArrayLiteralDesugarer implements AstVisitor {
public constructor(
private astCreator: AstCreator,
) {}
public desugar(stmts: Stmt[]) {
visitStmts(stmts, this);
}
visitArrayExpr(expr: Expr): VisitRes {
if (expr.kind.type !== "array") {
throw new Error();
}
const npos: Pos = { index: 0, line: 1, col: 1 };
const Expr = (kind: ExprKind, pos = npos) =>
this.astCreator.expr(kind, pos);
const Stmt = (kind: StmtKind, pos = npos) =>
this.astCreator.stmt(kind, pos);
const EType = (kind: ETypeKind, pos = npos) =>
this.astCreator.etype(kind, pos);
const std = (ident: string): Expr =>
Expr({
type: "path",
subject: Expr({
type: "ident",
ident: "std",
}),
ident,
});
if (expr.kind.exprs.length < 1) {
throw new Error("");
}
expr.kind = {
type: "block",
stmts: [
Stmt({
type: "let",
param: {
ident: "::value",
pos: npos,
},
value: Expr({
type: "call",
subject: Expr({
type: "etype_args",
subject: std("array_new"),
etypeArgs: [
EType({
type: "type_of",
expr: expr.kind.exprs[0],
}),
],
}),
args: [],
}),
}),
...expr.kind.exprs
.map((expr) =>
Stmt({
type: "expr",
expr: Expr({
type: "call",
subject: std("array_push"),
args: [
Expr({ type: "ident", ident: "::value" }),
expr,
],
}),
})
),
],
expr: Expr({ type: "ident", ident: "::value" }),
};
visitExpr(expr, this);
return "stop";
}
}

View File

@ -0,0 +1,102 @@
import {
AstCreator,
ETypeKind,
Expr,
ExprKind,
Stmt,
StmtKind,
} from "../ast.ts";
import {
AstVisitor,
visitExpr,
visitField,
VisitRes,
visitStmts,
} from "../ast_visitor.ts";
import { Pos } from "../token.ts";
export class StructLiteralDesugarer implements AstVisitor {
public constructor(
private astCreator: AstCreator,
) {}
public desugar(stmts: Stmt[]) {
visitStmts(stmts, this);
}
visitStructExpr(expr: Expr): VisitRes {
if (expr.kind.type !== "struct") {
throw new Error();
}
const npos: Pos = { index: 0, line: 1, col: 1 };
const Expr = (kind: ExprKind, pos = npos) =>
this.astCreator.expr(kind, pos);
const Stmt = (kind: StmtKind, pos = npos) =>
this.astCreator.stmt(kind, pos);
const EType = (kind: ETypeKind, pos = npos) =>
this.astCreator.etype(kind, pos);
const std = (ident: string): Expr =>
Expr({
type: "path",
subject: Expr({
type: "ident",
ident: "std",
}),
ident,
});
// Yes, I know this isn't a deep clone,
// but I don't really need it to be.
const oldExpr = { ...expr };
const fields = expr.kind.fields;
expr.kind = {
type: "block",
stmts: [
Stmt({
type: "let",
param: {
ident: "::value",
pos: npos,
},
value: Expr({
type: "call",
subject: Expr({
type: "etype_args",
subject: std("struct_new"),
etypeArgs: [
EType({ type: "type_of", expr: oldExpr }),
],
}),
args: [],
}),
}),
...expr.kind.fields
.map((field) =>
Stmt({
type: "assign",
assignType: "=",
subject: Expr({
type: "field",
subject: Expr({
type: "ident",
ident: "::value",
}),
ident: field.ident,
}),
value: field.expr,
})
),
],
expr: Expr({ type: "ident", ident: "::value" }),
};
for (const field of fields) {
visitField(field, this);
}
return "stop";
}
}

View File

@ -277,7 +277,7 @@ class MonoFnLowerer {
case "group":
return void this.lowerExpr(expr.kind.expr);
case "field":
break;
return this.lowerFieldExpr(expr);
case "index":
return this.lowerIndexExpr(expr);
case "call":
@ -298,6 +298,20 @@ class MonoFnLowerer {
throw new Error(`unhandled expr '${expr.kind.type}'`);
}
private lowerFieldExpr(expr: Expr) {
if (expr.kind.type !== "field") {
throw new Error();
}
this.lowerExpr(expr.kind.subject);
this.program.add(Ops.PushString, expr.kind.ident);
if (expr.kind.subject.vtype?.type == "struct") {
this.program.add(Ops.Builtin, Builtins.StructAt);
return;
}
throw new Error(`unhandled field subject type '${expr.kind.subject}'`);
}
private lowerIndexExpr(expr: Expr) {
if (expr.kind.type !== "index") {
throw new Error();

View File

@ -6,6 +6,7 @@ import {
ETypeKind,
Expr,
ExprKind,
Field,
GenericParam,
Param,
Stmt,
@ -17,7 +18,7 @@ import { printStackTrace, Reporter } from "./info.ts";
import { Lexer } from "./lexer.ts";
import { Pos, Token } from "./token.ts";
type Res<T> = { ok: true; value: T } | { ok: false };
type Res<T> = { ok: true; value: T } | { ok: false; pos?: Pos };
export class Parser {
private currentToken: Token | null;
@ -203,6 +204,9 @@ export class Parser {
args.push(this.parseExpr());
while (this.test(",")) {
this.step();
if (this.done() || this.test(")")) {
break;
}
args.push(this.parseExpr());
}
}
@ -536,6 +540,80 @@ export class Parser {
return this.expr({ type: "for", decl, cond, incr, body }, pos);
}
private parseArray(): Expr {
const pos = this.pos();
this.step();
const exprs: Expr[] = [];
if (!this.test("]")) {
exprs.push(this.parseExpr());
while (this.test(",")) {
this.step();
if (this.done() || this.test("]")) {
break;
}
exprs.push(this.parseExpr());
}
}
if (!this.test("]")) {
this.report("expected ']'");
return this.expr({ type: "error" }, pos);
}
this.step();
return this.expr({ type: "array", exprs }, pos);
}
private parseStruct(): Expr {
const pos = this.pos();
this.step();
if (!this.test("{")) {
this.report("expected '{'");
return this.expr({ type: "error" }, pos);
}
this.step();
const fields: Field[] = [];
if (!this.test("}")) {
const res = this.parseStructField();
if (!res.ok) {
return this.expr({ type: "error" }, res.pos!);
}
fields.push(res.value);
while (this.test(",")) {
this.step();
if (this.done() || this.test("}")) {
break;
}
const res = this.parseStructField();
if (!res.ok) {
return this.expr({ type: "error" }, res.pos!);
}
fields.push(res.value);
}
}
if (!this.test("}")) {
this.report("expected '}'");
return this.expr({ type: "error" }, pos);
}
this.step();
return this.expr({ type: "struct", fields }, pos);
}
private parseStructField(): Res<Field> {
const pos = this.pos();
if (!this.test("ident")) {
this.report("expected 'ident'");
return { ok: false, pos };
}
const ident = this.current().identValue!;
this.step();
if (!this.test(":")) {
this.report("expected ':'");
return { ok: false, pos };
}
this.step();
const expr = this.parseExpr();
return { ok: true, value: { ident, expr, pos } };
}
private parseIf(): Expr {
const pos = this.pos();
this.step();
@ -815,6 +893,12 @@ export class Parser {
this.step();
return this.expr({ type: "group", expr }, pos);
}
if (this.test("[")) {
return this.parseArray();
}
if (this.test("struct")) {
return this.parseStruct();
}
if (this.test("{")) {
return this.parseBlock();
}

View File

@ -47,6 +47,24 @@ export function vtypesEqual(
if (a.type === "array" && b.type === "array") {
return vtypesEqual(a.inner, b.inner, generics);
}
if (a.type === "struct" && b.type === "struct") {
if (a.fields.length !== b.fields.length) {
return false;
}
const match = a.fields
.map((af) => ({
ident: af.ident,
af,
bf: b.fields.find((bf) => bf.ident === af.ident),
}));
if (match.some((m) => m.bf === undefined)) {
return false;
}
if (match.some((m) => !vtypesEqual(m.af.vtype, m.bf!.vtype))) {
return false;
}
return true;
}
if (a.type === "fn" && b.type === "fn") {
if (a.params.length !== b.params.length) {
return false;
@ -101,6 +119,12 @@ export function vtypeToString(vtype: VType): string {
if (vtype.type === "array") {
return `[${vtypeToString(vtype.inner)}]`;
}
if (vtype.type === "struct") {
const fields = vtype.fields
.map((field) => `${field.ident}: ${vtypeToString(field.vtype)}`)
.join(", ");
return `struct { ${fields} }`;
}
if (vtype.type === "fn") {
const paramString = vtype.params.map((param) =>
`${param.ident}: ${vtypeToString(param.vtype)}`

View File

@ -1,6 +1,7 @@
#include "alloc.hpp"
#include <format>
#include <iostream>
#include <string>
using namespace sliger::heap;
@ -14,3 +15,18 @@ auto Array::at(int32_t index) & -> Value&
}
return values.at(static_cast<size_t>(index));
}
auto Struct::at(const std::string& field) & -> Value&
{
if (this->fields.find(field) == this->fields.end()) {
std::cout << std::format(
"field name not in struct, got: \"{}\"\n", field);
exit(1);
}
return this->fields.at(field);
}
void Struct::assign(const std::string& field, Value&& value)
{
this->fields.insert_or_assign(field, value);
}

View File

@ -19,6 +19,9 @@ struct Array {
struct Struct {
std::unordered_map<std::string, Value> fields;
auto at(const std::string&) & -> Value&;
void assign(const std::string&, Value&& value);
};
enum class AllocType {

View File

@ -54,7 +54,9 @@ enum class Builtin : uint32_t {
ArrayPush = 0x22,
ArrayAt = 0x23,
ArrayLength = 0x24,
StructSet = 0x30,
StructNew = 0x30,
StructSet = 0x31,
StructAt = 0x32,
Print = 0x40,
FileOpen = 0x41,
FileClose = 0x42,

View File

@ -289,6 +289,11 @@ void VM::run_instruction()
this->current_pos = { index, line, col };
break;
}
default:
std::cerr << std::format("unrecognized instruction '{}', pc = {}",
std::to_underlying(op), this->pc);
std::exit(1);
break;
}
this->instruction_counter += 1;
}
@ -331,12 +336,11 @@ void VM::run_builtin(Builtin builtin_id)
run_array_builtin(builtin_id);
break;
case Builtin::StructSet: {
assert_stack_has(2);
std::cerr << std::format("not implemented\n");
std::exit(1);
case Builtin::StructNew:
case Builtin::StructSet:
case Builtin::StructAt:
run_struct_builtin(builtin_id);
break;
}
case Builtin::Print:
case Builtin::FileOpen:
@ -407,6 +411,7 @@ void VM::run_string_builtin(Builtin builtin_id)
break;
}
}
void VM::run_array_builtin(Builtin builtin_id)
{
switch (builtin_id) {
@ -456,6 +461,40 @@ void VM::run_array_builtin(Builtin builtin_id)
}
}
void VM::run_struct_builtin(Builtin builtin_id)
{
switch (builtin_id) {
case Builtin::StructNew: {
auto alloc_res = this->heap.alloc<heap::AllocType::Struct>();
stack_push(Ptr(alloc_res.val()));
break;
}
case Builtin::StructSet: {
assert_stack_has(2);
auto field = stack_pop().as_string().value;
auto struct_ptr = stack_pop().as_ptr().value;
auto value = stack_pop();
this->heap.at(struct_ptr)
.val()
->as_struct()
.assign(field, std::move(value));
stack_push(Null());
break;
}
case Builtin::StructAt: {
assert_stack_has(2);
auto field = stack_pop().as_string().value;
auto struct_ptr = stack_pop().as_ptr().value;
stack_push(this->heap.at(struct_ptr).val()->as_struct().at(field));
break;
}
default:
break;
}
}
void VM::run_file_builtin(Builtin builtin_id)
{
switch (builtin_id) {

View File

@ -199,6 +199,7 @@ private:
void run_builtin(Builtin builtin_id);
void run_string_builtin(Builtin builtin_id);
void run_array_builtin(Builtin builtin_id);
void run_struct_builtin(Builtin builtin_id);
void run_file_builtin(Builtin builtin_id);
inline void step() { this->pc += 1; }

View File

@ -29,6 +29,11 @@ pub fn array_length<T>(array: [T]) -> int {}
#[builtin(ArrayAt)]
pub fn array_at<T>(array: [T], index: int) -> T {}
#[builtin(StructNew)]
pub fn struct_new<S>() -> S {}
#[builtin(StructSet)]
pub fn struct_set<S, T>(subject: S, value: T) {}
#[builtin(FileOpen)]
pub fn file_open(filename: string, mode: string) -> int {}
#[builtin(FileClose)]
@ -168,3 +173,10 @@ pub fn array_to_sorted(array: [int]) -> [int] {
cloned
}
pub fn assert(value: bool, msg: string) {
if not value {
println("assertion failed: " + msg);
exit(1);
}
}

11
tests/array_literal.slg Normal file
View File

@ -0,0 +1,11 @@
mod std;
fn main() {
let ints = [1, 2, 3];
std::assert(ints[1] == 2, "test int array");
let strings = ["foo", "bar", "baz"];
std::assert(strings[1] == "bar", "test string array");
std::println("tests ran successfully");
}

20
tests/struct_literal.slg Normal file
View File

@ -0,0 +1,20 @@
mod std;
fn main() {
let d = true;
let v = struct {
a: 123,
b: struct {
c: "foo",
d: d,
},
};
std::assert(v.a == 123, "test field");
std::assert(v.b.c == "foo", "test nested field");
std::assert(v.b.d == true, "test resolved field");
std::println("tests ran successfully");
}