diff --git a/compiler/arch.ts b/compiler/arch.ts index b6fc64f..2448b92 100644 --- a/compiler/arch.ts +++ b/compiler/arch.ts @@ -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, diff --git a/compiler/ast.ts b/compiler/ast.ts index 1b98fe1..7119da5 100644 --- a/compiler/ast.ts +++ b/compiler/ast.ts @@ -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; diff --git a/compiler/ast_visitor.ts b/compiler/ast_visitor.ts index 80b69bd..62eb19b 100644 --- a/compiler/ast_visitor.ts +++ b/compiler/ast_visitor.ts @@ -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 { 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 { 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 { 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( 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( if (param.etype) visitEType(param.etype, v, ...args); } +export function visitField( + field: Field, + v: AstVisitor, + ...args: Args +) { + if (v.visitField?.(field, ...args) == "stop") return; + visitExpr(field.expr, v, ...args); +} + export function visitEType( etype: EType, v: AstVisitor, @@ -271,6 +292,10 @@ export function visitEType( 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 '${ diff --git a/compiler/checker.ts b/compiler/checker.ts index d8f92da..7a42f4b 100644 --- a/compiler/checker.ts +++ b/compiler/checker.ts @@ -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) { diff --git a/compiler/compiler.ts b/compiler/compiler.ts index 9851c90..6d85a1b 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -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); diff --git a/compiler/desugar/array_literal.ts b/compiler/desugar/array_literal.ts new file mode 100644 index 0000000..5a75011 --- /dev/null +++ b/compiler/desugar/array_literal.ts @@ -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"; + } +} diff --git a/compiler/desugar/struct_literal.ts b/compiler/desugar/struct_literal.ts new file mode 100644 index 0000000..93405ad --- /dev/null +++ b/compiler/desugar/struct_literal.ts @@ -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"; + } +} diff --git a/compiler/lowerer.ts b/compiler/lowerer.ts index 4adc504..671208f 100644 --- a/compiler/lowerer.ts +++ b/compiler/lowerer.ts @@ -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(); diff --git a/compiler/parser.ts b/compiler/parser.ts index 1f6b002..d854faa 100644 --- a/compiler/parser.ts +++ b/compiler/parser.ts @@ -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 = { ok: true; value: T } | { ok: false }; +type Res = { 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 { + 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(); } diff --git a/compiler/vtype.ts b/compiler/vtype.ts index 24bf59d..bf432d1 100644 --- a/compiler/vtype.ts +++ b/compiler/vtype.ts @@ -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)}` diff --git a/runtime/alloc.cpp b/runtime/alloc.cpp index 374bb4b..d60ef02 100644 --- a/runtime/alloc.cpp +++ b/runtime/alloc.cpp @@ -1,6 +1,7 @@ #include "alloc.hpp" #include #include +#include using namespace sliger::heap; @@ -14,3 +15,18 @@ auto Array::at(int32_t index) & -> Value& } return values.at(static_cast(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); +} diff --git a/runtime/alloc.hpp b/runtime/alloc.hpp index 51fa1af..21c4f96 100644 --- a/runtime/alloc.hpp +++ b/runtime/alloc.hpp @@ -19,6 +19,9 @@ struct Array { struct Struct { std::unordered_map fields; + + auto at(const std::string&) & -> Value&; + void assign(const std::string&, Value&& value); }; enum class AllocType { diff --git a/runtime/arch.hpp b/runtime/arch.hpp index 91ce34d..24cf056 100644 --- a/runtime/arch.hpp +++ b/runtime/arch.hpp @@ -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, diff --git a/runtime/vm.cpp b/runtime/vm.cpp index a659714..00dfd37 100644 --- a/runtime/vm.cpp +++ b/runtime/vm.cpp @@ -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(); + 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) { diff --git a/runtime/vm.hpp b/runtime/vm.hpp index 5ed6278..fca2dc5 100644 --- a/runtime/vm.hpp +++ b/runtime/vm.hpp @@ -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; } diff --git a/std/lib.slg b/std/lib.slg index 80e7579..b5c8d79 100644 --- a/std/lib.slg +++ b/std/lib.slg @@ -29,6 +29,11 @@ pub fn array_length(array: [T]) -> int {} #[builtin(ArrayAt)] pub fn array_at(array: [T], index: int) -> T {} +#[builtin(StructNew)] +pub fn struct_new() -> S {} +#[builtin(StructSet)] +pub fn struct_set(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); + } +} + diff --git a/tests/array_literal.slg b/tests/array_literal.slg new file mode 100644 index 0000000..c7d8d34 --- /dev/null +++ b/tests/array_literal.slg @@ -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"); +} diff --git a/tests/struct_literal.slg b/tests/struct_literal.slg new file mode 100644 index 0000000..b3ed366 --- /dev/null +++ b/tests/struct_literal.slg @@ -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"); +} +