diff --git a/slige/compiler/ast/ast.ts b/slige/compiler/ast/ast.ts new file mode 100644 index 0000000..c50faf3 --- /dev/null +++ b/slige/compiler/ast/ast.ts @@ -0,0 +1,269 @@ +import { AstId, File as CtxFile, IdentId } from "../ids.ts"; +import { Span } from "../diagnostics.ts"; + +export type File = { + stmts: Stmt[]; + file: CtxFile; +}; + +export type Stmt = { + id: AstId; + kind: StmtKind; + span: Span; +}; + +export type StmtKind = + | { tag: "error" } + | { tag: "item" } & ItemStmt + | { tag: "let" } & LetStmt + | { tag: "return" } & ReturnStmt + | { tag: "break" } & BreakStmt + | { tag: "continue" } + | { tag: "assign" } & AssignStmt + | { tag: "expr" } & ExprStmt; + +export type ItemStmt = { item: Item }; + +export type LetStmt = { + pat: Pat; + ty?: Ty; + expr?: Expr; +}; + +export type ReturnStmt = { expr?: Expr }; +export type BreakStmt = { expr?: Expr }; + +export type AssignStmt = { + assignType: AssignType; + subject: Expr; + value: Expr; +}; + +export type AssignType = "=" | "+=" | "-="; + +export type ExprStmt = { expr: Expr }; + +export type Item = { + id: AstId; + kind: ItemKind; + span: Span; + ident: Ident; + pub: boolean; +}; + +export type ItemKind = + | { tag: "error" } + | { tag: "mod_block" } & ModBlockItem + | { tag: "mod_file" } & ModFileItem + | { tag: "enum" } & EnumItem + | { tag: "struct" } & StructItem + | { tag: "fn" } & FnItem + | { tag: "use" } & UseItem + | { tag: "type_alias" } & TypeAliasItem; + +export type ModBlockItem = { block: Block }; + +export type ModFileItem = { filePath: string; file?: CtxFile; ast?: File }; + +export type EnumItem = { variants: Variant[] }; +export type StructItem = { data: VariantData }; + +export type FnItem = { + generics?: Generics; + params: Param[]; + returnTy?: Ty; + body?: Block; +}; + +export type UseItem = { _: 0 }; +export type TypeAliasItem = { ty: Ty }; + +export type Variant = { + ident: Ident; + data: VariantData; + pub: boolean; + span: Span; +}; + +export type VariantData = { + kind: VariantDataKind; + span: Span; +}; + +export type VariantDataKind = + | { tag: "error" } + | { tag: "unit" } + | { tag: "tuple" } & TupleVariantData + | { tag: "struct" } & StructVariantData; + +export type TupleVariantData = { elems: VariantData[] }; +export type StructVariantData = { fields: FieldDef[] }; + +export type FieldDef = { + ident: Ident; + ty: Ty; + pub: boolean; + span: Span; +}; + +export type Param = { + pat: Pat; + ty: Ty; + span: Span; +}; + +export type Generics = { + params: GenericParam[]; +}; + +export type GenericParam = { + ident: Ident; + span: Span; +}; + +export type Expr = { + id: AstId; + kind: ExprKind; + span: Span; +}; + +export type ExprKind = + | { tag: "error" } + | { tag: "path" } & PathExpr + | { tag: "null" } + | { tag: "int" } & IntExpr + | { tag: "bool" } & BoolExpr + | { tag: "str" } & StringExpr + | { tag: "group" } & GroupExpr + | { tag: "array" } & ArrayExpr + | { tag: "repeat" } & RepeatExpr + | { tag: "struct" } & StructExpr + | { tag: "ref" } & RefExpr + | { tag: "deref" } & DerefExpr + | { tag: "elem" } & ElemExpr + | { tag: "field" } & FieldExpr + | { tag: "index" } & IndexExpr + | { tag: "call" } & CallExpr + | { tag: "unary" } & UnaryExpr + | { tag: "binary" } & BinaryExpr + | { tag: "block" } & BlockExpr + | { tag: "if" } & IfExpr + | { tag: "loop" } & LoopExpr + | { tag: "while" } & WhileExpr + | { tag: "for" } & ForExpr + | { tag: "c_for" } & CForExpr; + +export type PathExpr = { qty?: Ty; path: Path }; +export type IntExpr = { value: number }; +export type BoolExpr = { value: boolean }; +export type StringExpr = { value: string }; +export type GroupExpr = { expr: Expr }; +export type ArrayExpr = { exprs: Expr[] }; +export type RepeatExpr = { expr: Expr; length: Expr }; +export type StructExpr = { path?: Path; fields: ExprField[] }; +export type RefExpr = { expr: Expr; refType: RefType; mut: boolean }; +export type DerefExpr = { expr: Expr }; +export type ElemExpr = { expr: Expr; elem: number }; +export type FieldExpr = { expr: Expr; ident: Ident }; +export type IndexExpr = { expr: Expr; index: Expr }; +export type CallExpr = { expr: Expr; args: Expr[] }; +export type UnaryExpr = { unaryType: UnaryType; expr: Expr }; +export type BinaryExpr = { binaryType: BinaryType; left: Expr; right: Expr }; +export type BlockExpr = { block: Block }; +export type IfExpr = { cond: Expr; truthy: Expr; falsy?: Expr }; +export type LoopExpr = { body: Expr }; +export type WhileExpr = { cond: Expr; body: Expr }; +export type ForExpr = { pat: Pat; expr: Expr; body: Expr }; +export type CForExpr = { decl?: Stmt; cond?: Expr; incr?: Stmt; body: Expr }; + +export type RefType = "ref" | "ptr"; +export type UnaryType = "not" | "-"; +export type BinaryType = + | "+" + | "*" + | "==" + | "-" + | "/" + | "!=" + | "<" + | ">" + | "<=" + | ">=" + | "or" + | "and"; + +export type ExprField = { + ident: Ident; + expr: Expr; + span: Span; +}; + +export type Block = { + stmts: Stmt[]; + expr?: Expr; + span: Span; +}; + +export type Pat = { + id: AstId; + kind: PatKind; + span: Span; +}; + +export type PatKind = + | { tag: "error" } + | { tag: "bind" } & BindPat + | { tag: "path" } & PathPat; + +export type BindPat = { ident: Ident; mut: boolean }; +export type PathPat = { qty?: Ty; path: Path }; + +export type Ty = { + id: AstId; + kind: TyKind; + span: Span; +}; + +export type TyKind = + | { tag: "error" } + | { tag: "null" } + | { tag: "int" } + | { tag: "bool" } + | { tag: "str" } + | { tag: "path" } & PathTy + | { tag: "ref" } & RefTy + | { tag: "ptr" } & PtrTy + | { tag: "slice" } & SliceTy + | { tag: "array" } & ArrayTy + | { tag: "anon_struct" } & AnonStructTy; + +export type PathTy = { qty?: Ty; path: Path }; +export type RefTy = { ty: Ty; mut: boolean }; +export type PtrTy = { ty: Ty; mut: boolean }; +export type SliceTy = { ty: Ty }; +export type ArrayTy = { ty: Ty; length: Expr }; +export type TupleTy = { elems: Ty[] }; +export type AnonStructTy = { fields: AnonFieldDef[] }; + +export type AnonFieldDef = { + ident: Ident; + ty: Ty; + span: Span; +}; + +export type Path = { + segments: PathSegment[]; + span: Span; +}; + +export type PathSegment = { + ident: Ident; + genericArgs?: Ty[]; + span: Span; +}; + +export type Ident = { + id: IdentId; + text: string; + span: Span; +}; diff --git a/slige/compiler/ast/cx.ts b/slige/compiler/ast/cx.ts new file mode 100644 index 0000000..903aea6 --- /dev/null +++ b/slige/compiler/ast/cx.ts @@ -0,0 +1,53 @@ +import { Span } from "../diagnostics.ts"; +import { AstId, Ids } from "../ids.ts"; +import { + Expr, + ExprKind, + Ident, + Item, + ItemKind, + Pat, + PatKind, + Stmt, + StmtKind, + Ty, + TyKind, +} from "./ast.ts"; + +export class Cx { + private astIdGen = new Ids(); + + private id(): AstId { + return this.astIdGen.nextThenStep(); + } + + public stmt(kind: StmtKind, span: Span): Stmt { + const id = this.id(); + return { id, kind, span }; + } + + public item( + kind: ItemKind, + span: Span, + ident: Ident, + pub: boolean, + ): Item { + const id = this.id(); + return { id, kind, span, ident, pub }; + } + + public expr(kind: ExprKind, span: Span): Expr { + const id = this.id(); + return { id, kind, span }; + } + + public pat(kind: PatKind, span: Span): Pat { + const id = this.id(); + return { id, kind, span }; + } + + public ty(kind: TyKind, span: Span): Ty { + const id = this.id(); + return { id, kind, span }; + } +} diff --git a/slige/compiler/ast/mod.ts b/slige/compiler/ast/mod.ts new file mode 100644 index 0000000..980154c --- /dev/null +++ b/slige/compiler/ast/mod.ts @@ -0,0 +1,3 @@ +export * from "./ast.ts"; +export * from "./visitor.ts"; +export * from "./cx.ts"; diff --git a/slige/compiler/ast/to_string.ts b/slige/compiler/ast/to_string.ts new file mode 100644 index 0000000..e7289cf --- /dev/null +++ b/slige/compiler/ast/to_string.ts @@ -0,0 +1,33 @@ +import { Ctx } from "../ctx.ts"; +import { exhausted, todo } from "../util.ts"; +import { Block, Item } from "./ast.ts"; + +export function itemToString(ctx: Ctx, item: Item): string { + const ident = ctx.identText(item.ident.id); + const k = item.kind; + switch (k.tag) { + case "error": + return ``; + case "mod_block": { + const block = blockToString(ctx, k.block); + return `mod ${item} ${block}`; + } + case "mod_file": + return todo(); + case "enum": + return todo(); + case "struct": + return todo(); + case "fn": + return todo(); + case "use": + return todo(); + case "type_alias": + return todo(); + } + return exhausted(k); +} + +export function blockToString(ctx: Ctx, block: Block): string { + return todo(); +} diff --git a/slige/compiler/ast/visitor.ts b/slige/compiler/ast/visitor.ts new file mode 100644 index 0000000..c345eea --- /dev/null +++ b/slige/compiler/ast/visitor.ts @@ -0,0 +1,581 @@ +import { exhausted } from "../util.ts"; +import { + AnonStructTy, + ArrayExpr, + ArrayTy, + AssignStmt, + BinaryExpr, + BindPat, + Block, + BlockExpr, + BoolExpr, + BreakStmt, + CallExpr, + CForExpr, + DerefExpr, + ElemExpr, + EnumItem, + Expr, + ExprStmt, + FieldExpr, + File, + FnItem, + ForExpr, + Generics, + GroupExpr, + Ident, + IfExpr, + IndexExpr, + IntExpr, + Item, + ItemStmt, + LetStmt, + LoopExpr, + ModBlockItem, + ModFileItem, + Param, + Pat, + Path, + PathExpr, + PathPat, + PathTy, + PtrTy, + RefExpr, + RefTy, + RepeatExpr, + ReturnStmt, + SliceTy, + Stmt, + StringExpr, + StructExpr, + StructItem, + TupleTy, + Ty, + TypeAliasItem, + UnaryExpr, + UseItem, + Variant, + VariantData, + WhileExpr, +} from "./ast.ts"; + +export type VisitRes = "stop" | void; + +type R = VisitRes; +type PM = unknown[]; + +export interface Visitor< + P extends PM = [], +> { + visitFile?(file: File, ...p: P): R; + + visitStmt?(stmt: Stmt, ...p: P): R; + visitErrorStmt?(stmt: Stmt, ...p: P): R; + visitItemStmt?(stmt: Stmt, kind: ItemStmt, ...p: P): R; + visitLetStmt?(stmt: Stmt, kind: LetStmt, ...p: P): R; + visitReturnStmt?(stmt: Stmt, kind: ReturnStmt, ...p: P): R; + visitBreakStmt?(stmt: Stmt, kind: BreakStmt, ...p: P): R; + visitContinueStmt?(stmt: Stmt, ...p: P): R; + visitAssignStmt?(stmt: Stmt, kind: AssignStmt, ...p: P): R; + visitExprStmt?(stmt: Stmt, kind: ExprStmt, ...p: P): R; + + visitItem?(item: Item, ...p: P): R; + visitErrorItem?(item: Item, ...p: P): R; + visitModBlockItem?(item: Item, kind: ModBlockItem, ...p: P): R; + visitModFileItem?(item: Item, kind: ModFileItem, ...p: P): R; + visitEnumItem?(item: Item, kind: EnumItem, ...p: P): R; + visitStructItem?(item: Item, kind: StructItem, ...p: P): R; + visitFnItem?(item: Item, kind: FnItem, ...p: P): R; + visitUseItem?(item: Item, kind: UseItem, ...p: P): R; + visitTypeAliasItem?(item: Item, kind: TypeAliasItem, ...p: P): R; + + visitVariant?(variant: Variant, ...p: P): R; + + visitExpr?(expr: Expr, ...p: P): R; + visitErrorExpr?(expr: Expr, ...p: P): R; + visitPathExpr?(expr: Expr, kind: PathExpr, ...p: P): R; + visitNullExpr?(expr: Expr, ...p: P): R; + visitIntExpr?(expr: Expr, kind: IntExpr, ...p: P): R; + visitBoolExpr?(expr: Expr, kind: BoolExpr, ...p: P): R; + visitStringExpr?(expr: Expr, kind: StringExpr, ...p: P): R; + visitGroupExpr?(expr: Expr, kind: GroupExpr, ...p: P): R; + visitArrayExpr?(expr: Expr, kind: ArrayExpr, ...p: P): R; + visitRepeatExpr?(expr: Expr, kind: RepeatExpr, ...p: P): R; + visitStructExpr?(expr: Expr, kind: StructExpr, ...p: P): R; + visitRefExpr?(expr: Expr, kind: RefExpr, ...p: P): R; + visitDerefExpr?(expr: Expr, kind: DerefExpr, ...p: P): R; + visitElemExpr?(expr: Expr, kind: ElemExpr, ...p: P): R; + visitFieldExpr?(expr: Expr, kind: FieldExpr, ...p: P): R; + visitIndexExpr?(expr: Expr, kind: IndexExpr, ...p: P): R; + visitCallExpr?(expr: Expr, kind: CallExpr, ...p: P): R; + visitUnaryExpr?(expr: Expr, kind: UnaryExpr, ...p: P): R; + visitBinaryExpr?(expr: Expr, kind: BinaryExpr, ...p: P): R; + visitBlockExpr?(expr: Expr, kind: BlockExpr, ...p: P): R; + visitIfExpr?(expr: Expr, kind: IfExpr, ...p: P): R; + visitLoopExpr?(expr: Expr, kind: LoopExpr, ...p: P): R; + visitWhileExpr?(expr: Expr, kind: WhileExpr, ...p: P): R; + visitForExpr?(expr: Expr, kind: ForExpr, ...p: P): R; + visitCForExpr?(expr: Expr, kind: CForExpr, ...p: P): R; + + visitPat?(pat: Pat, ...p: P): R; + visitErrorPat?(pat: Pat, ...p: P): R; + visitBindPat?(pat: Pat, kind: BindPat, ...p: P): R; + visitPathPat?(pat: Pat, kind: PathPat, ...p: P): R; + + visitTy?(ty: Ty, ...p: P): R; + visitErrorTy?(ty: Ty, ...p: P): R; + visitNullTy?(ty: Ty, ...p: P): R; + visitIntTy?(ty: Ty, ...p: P): R; + visitBoolTy?(ty: Ty, ...p: P): R; + visitStrTy?(ty: Ty, ...p: P): R; + visitPathTy?(ty: Ty, kind: PathTy, ...p: P): R; + visitRefTy?(ty: Ty, kind: RefTy, ...p: P): R; + visitPtrTy?(ty: Ty, kind: PtrTy, ...p: P): R; + visitSliceTy?(ty: Ty, kind: SliceTy, ...p: P): R; + visitArrayTy?(ty: Ty, kind: ArrayTy, ...p: P): R; + visitTupleTy?(ty: Ty, kind: TupleTy, ...p: P): R; + visitAnonStructTy?(ty: Ty, kind: AnonStructTy, ...p: P): R; + + visitBlock?(block: Block, ...p: P): R; + visitPath?(path: Path, ...p: P): R; + visitIdent?(ident: Ident, ...p: P): R; +} + +export function visitFile< + P extends PM = [], +>( + v: Visitor

, + file: File, + ...p: P +) { + if (v.visitFile?.(file, ...p) === "stop") return; + visitStmts(v, file.stmts, ...p); +} + +export function visitStmts< + P extends PM = [], +>( + v: Visitor

, + stmts: Stmt[], + ...p: P +) { + for (const stmt of stmts) { + visitStmt(v, stmt, ...p); + } +} + +export function visitStmt< + P extends PM = [], +>( + v: Visitor

, + stmt: Stmt, + ...p: P +) { + const kind = stmt.kind; + switch (kind.tag) { + case "error": + if (v.visitErrorStmt?.(stmt, ...p) === "stop") return; + return; + case "item": + if (v.visitItemStmt?.(stmt, kind, ...p) === "stop") return; + visitItem(v, kind.item, ...p); + return; + case "let": + if (v.visitLetStmt?.(stmt, kind, ...p) === "stop") return; + visitPat(v, kind.pat, ...p); + if (kind.ty) { + visitTy(v, kind.ty, ...p); + } + if (kind.expr) { + visitExpr(v, kind.expr, ...p); + } + return; + case "return": + if (v.visitReturnStmt?.(stmt, kind, ...p) === "stop") return; + if (kind.expr) { + visitExpr(v, kind.expr, ...p); + } + return; + case "break": + if (v.visitBreakStmt?.(stmt, kind, ...p) === "stop") return; + kind.expr && visitExpr(v, kind.expr, ...p); + return; + case "continue": + if (v.visitContinueStmt?.(stmt, ...p) === "stop") return; + return; + case "assign": + if (v.visitAssignStmt?.(stmt, kind, ...p) === "stop") return; + visitExpr(v, kind.subject, ...p); + visitExpr(v, kind.value, ...p); + return; + case "expr": + if (v.visitExprStmt?.(stmt, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + return; + } + exhausted(kind); +} + +export function visitItem< + P extends PM = [], +>( + v: Visitor

, + item: Item, + ...p: P +) { + visitIdent(v, item.ident, ...p); + const kind = item.kind; + switch (kind.tag) { + case "error": + if (v.visitErrorItem?.(item, ...p) === "stop") return; + return; + case "mod_block": + if (v.visitModBlockItem?.(item, kind, ...p) === "stop") return; + visitBlock(v, kind.block, ...p); + return; + case "mod_file": + if (v.visitModFileItem?.(item, kind, ...p) === "stop") return; + return; + case "enum": + if (v.visitEnumItem?.(item, kind, ...p) === "stop") return; + for (const variant of kind.variants) { + visitVariant(v, variant, ...p); + } + return; + case "struct": + if (v.visitStructItem?.(item, kind, ...p) === "stop") return; + visitVariantData(v, kind.data, ...p); + return; + case "fn": + if (v.visitFnItem?.(item, kind, ...p) === "stop") return; + for (const param of kind.params) { + visitParam(v, param, ...p); + } + kind.returnTy && visitTy(v, kind.returnTy, ...p); + return; + case "use": + if (v.visitUseItem?.(item, kind, ...p) === "stop") return; + return; + case "type_alias": + if (v.visitTypeAliasItem?.(item, kind, ...p) === "stop") return; + visitTy(v, kind.ty, ...p); + return; + } + exhausted(kind); +} + +export function visitVariant< + P extends PM = [], +>( + v: Visitor

, + variant: Variant, + ...p: P +) { + if (v.visitVariant?.(variant, ...p) === "stop") return; + visitIdent(v, variant.ident, ...p); + visitVariantData(v, variant.data, ...p); +} + +export function visitVariantData< + P extends PM = [], +>( + v: Visitor

, + data: VariantData, + ...p: P +) { + const dk = data.kind; + switch (dk.tag) { + case "error": + return; + case "unit": + return; + case "tuple": + for (const elem of dk.elems) { + visitVariantData(v, elem, ...p); + } + return; + case "struct": + for (const field of dk.fields) { + visitIdent(v, field.ident, ...p); + visitTy(v, field.ty, ...p); + } + return; + } + exhausted(dk); +} + +export function visitGenerics< + P extends PM = [], +>( + v: Visitor

, + generics: Generics, + ...p: P +) { + for (const param of generics.params) { + visitIdent(v, param.ident, ...p); + } +} + +export function visitParam< + P extends PM = [], +>( + v: Visitor

, + param: Param, + ...p: P +) { + visitPat(v, param.pat, ...p); + visitTy(v, param.ty, ...p); +} + +export function visitExpr< + P extends PM = [], +>( + v: Visitor

, + expr: Expr, + ...p: P +) { + const kind = expr.kind; + switch (kind.tag) { + case "error": + if (v.visitErrorExpr?.(expr, ...p) === "stop") return; + return; + case "path": + if (v.visitPathExpr?.(expr, kind, ...p) === "stop") return; + visitPath(v, kind.path, ...p); + return; + case "null": + if (v.visitNullExpr?.(expr, ...p) === "stop") return; + return; + case "int": + if (v.visitIntExpr?.(expr, kind, ...p) === "stop") return; + return; + case "str": + if (v.visitStringExpr?.(expr, kind, ...p) === "stop") return; + return; + case "bool": + if (v.visitBoolExpr?.(expr, kind, ...p) === "stop") return; + return; + case "group": + if (v.visitGroupExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + return; + case "array": + if (v.visitArrayExpr?.(expr, kind, ...p) === "stop") return; + for (const expr of kind.exprs) { + visitExpr(v, expr, ...p); + } + return; + case "repeat": + if (v.visitRepeatExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + visitExpr(v, kind.length, ...p); + return; + case "struct": + if (v.visitStructExpr?.(expr, kind, ...p) === "stop") return; + if (kind.path) { + visitPath(v, kind.path, ...p); + } + for (const field of kind.fields) { + visitIdent(v, field.ident, ...p); + visitExpr(v, field.expr, ...p); + } + return; + case "ref": + if (v.visitRefExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + return; + case "deref": + if (v.visitDerefExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + return; + case "elem": + if (v.visitElemExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + return; + case "field": + if (v.visitFieldExpr?.(expr, kind, ...p) === "stop") return; + v.visitExpr?.(kind.expr, ...p); + v.visitIdent?.(kind.ident, ...p); + return; + case "index": + if (v.visitIndexExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + visitExpr(v, kind.index, ...p); + return; + case "call": + if (v.visitCallExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + for (const expr of kind.args) { + visitExpr(v, expr, ...p); + } + return; + case "unary": + if (v.visitUnaryExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.expr, ...p); + return; + case "binary": + if (v.visitBinaryExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.left, ...p); + visitExpr(v, kind.right, ...p); + return; + case "block": + if (v.visitBlockExpr?.(expr, kind, ...p) === "stop") return; + visitBlock(v, kind.block, ...p); + return; + case "if": + if (v.visitIfExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.cond, ...p); + visitExpr(v, kind.truthy, ...p); + if (kind.falsy) { + visitExpr(v, kind.falsy, ...p); + } + return; + case "loop": + if (v.visitLoopExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.body, ...p); + return; + case "while": + if (v.visitWhileExpr?.(expr, kind, ...p) === "stop") return; + visitExpr(v, kind.cond, ...p); + visitExpr(v, kind.body, ...p); + return; + case "for": + if (v.visitForExpr?.(expr, kind, ...p) === "stop") return; + visitPat(v, kind.pat, ...p); + visitExpr(v, kind.expr, ...p); + visitExpr(v, kind.body, ...p); + return; + case "c_for": + if (v.visitCForExpr?.(expr, kind, ...p) === "stop") return; + if (kind.decl) { + visitStmt(v, kind.decl, ...p); + } + if (kind.cond) { + visitExpr(v, kind.cond, ...p); + } + if (kind.incr) { + visitStmt(v, kind.incr, ...p); + } + return; + } + exhausted(kind); +} + +export function visitPat< + P extends PM = [], +>( + v: Visitor

, + pat: Pat, + ...p: P +) { + const kind = pat.kind; + switch (kind.tag) { + case "error": + if (v.visitErrorPat?.(pat, ...p) === "stop") return; + return; + case "bind": + if (v.visitBindPat?.(pat, kind, ...p) === "stop") return; + visitIdent(v, kind.ident, ...p); + return; + case "path": + if (v.visitPathPat?.(pat, kind, ...p) === "stop") return; + visitPath(v, kind.path, ...p); + return; + } + exhausted(kind); +} + +export function visitTy< + P extends PM = [], +>( + v: Visitor

, + ty: Ty, + ...p: P +) { + const kind = ty.kind; + switch (kind.tag) { + case "error": + if (v.visitErrorTy?.(ty, ...p) === "stop") return; + return; + case "null": + if (v.visitNullTy?.(ty, ...p) === "stop") return; + return; + case "int": + if (v.visitIntTy?.(ty, ...p) === "stop") return; + return; + case "bool": + if (v.visitBoolTy?.(ty, ...p) === "stop") return; + return; + case "str": + if (v.visitStrTy?.(ty, ...p) === "stop") return; + return; + case "path": + if (v.visitPathTy?.(ty, kind, ...p) === "stop") return; + v.visitPath?.(kind.path, ...p); + return; + case "ref": + if (v.visitRefTy?.(ty, kind, ...p) === "stop") return; + v.visitTy?.(kind.ty, ...p); + return; + case "ptr": + if (v.visitPtrTy?.(ty, kind, ...p) === "stop") return; + v.visitTy?.(kind.ty, ...p); + return; + case "slice": + if (v.visitSliceTy?.(ty, kind, ...p) === "stop") return; + v.visitTy?.(kind.ty, ...p); + return; + case "array": + if (v.visitArrayTy?.(ty, kind, ...p) === "stop") return; + v.visitTy?.(kind.ty, ...p); + v.visitExpr?.(kind.length, ...p); + return; + case "anon_struct": + if (v.visitAnonStructTy?.(ty, kind, ...p) === "stop") return; + for (const field of kind.fields) { + v.visitIdent?.(field.ident, ...p); + v.visitTy?.(field.ty, ...p); + } + return; + } + exhausted(kind); +} + +export function visitBlock< + P extends PM = [], +>( + v: Visitor

, + block: Block, + ...p: P +) { + if (v.visitBlock?.(block, ...p) === "stop") return; + for (const stmt of block.stmts) { + visitStmt(v, stmt, ...p); + } + if (block.expr) { + visitExpr(v, block.expr, ...p); + } +} + +export function visitPath< + P extends PM = [], +>( + v: Visitor

, + path: Path, + ...p: P +) { + if (v.visitPath?.(path, ...p) === "stop") return; + for (const seg of path.segments) { + visitIdent(v, seg.ident, ...p); + } +} + +export function visitIdent< + P extends PM = [], +>( + v: Visitor

, + ident: Ident, + ...p: P +) { + v.visitIdent?.(ident, ...p); +} diff --git a/slige/compiler/check/checker.ts b/slige/compiler/check/checker.ts new file mode 100644 index 0000000..20d880c --- /dev/null +++ b/slige/compiler/check/checker.ts @@ -0,0 +1,209 @@ +import * as ast from "../ast/mod.ts"; +import { Ctx, File } from "../ctx.ts"; +import { Span } from "../diagnostics.ts"; +import { AstId, IdMap } from "../ids.ts"; +import { Resols } from "../resolve/resolver.ts"; +import { tyToString } from "../ty/to_string.ts"; +import { Ty } from "../ty/ty.ts"; +import { exhausted, Res, todo } from "../util.ts"; + +export class Checker { + private itemTys = new IdMap(); + private exprTys = new IdMap(); + private tyTys = new IdMap(); + + private currentFile: File; + + public constructor( + private ctx: Ctx, + private entryFileAst: ast.File, + private resols: Resols, + ) { + this.currentFile = ctx.entryFile(); + } + + private checkBlock(block: ast.Block, expected: Ty): Ty { + this.checkStmts(block.stmts); + return block.expr && + this.checkExpr(block.expr, expected) || + Ty({ tag: "null" }); + } + + private checkStmts(stmts: ast.Stmt[]) { + } + + public fnItemTy(item: ast.Item, kind: ast.FnItem): Ty { + return this.itemTys.get(item.id) || this.checkFnItem(item, kind); + } + + private checkFnItem(item: ast.Item, kind: ast.FnItem): Ty { + const params = kind.params.map((param) => this.tyTy(param.ty)); + const returnTy = kind.returnTy && this.tyTy(kind.returnTy) || + Ty({ tag: "null" }); + return Ty({ tag: "fn", item, kind, params, returnTy }); + } + + public exprTy(expr: ast.Expr): Ty { + return this.exprTys.get(expr.id) || + this.checkExpr(expr, Ty({ tag: "unknown" })); + } + + private checkExpr(expr: ast.Expr, expected: Ty): Ty { + const k = expr.kind; + switch (k.tag) { + case "error": + return Ty({ tag: "error" }); + case "path": + return this.checkPathExpr(expr, k, expected); + case "null": + return todo(); + case "int": + return todo(); + case "bool": + return todo(); + case "str": + return todo(); + case "group": + return todo(); + case "array": + return todo(); + case "repeat": + return todo(); + case "struct": + return todo(); + case "ref": + return todo(); + case "deref": + return todo(); + case "elem": + return todo(); + case "field": + return todo(); + case "index": + return todo(); + case "call": + return todo(); + case "unary": + return todo(); + case "binary": + return todo(); + case "block": + return todo(); + case "if": + return todo(); + case "loop": + return todo(); + case "while": + return todo(); + case "for": + return todo(); + case "c_for": + return todo(); + } + exhausted(k); + } + + private checkPathExpr( + expr: ast.Expr, + kind: ast.PathExpr, + expected: Ty, + ): Ty { + const res = this.resols.exprRes(expr.id); + switch (res.kind.tag) { + case "error": + return Ty({ tag: "error" }); + case "fn": { + const fn = res.kind.item; + const ty = this.fnItemTy(fn, res.kind.kind); + const resu = this.resolveTys(ty, expected); + if (!resu.ok) { + this.report(resu.val, expr.span); + return Ty({ tag: "error" }); + } + return resu.val; + } + case "local": { + const ty = this.exprTy(expr); + const resu = this.resolveTys(ty, expected); + if (!resu.ok) { + this.report(resu.val, expr.span); + return Ty({ tag: "error" }); + } + return resu.val; + } + } + exhausted(res.kind); + } + + private tyTy(ty: ast.Ty): Ty { + return this.tyTys.get(ty.id) || + this.checkTy(ty); + } + + private checkTy(ty: ast.Ty): Ty { + const k = ty.kind; + switch (k.tag) { + case "error": + return Ty({ tag: "error" }); + case "null": + case "int": + return Ty({ tag: "int" }); + case "bool": + case "str": + case "path": + case "ref": + case "ptr": + case "slice": + case "array": + case "anon_struct": + return todo(k); + } + exhausted(k); + } + + private report(msg: string, span: Span) { + this.ctx.report({ + severity: "error", + file: this.currentFile, + span, + msg, + }); + } + + private resolveTys(a: Ty, b: Ty): Res { + const as = tyToString(this.ctx, a); + const bs = tyToString(this.ctx, b); + const incompat = () => + Res.Err( + `type '${as}' not compatible with type '${bs}'`, + ); + switch (a.kind.tag) { + case "error": + return Res.Ok(b); + case "unknown": + return Res.Ok(b); + case "null": { + if (b.kind.tag !== "null") { + return incompat(); + } + return Res.Ok(a); + } + case "int": { + if (b.kind.tag !== "int") { + return incompat(); + } + return Res.Ok(a); + } + case "fn": { + if (b.kind.tag !== "fn") { + return incompat(); + } + if (b.kind.item.id === a.kind.item.id) { + return incompat(); + } + return Res.Ok(a); + } + } + exhausted(a.kind); + } +} diff --git a/slige/compiler/ctx.ts b/slige/compiler/ctx.ts new file mode 100644 index 0000000..e53e5b6 --- /dev/null +++ b/slige/compiler/ctx.ts @@ -0,0 +1,141 @@ +import * as ast from "./ast/mod.ts"; +import { + Pos, + prettyPrintReport, + printStackTrace, + Report, + Span, +} from "./diagnostics.ts"; +import { DefId, File, IdentId, IdMap, Ids } from "./ids.ts"; +export { type File } from "./ids.ts"; + +export class Ctx { + private fileIds = new Ids(); + private files = new IdMap(); + private _entryFile?: File; + + private reports: Report[] = []; + + public fileHasChildWithIdent(file: File, childIdent: string): boolean { + return this.files.get(file)! + .subFiles.has(childIdent); + } + + public addFile( + ident: string, + absPath: string, + relPath: string, + superFile: File | undefined, + text: string, + ): File { + const file = this.fileIds.nextThenStep(); + this.files.set(file, { + ident, + absPath, + relPath, + superFile, + subFiles: new Map(), + text, + }); + this._entryFile = this._entryFile ?? file; + if (superFile) { + this.files.get(superFile)! + .subFiles.set(ident, file); + } + return file; + } + + public addFileAst(file: File, ast: ast.File) { + this.files.get(file)!.ast = ast; + } + + public fileInfo(file: File): FileInfo { + return this.files.get(file)!; + } + + public entryFile(): File { + if (!this._entryFile) { + throw new Error(); + } + return this._entryFile; + } + + public iterFiles(): IteratorObject { + return this.files.keys(); + } + + // + + private identIds = new Ids(); + private identStringToId = new Map(); + private identIdToString = new IdMap(); + + public internIdent(ident: string): IdentId { + if (this.identStringToId.has(ident)) { + return this.identStringToId.get(ident)!; + } + const id = this.identIds.nextThenStep(); + this.identStringToId.set(ident, id); + this.identIdToString.set(id, ident); + return id; + } + + public identText(ident: IdentId): string { + return this.identIdToString.get(ident)!; + } + + public filePosLineText(file: File, pos: Pos): string { + const fileTextLines = this.fileInfo(file).text.split("\n"); + return fileTextLines[pos.line - 1]; + } + + public fileSpanText(file: File, span: Span): string { + let result = ""; + const fileTextLines = this.fileInfo(file).text.split("\n"); + + for (let i = 0; i < fileTextLines.length; i++) { + if (i > span.end.line - 1) { + break; + } + if (i >= span.begin.line - 1) { + result += fileTextLines[i] + "\n"; + } + } + return result; + } + + // + + public report(rep: Report) { + this.reports.push(rep); + this.reportImmediately(rep); + } + + public enableReportImmediately = false; + public enableStacktrace = false; + private reportImmediately(rep: Report) { + if (this.enableReportImmediately) { + prettyPrintReport(this, rep); + if (this.enableStacktrace) { + printStackTrace(); + } + } + } + + public printAsts() { + for (const [_, info] of this.files) { + console.log(`${info.absPath}:`); + console.log(JSON.stringify(info.ast!, null, 2)); + } + } +} + +export type FileInfo = { + ident: string; + absPath: string; + relPath: string; + superFile?: File; + subFiles: Map; + text: string; + ast?: ast.File; +}; diff --git a/slige/compiler/deno.jsonc b/slige/compiler/deno.jsonc new file mode 100644 index 0000000..3fecf5f --- /dev/null +++ b/slige/compiler/deno.jsonc @@ -0,0 +1,5 @@ +{ + "fmt": { + "indentWidth": 4 + } +} diff --git a/slige/compiler/deno.lock b/slige/compiler/deno.lock new file mode 100644 index 0000000..9c112a1 --- /dev/null +++ b/slige/compiler/deno.lock @@ -0,0 +1,23 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/path@*": "1.0.8", + "npm:@types/node@*": "22.5.4" + }, + "jsr": { + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + } + }, + "npm": { + "@types/node@22.5.4": { + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dependencies": [ + "undici-types" + ] + }, + "undici-types@6.19.8": { + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + } + } +} diff --git a/slige/compiler/diagnostics.ts b/slige/compiler/diagnostics.ts new file mode 100644 index 0000000..35d7e6a --- /dev/null +++ b/slige/compiler/diagnostics.ts @@ -0,0 +1,138 @@ +import { Ctx, File } from "./ctx.ts"; +import { exhausted } from "./util.ts"; + +export type Span = { + begin: Pos; + end: Pos; +}; + +export type Pos = { + idx: number; + line: number; + col: number; +}; + +export const Span = { + fromto: ({ begin }: Span, { end }: Span): Span => ({ begin, end }), +} as const; + +export type Report = { + severity: "fatal" | "error" | "warning" | "info"; + origin?: string; + msg: string; + file?: File; + span?: Span; + pos?: Pos; +}; + +function severityColor(severity: "fatal" | "error" | "warning" | "info") { + switch (severity) { + case "fatal": + return "\x1b[1m\x1b[31m"; + case "error": + return "\x1b[1m\x1b[31m"; + case "warning": + return "\x1b[1m\x1b[33m"; + case "info": + return "\x1b[1m\x1b[34m"; + } + exhausted(severity); +} + +export function prettyPrintReport(ctx: Ctx, rep: Report) { + const { severity, msg } = rep; + const origin = rep.origin ? `\x1b[1m${rep.origin}:\x1b[0m ` : ""; + console.error( + `${origin}${ + severityColor(severity) + }${severity}:\x1b[0m \x1b[37m${msg}\x1b[0m`, + ); + if (rep.file && (rep.span || rep.pos)) { + const errorLineOffset = 2; + const { absPath: path } = ctx.fileInfo(rep.file); + const { line, col } = rep.span?.begin ?? rep.pos!; + console.error(` --> ./${path}:${line}:${col}`); + if (rep.span) { + const spanLines = ctx.fileSpanText(rep.file, rep.span).split("\n"); + spanLines.pop(); + if (spanLines.length == 1) { + console.error( + `${rep.span.begin.line.toString().padStart(4, " ")}| ${ + spanLines[0] + }`, + ); + console.error( + ` | ${severityColor(severity)}${ + " ".repeat(rep.span.begin.col) + }${ + "~".repeat(rep.span.end.col - rep.span.begin.col) + }\x1b[0m`, + ); + return; + } + for (let i = 0; i < spanLines.length; i++) { + console.error( + `${ + (rep.span.begin.line + i).toString().padStart(4, " ") + }| ${spanLines[i]}`, + ); + if (i == 0) { + console.error( + ` | ${" ".repeat(rep.span.begin.col - 1)}${ + severityColor(severity) + }${ + "~".repeat( + spanLines[i].length - (rep.span.begin.col - 1), + ) + }\x1b[0m`, + ); + } else if (i == spanLines.length - 1) { + console.error( + ` | ${severityColor(severity)}${ + "~".repeat(rep.span.end.col) + }\x1b[0m`, + ); + } else { + console.error( + ` | ${severityColor(severity)}${ + "~".repeat(spanLines[i].length) + }\x1b[0m`, + ); + } + } + } else if (rep.pos) { + console.error( + `${rep.pos.line.toString().padStart(4, " ")}| ${ + ctx.filePosLineText(rep.file, rep.pos) + }`, + ); + console.error( + ` | ${severityColor(severity)}${ + " ".repeat(rep.pos.col) + }^\x1b[0m`, + ); + } + } +} + +export function printStackTrace() { + class StackTracer extends Error { + constructor() { + super("StackTracer"); + } + } + try { + throw new StackTracer(); + } catch (error) { + if (!(error instanceof StackTracer)) { + throw error; + } + console.log( + error.stack?.replace( + "Error: StackTracer", + "Stack trace:", + ) ?? + error, + ); + } +} diff --git a/slige/compiler/ids.ts b/slige/compiler/ids.ts new file mode 100644 index 0000000..ec4af8c --- /dev/null +++ b/slige/compiler/ids.ts @@ -0,0 +1,91 @@ +// + +export type File = IdBase & { readonly _: unique symbol }; +export type IdentId = IdBase & { readonly _: unique symbol }; +export type AstId = IdBase & { readonly _: unique symbol }; + +export type DefId = IdBase & { readonly _: unique symbol }; + +// + +export type IdBase = { rawId: number }; + +export type IdRaw = IdType["rawId"]; + +export const idRaw = (id: IdType): IdRaw => + id.rawId; + +export const idFromRaw = ( + rawId: IdRaw, +): IdType => ({ rawId } as IdType); + +export class Ids { + private next = 0; + public nextThenStep(): IdType { + const rawId = this.next; + this.next += 1; + return idFromRaw(rawId); + } +} + +export class IdMap implements Map { + private map = new Map, V>(); + + set(id: Id, val: V) { + this.map.set(idRaw(id), val); + return this; + } + + get(id: Id): V | undefined { + return this.map.get(idRaw(id)); + } + + has(id: Id): boolean { + return this.map.has(idRaw(id)); + } + + keys(): MapIterator { + return this.map.keys() + .map((rawId) => idFromRaw(rawId)); + } + + clear(): void { + this.map.clear(); + } + + delete(id: Id): boolean { + return this.map.delete(idRaw(id)); + } + + forEach( + callbackfn: (value: V, key: Id, map: Map) => void, + thisArg?: unknown, + ): void { + this.map.forEach( + (value, key, _map) => callbackfn(value, idFromRaw(key), this), + thisArg, + ); + } + + get size(): number { + return this.map.size; + } + + entries(): MapIterator<[Id, V]> { + return this.map.entries() + .map(([rawId, v]) => [idFromRaw(rawId), v]); + } + + values(): MapIterator { + return this.map.values(); + } + + [Symbol.iterator](): MapIterator<[Id, V]> { + return this.map[Symbol.iterator]() + .map(([rawId, v]) => [idFromRaw(rawId), v]); + } + + get [Symbol.toStringTag](): string { + return this.map[Symbol.toStringTag]; + } +} diff --git a/slige/compiler/main.ts b/slige/compiler/main.ts new file mode 100644 index 0000000..73e3e81 --- /dev/null +++ b/slige/compiler/main.ts @@ -0,0 +1,134 @@ +import * as path from "jsr:@std/path"; +import { Parser } from "./parse/parser.ts"; +import * as ast from "./ast/mod.ts"; +import { Ctx, File } from "./ctx.ts"; +import { Resolver } from "./resolve/resolver.ts"; +import { Checker } from "./check/checker.ts"; +import { AstLowerer } from "./middle/ast_lower.ts"; + +async function main() { + const filePath = Deno.args[0]; + const compiler = new PackCompiler(filePath, new NullEmitter()); + compiler.enableDebug(); + await compiler.compile(); +} + +export type Pack = { + rootMod: Mod; +}; + +export type Mod = null; + +export interface PackEmitter { + emit(pack: Pack): void; +} + +export class NullEmitter implements PackEmitter { + emit(pack: Pack): void { + } +} + +export class PackCompiler { + private ctx = new Ctx(); + private astCx = new ast.Cx(); + + public constructor( + private entryFilePath: string, + private emitter: PackEmitter, + ) {} + + public async compile() { + const [entryFile, entryFileAst] = await FileTreeAstCollector + .fromEntryFile(this.ctx, this.astCx, this.entryFilePath) + .collect(); + const resols = new Resolver(this.ctx, entryFileAst).resolve(); + const checker = new Checker(this.ctx, entryFileAst, resols); + new AstLowerer(this.ctx, resols, checker, entryFileAst).lower(); + } + + public enableDebug() { + this.ctx.enableReportImmediately = true; + this.ctx.enableStacktrace = true; + } +} + +type _P = { file: File }; +export class FileTreeAstCollector implements ast.Visitor<[_P]> { + private subFilePromise = Promise.resolve(); + + private constructor( + private ctx: Ctx, + private astCx: ast.Cx, + private superFile: File | undefined, + private ident: string, + private absPath: string, + private relPath: string, + ) {} + + public static fromEntryFile( + ctx: Ctx, + astCx: ast.Cx, + entryFilePath: string, + ): FileTreeAstCollector { + return new FileTreeAstCollector( + ctx, + astCx, + undefined, + "root", + entryFilePath, + entryFilePath, + ); + } + + public async collect(): Promise<[File, ast.File]> { + const text = await Deno.readTextFile(this.absPath); + const file = this.ctx.addFile( + this.ident, + this.absPath, + this.relPath, + this.superFile, + text, + ); + const fileAst = new Parser(this.ctx, this.astCx, file).parse(); + this.ctx.addFileAst(file, fileAst); + ast.visitFile(this, fileAst, { file }); + await this.subFilePromise; + return [file, fileAst]; + } + + visitModFileItem( + item: ast.Item, + kind: ast.ModFileItem, + { file }: _P, + ): ast.VisitRes { + const ident = this.ctx.identText(item.ident.id); + const { filePath: relPath } = kind; + const absPath = path.join(path.dirname(this.absPath), relPath); + this.subFilePromise = this.subFilePromise + .then(async () => { + if (this.ctx.fileHasChildWithIdent(file, ident)) { + this.ctx.report({ + severity: "fatal", + msg: `module '${ident}' already declared`, + file, + span: item.span, + }); + Deno.exit(1); + } + const [modFile, modAst] = await new FileTreeAstCollector( + this.ctx, + this.astCx, + file, + ident, + absPath, + relPath, + ) + .collect(); + kind.file = modFile; + kind.ast = modAst; + }); + return "stop"; + } +} + +main(); diff --git a/slige/compiler/middle/ast_lower.ts b/slige/compiler/middle/ast_lower.ts new file mode 100644 index 0000000..b6299a9 --- /dev/null +++ b/slige/compiler/middle/ast_lower.ts @@ -0,0 +1,245 @@ +import * as ast from "../ast/mod.ts"; +import { Checker } from "../check/checker.ts"; +import { Ctx } from "../ctx.ts"; +import { IdMap, Ids } from "../ids.ts"; +import { LocalId as ReLocalId, Resols } from "../resolve/resolver.ts"; +import { Ty } from "../ty/ty.ts"; +import { exhausted, Res, todo } from "../util.ts"; +import { BinaryType, Operand, StmtKind, TerKind } from "./mir.ts"; +import { Block, BlockId, Fn, Local, LocalId, RVal, Stmt, Ter } from "./mir.ts"; + +export class AstLowerer implements ast.Visitor { + public constructor( + private ctx: Ctx, + private re: Resols, + private ch: Checker, + private ast: ast.File, + ) {} + + public lower() { + ast.visitFile(this, this.ast); + } + + visitFnItem(item: ast.Item, kind: ast.FnItem): ast.VisitRes { + new FnLowerer(this.ctx, this.re, this.ch, item, kind).lower(); + } +} + +export class FnLowerer { + private localIds = new Ids(); + private locals = new IdMap(); + + private blockIds = new Ids(); + private blocks = new IdMap(); + + private currentBlock?: Block; + + private reLocals = new IdMap(); + + public constructor( + private ctx: Ctx, + private re: Resols, + private ch: Checker, + private item: ast.Item, + private kind: ast.FnItem, + ) {} + + public lower(): Res { + const entry = this.pushBlock(); + + const fnTy = this.ch.fnItemTy(this.item, this.kind); + const returnPlace = this.local(fnTy); + const returnVal = this.lowerBlock(this.kind.body!); + + this.addStmt({ + tag: "assign", + place: { local: returnPlace, proj: [] }, + rval: returnVal, + }); + + this.setTer({ tag: "return" }); + + return Res.Ok({ + label: this.ctx.identText(this.item.ident.id), + locals: this.locals, + blocks: this.blocks, + entry, + }); + } + + private lowerBlock(block: ast.Block): RVal { + for (const stmt of block.stmts) { + this.lowerStmt(stmt); + } + return block.expr && this.lowerExpr(block.expr) || { + tag: "use", + operand: { tag: "const", val: { tag: "null" } }, + }; + } + + private lowerStmt(stmt: ast.Stmt) { + const k = stmt.kind; + switch (k.tag) { + case "error": + return { tag: "error" }; + case "item": + case "let": + case "return": + case "break": + case "continue": + case "assign": + case "expr": + return todo(); + } + exhausted(k); + } + + private lowerExpr(expr: ast.Expr): RVal { + const k = expr.kind; + switch (k.tag) { + case "error": + return { tag: "error" }; + case "path": + return this.lowerPathExpr(expr, k); + case "null": + case "int": + case "bool": + case "str": + case "group": + case "array": + case "repeat": + case "struct": + case "ref": + case "deref": + case "elem": + case "field": + case "index": + case "call": + case "unary": + return todo(k.tag); + case "binary": + return this.lowerBinaryExpr(expr, k); + case "block": + case "if": + case "loop": + case "while": + case "for": + case "c_for": + return todo(k.tag); + } + exhausted(k); + } + + private lowerPathExpr(expr: ast.Expr, kind: ast.PathExpr): RVal { + const re = this.re.exprRes(expr.id); + switch (re.kind.tag) { + case "error": + return { tag: "error" }; + case "fn": + return todo(); + case "local": { + const ty = this.ch.exprTy(expr); + const local = this.local(ty); + this.reLocals.set(re.kind.id, local); + const isCopyable = (() => { + switch (ty.kind.tag) { + case "error": + case "unknown": + return false; + case "null": + case "int": + return true; + case "fn": + return false; + } + exhausted(ty.kind); + })(); + return { + tag: "use", + operand: { + tag: isCopyable ? "copy" : "move", + place: { local, proj: [] }, + }, + }; + } + } + exhausted(re.kind); + } + + private lowerBinaryExpr(expr: ast.Expr, kind: ast.BinaryExpr): RVal { + const left = this.rvalAsOperand( + this.lowerExpr(kind.left), + this.ch.exprTy(kind.left), + ); + const right = this.rvalAsOperand( + this.lowerExpr(kind.right), + this.ch.exprTy(kind.right), + ); + const binaryType = ((kind): BinaryType => { + switch (kind.binaryType) { + case "+": + return "add"; + case "-": + return "sub"; + case "*": + return "mul"; + case "/": + return "div"; + case "==": + return "eq"; + case "!=": + return "ne"; + case "<": + return "lt"; + case ">": + return "lte"; + case "<=": + return "lte"; + case ">=": + return "gte"; + case "or": + return "or"; + case "and": + return "and"; + } + return todo(kind.binaryType); + })(kind); + return { tag: "binary", binaryType, left, right }; + } + + private rvalAsOperand(rval: RVal, ty: Ty): Operand { + const local = this.local(ty); + this.addStmt({ tag: "assign", place: { local, proj: [] }, rval }); + return { tag: "move", place: { local, proj: [] } }; + } + + private local(ty: Ty): LocalId { + const id = this.localIds.nextThenStep(); + this.locals.set(id, { id, ty }); + return id; + } + + private pushBlock(): BlockId { + const id = this.blockIds.nextThenStep(); + const block: Block = { + id, + stmts: [], + terminator: { kind: { tag: "unset" } }, + }; + this.blocks.set(id, block); + this.currentBlock = block; + return id; + } + + private setTer(kind: TerKind) { + this.currentBlock!.terminator = { kind }; + } + + private addStmt(kind: StmtKind) { + this.currentBlock!.stmts.push({ kind }); + } +} + +function unpack(blocks: Block[], val: [Block[], RVal]): [Block[], RVal] { + return [[...blocks, ...val[0]], val[1]]; +} diff --git a/slige/compiler/middle/mir.ts b/slige/compiler/middle/mir.ts new file mode 100644 index 0000000..b91747c --- /dev/null +++ b/slige/compiler/middle/mir.ts @@ -0,0 +1,115 @@ +import { Span } from "../diagnostics.ts"; +import { IdBase, IdMap } from "../ids.ts"; +import { Ty } from "../ty/ty.ts"; + +export type Fn = { + label: string; + locals: IdMap; + blocks: IdMap; + entry: BlockId; +}; + +export type LocalId = IdBase & { readonly _: unique symbol }; + +export type Local = { + id: LocalId; + ty: Ty; +}; + +export type BlockId = IdBase & { readonly _: unique symbol }; + +export type Block = { + id: BlockId; + stmts: Stmt[]; + terminator: Ter; +}; + +export type Stmt = { + kind: StmtKind; +}; + +export type StmtKind = + | { tag: "error" } + | { tag: "assign"; place: Place; rval: RVal } + | { tag: "fake_read"; place: Place } + | { tag: "deinit"; place: Place } + | { tag: "live"; local: LocalId } + | { tag: "dead"; local: LocalId } + | { tag: "mention"; place: Place }; + +export type Ter = { + kind: TerKind; +}; + +export type TerKind = + | { tag: "unset" } + | { tag: "goto"; target: BlockId } + | { + tag: "switch"; + discr: Operand; + targets: SwitchTarget[]; + otherwise: BlockId; + } + | { tag: "return" } + | { tag: "unreachable" } + | { tag: "drop"; place: Place; target: BlockId } + | { tag: "call"; func: Operand; args: Operand[]; dest: Operand }; + +export type SwitchTarget = { + value: number; + target: BlockId; +}; + +export type Place = { + local: LocalId; + proj: ProjElem[]; +}; + +// https://doc.rust-lang.org/beta/nightly-rustc/rustc_middle/mir/type.PlaceElem.html +export type ProjElem = + | { tag: "deref" } + | { tag: "repeat" } + | { tag: "field"; fieldIdx: number } + | { tag: "index"; local: LocalId } + | { tag: "downcast"; variantIdx: number }; + +// https://doc.rust-lang.org/beta/nightly-rustc/rustc_middle/mir/enum.Rvalue.html +export type RVal = + | { tag: "error" } + | { tag: "use"; operand: Operand } + | { tag: "repeat"; operand: Operand; length: Const } + | { tag: "ref"; place: Place; mut: boolean } + | { tag: "ptr"; place: Place; mut: boolean } + | { tag: "binary"; binaryType: BinaryType; left: Operand; right: Operand } + | { tag: "unary"; unaryType: UnaryType; operand: Operand }; + +export type BinaryType = + | "add" + | "sub" + | "mul" + | "div" + | "rem" + | "xor" + | "and" + | "or" + | "shl" + | "shr" + | "eq" + | "ne" + | "lt" + | "lte" + | "gt" + | "gte"; + +export type UnaryType = "not" | "neg"; + +export type Operand = + | { tag: "copy"; place: Place } + | { tag: "move"; place: Place } + | { tag: "const"; val: Const }; + +export type Const = + | { tag: "null" } + | { tag: "int"; value: number } + | { tag: "bool"; value: boolean } + | { tag: "string"; value: string }; diff --git a/slige/compiler/parse/lexer.ts b/slige/compiler/parse/lexer.ts new file mode 100644 index 0000000..906f363 --- /dev/null +++ b/slige/compiler/parse/lexer.ts @@ -0,0 +1,339 @@ +import { Ctx, File } from "../ctx.ts"; +import { Pos, Span } from "../diagnostics.ts"; +import { ControlFlow, range } from "../util.ts"; +import { Token, TokenIter } from "./token.ts"; + +export class Lexer implements TokenIter { + private idx = 0; + private line = 1; + private col = 1; + + private text: string; + + public constructor( + private ctx: Ctx, + private file: File, + ) { + this.text = ctx.fileInfo(file).text; + } + + next(): Token | null { + if (this.done()) { + return null; + } + let cf: ControlFlow; + if ( + cf = this.lexWithTail( + (span) => this.token("whitespace", span), + /[ \t\r\n]/, + ), cf.break + ) { + return cf.val; + } + if ( + cf = this.lexWithTail( + (span, val) => { + return keywords.has(val) + ? this.token(val, span) + : this.token("ident", span, { + type: "ident", + identId: this.ctx.internIdent(val), + identText: val, + }); + }, + /[a-zA-Z_]/, + /[a-zA-Z0-9_]/, + ), cf.break + ) { + return cf.val; + } + if ( + cf = this.lexWithTail( + (span, val) => + this.token("int", span, { + type: "int", + intValue: parseInt(val), + }), + /[1-9]/, + /[0-9]/, + ), cf.break + ) { + return cf.val; + } + const begin = this.pos(); + let end = begin; + const pos = begin; + if (this.test("0")) { + this.step(); + if (!this.done() && this.test(/[0-9]/)) { + this.report("invalid number", pos); + return this.token("error", { begin, end }); + } + return this.token("int", { begin, end }, { + type: "int", + intValue: 0, + }); + } + + if (this.test("'")) { + this.step(); + let value: string; + if (this.test("\\")) { + this.step(); + if (this.done()) { + this.report("malformed character literal", pos); + return this.token("error", { begin, end }); + } + value = { + n: "\n", + t: "\t", + "0": "\0", + }[this.current()] ?? this.current(); + } else { + value = this.current(); + } + this.step(); + if (this.done() || !this.test("'") || value.length === 0) { + this.report("malformed character literal", pos); + return this.token("error", { begin, end }); + } + this.step(); + return this.token("int", { begin, end }, { + type: "int", + intValue: value.charCodeAt(0), + }); + } + + if (this.test('"')) { + this.step(); + let value = ""; + while (!this.done() && !this.test('"')) { + if (this.test("\\")) { + this.step(); + if (this.done()) { + break; + } + value += { + n: "\n", + t: "\t", + "0": "\0", + }[this.current()] ?? this.current(); + } else { + value += this.current(); + } + this.step(); + } + if (this.done() || !this.test('"')) { + this.report("unclosed/malformed string", pos); + return this.token("error", { begin, end }); + } + this.step(); + return this.token("str", { begin, end }, { + type: "str", + stringValue: value, + }); + } + + if (this.test("/")) { + this.step(); + + if (this.test("/")) { + while (!this.done() && !this.test("\n")) { + end = this.pos(); + this.step(); + } + return this.token("comment", { begin, end }); + } + + if (this.test("*")) { + end = this.pos(); + this.step(); + let depth = 1; + let last: string | undefined = undefined; + while (!this.done() && depth > 0) { + if (last === "*" && this.current() === "/") { + depth -= 1; + last = undefined; + } else if (last === "/" && this.current() === "*") { + depth += 1; + last = undefined; + } else { + last = this.current(); + } + end = this.pos(); + this.step(); + } + if (depth !== 0) { + this.report("unclosed/malformed multiline comment", pos); + return this.token("comment", { begin, end }); + } + } + + return this.token("/", { begin, end }); + } + + const match = this.text.slice(this.idx).match( + new RegExp(`^(${ + staticTokenRes + .map((tok) => tok.length > 1 ? `(?:${tok})` : tok) + .join("|") + })`), + ); + if (match) { + for (const _ of range(match[1].length)) { + end = this.pos(); + this.step(); + } + return this.token(match[1], { begin, end }); + } + + this.report(`illegal character '${this.current()}'`, pos); + this.step(); + return this.next(); + } + + private lexWithTail( + builder: (span: Span, val: string) => R, + startPat: RegExp, + tailPat = startPat, + ): ControlFlow { + const begin = this.pos(); + if (!this.test(startPat)) { + return ControlFlow.Continue(undefined); + } + let end = begin; + let val = this.current(); + this.step(); + while (this.test(tailPat)) { + end = begin; + val += this.current(); + this.step(); + } + return ControlFlow.Break(builder({ begin, end }, val)); + } + + private done(): boolean { + return this.idx >= this.text.length; + } + + private current(): string { + return this.text[this.idx]; + } + + private step() { + if (this.done()) { + return; + } + if (this.current() === "\n") { + this.line += 1; + this.col = 1; + } else { + this.col += 1; + } + this.idx += 1; + } + + private pos(): Pos { + return { + idx: this.idx, + line: this.line, + col: this.col, + }; + } + + private token(type: string, span: Span, token?: Partial): Token { + const length = span.end.idx - span.begin.idx + 1; + return { type, span, length, ...token }; + } + + private test(pattern: RegExp | string): boolean { + if (this.done()) { + return false; + } + if (typeof pattern === "string") { + return this.current() === pattern; + } else if (pattern.source.startsWith("^")) { + return pattern.test(this.text.slice(this.idx)); + } else { + return pattern.test(this.current()); + } + } + + private report(msg: string, pos: Pos) { + this.ctx.report({ + severity: "error", + origin: "parser", + file: this.file, + msg, + pos, + }); + } +} + +const keywords = new Set([ + "false", + "true", + "null", + "int", + "bool", + "str", + "return", + "break", + "continue", + "let", + "mut", + "fn", + "loop", + "if", + "else", + "struct", + "enum", + "or", + "and", + "not", + "while", + "for", + "in", + "mod", + "pub", + "use", + "type_alias", +]); + +const staticTokens = [ + "=", + "==", + "<", + "<=", + ">", + ">=", + "-", + "->", + "!", + "!=", + "+", + "+=", + "-=", + ":", + "::", + "::<", + "(", + ")", + "{", + "}", + "[", + "]", + "<", + ">", + ".", + ",", + ":", + ";", + "#", + "&", + "0", +] as const; + +const staticTokenRes = staticTokens + .toSorted((a, b) => b.length - a.length) + .map((tok) => tok.split("").map((c) => `\\${c}`).join("")); diff --git a/slige/compiler/parse/parser.ts b/slige/compiler/parse/parser.ts new file mode 100644 index 0000000..6caa4ec --- /dev/null +++ b/slige/compiler/parse/parser.ts @@ -0,0 +1,1271 @@ +import { + AnonFieldDef, + BinaryType, + ExprField, + PathSegment, + RefType, + UnaryType, +} from "../ast/ast.ts"; +import { + AssignType, + Block, + Expr, + ExprKind, + File, + GenericParam, + Ident, + Item, + ItemKind, + Param, + Pat, + Path, + PatKind, + Stmt, + StmtKind, + Ty, + TyKind, +} from "../ast/ast.ts"; +import { Cx } from "../ast/cx.ts"; +import { Ctx, File as CtxFile } from "../ctx.ts"; +import { Pos, Span } from "../diagnostics.ts"; +import { Res, todo } from "../util.ts"; +import { Lexer } from "./lexer.ts"; +import { TokenIter } from "./token.ts"; +import { SigFilter } from "./token.ts"; +import { Token } from "./token.ts"; + +type ParseRes = Res; + +type StmtDetails = { + pub: boolean; + annos: { ident: Ident; args: Expr[]; pos: Span }[]; +}; + +export class Parser { + private lexer: TokenIter; + private currentToken: Token | null; + + public constructor( + private ctx: Ctx, + private cx: Cx, + private file: CtxFile, + ) { + this.lexer = new SigFilter(new Lexer(this.ctx, this.file)); + this.currentToken = this.lexer.next(); + } + + public parse(): File { + return { stmts: this.parseStmts(), file: this.file }; + } + + private parseStmts(): Stmt[] { + const stmts: Stmt[] = []; + while (!this.done()) { + stmts.push(this.parseStmt()); + } + return stmts; + } + + private parseStmt(): Stmt { + if ( + ["#", "pub", "mod", "fn"].some((tt) => this.test(tt)) + ) { + return this.parseItemStmt(); + } else if ( + ["let", "type_alias", "return", "break"].some((tt) => this.test(tt)) + ) { + const expr = this.parseSingleLineBlockStmt(); + this.eatSemicolon(); + return expr; + } else if ( + ["{", "if", "loop", "while", "for"].some((tt) => this.test(tt)) + ) { + const expr = this.parseMultiLineBlockExpr(); + return (this.stmt({ tag: "expr", expr }, expr.span)); + } else { + const expr = this.parseAssign(); + this.eatSemicolon(); + return expr; + } + } + + private parseMultiLineBlockExpr(): Expr { + const begin = this.span(); + if (this.test("{")) { + return this.parseBlockExpr(); + } + if (this.test("if")) { + return this.parseIf(); + } + if (this.test("loop")) { + return this.parseLoop(); + } + if (this.test("while")) { + return this.parseWhile(); + } + if (this.test("for")) { + return this.parseFor(); + } + this.report("expected expr"); + return this.expr({ tag: "error" }, begin); + } + + private parseSingleLineBlockStmt(): Stmt { + const begin = this.span(); + if (this.test("let")) { + return this.parseLet(); + } + if (this.test("type_alias")) { + return this.parseTypeAlias(); + } + if (this.test("return")) { + return this.parseReturn(); + } + if (this.test("break")) { + return this.parseBreak(); + } + this.report("expected stmt"); + return this.stmt({ tag: "error" }, begin); + } + + private eatSemicolon() { + if (!this.test(";")) { + this.report( + `expected ';', got '${this.currentToken?.type ?? "eof"}'`, + ); + return; + } + this.step(); + } + + private parseExpr(): Expr { + return this.parseBinary(); + } + + private parseBlock(): ParseRes { + const begin = this.span(); + this.step(); + const stmts: Stmt[] = []; + while (!this.done()) { + if (this.test("}")) { + const span = Span.fromto(begin, this.span()); + this.step(); + return Res.Ok({ stmts, span }); + } else if ( + ["#", "pub", "mod", "fn"].some((tt) => this.test(tt)) + ) { + stmts.push(this.parseItemStmt()); + } else if ( + ["let", "type_alias", "return", "break"] + .some((tt) => this.test(tt)) + ) { + stmts.push(this.parseSingleLineBlockStmt()); + this.eatSemicolon(); + } else if ( + ["{", "if", "loop", "while", "for"].some((tt) => this.test(tt)) + ) { + const expr = this.parseMultiLineBlockExpr(); + const span = expr.span; + if (this.test("}")) { + this.step(); + return Res.Ok({ stmts, expr, span }); + } + stmts.push(this.stmt({ tag: "expr", expr }, span)); + } else { + const expr = this.parseExpr(); + if (this.test("=") || this.test("+=") || this.test("-=")) { + const assignType = this.current().type as AssignType; + this.step(); + const value = this.parseExpr(); + this.eatSemicolon(); + stmts.push( + this.stmt( + { + tag: "assign", + assignType, + subject: expr, + value, + }, + Span.fromto(expr.span, value.span), + ), + ); + } else if (this.test(";")) { + this.step(); + stmts.push(this.stmt({ tag: "expr", expr }, expr.span)); + } else if (this.test("}")) { + this.step(); + return Res.Ok({ stmts, expr, span: expr.span }); + } else { + this.report("expected ';' or '}'"); + return Res.Err(undefined); + } + } + } + this.report("expected '}'"); + return Res.Err(undefined); + } + + private parseBlockExpr(): Expr { + const block = this.parseBlock(); + return block.ok + ? this.expr({ tag: "block", block: block.val }, this.span()) + : this.expr({ tag: "error" }, this.span()); + } + + private parseItemStmt( + pos = this.span(), + details: StmtDetails = { + pub: false, + annos: [], + }, + ): Stmt { + const sbegin = this.span(); + if (this.test("#") && !details.pub) { + this.step(); + if (!this.test("[")) { + this.report("expected '['"); + return this.stmt({ tag: "error" }, sbegin); + } + this.step(); + if (!this.test("ident")) { + this.report("expected 'ident'"); + return this.stmt({ tag: "error" }, sbegin); + } + const ident = this.parseIdent(); + const args: Expr[] = []; + if (this.test("(")) { + this.step(); + if (!this.done() && !this.test(")")) { + args.push(this.parseExpr()); + while (this.test(",")) { + this.step(); + if (this.done() || this.test(")")) { + break; + } + args.push(this.parseExpr()); + } + } + if (!this.test(")")) { + this.report("expected ')'"); + return this.stmt({ tag: "error" }, sbegin); + } + this.step(); + } + if (!this.test("]")) { + this.report("expected ']'"); + return this.stmt({ tag: "error" }, sbegin); + } + this.step(); + const anno = { ident, args, pos: sbegin }; + return this.parseItemStmt(pos, { + ...details, + annos: [...details.annos, anno], + }); + } else if (this.test("pub") && !details.pub) { + this.step(); + return this.parseItemStmt(pos, { ...details, pub: true }); + } else if (this.test("mod")) { + return this.parseMod(details); + } else if (this.test("fn")) { + return this.parseFn(details); + } else { + this.report("expected item statement"); + return this.stmt({ tag: "error" }, pos); + } + } + + private parseMod(details: StmtDetails): Stmt { + const pos = this.span(); + this.step(); + if (!this.test("ident")) { + this.report("expected 'ident'"); + return this.stmt({ tag: "error" }, pos); + } + const ident = this.parseIdent(); + if (this.test("str")) { + const filePath = this.current().stringValue!; + this.step(); + this.eatSemicolon(); + return this.stmt({ + tag: "item", + item: this.item( + { tag: "mod_file", filePath }, + pos, + ident, + details.pub, + ), + }, pos); + } + + if (!this.test("{")) { + this.report("expected '{' or 'string'"); + return this.stmt({ tag: "error" }, pos); + } + this.step(); + + const stmts: Stmt[] = []; + while (!this.done() && !this.test("}")) { + stmts.push(this.parseStmt()); + } + + if (!this.test("}")) { + this.report("expected '}'"); + return this.stmt({ tag: "error" }, pos); + } + this.step(); + + return this.stmt({ + tag: "item", + item: this.item( + { + tag: "mod_block", + block: { + stmts, + span: Span.fromto(pos, stmts.at(-1)?.span ?? pos), + }, + }, + pos, + ident, + details.pub, + ), + }, pos); + } + + private parseFn(details: StmtDetails): Stmt { + const pos = this.span(); + this.step(); + if (!this.test("ident")) { + this.report("expected ident"); + return this.stmt({ tag: "error" }, pos); + } + const ident = this.parseIdent(); + let genericParams: GenericParam[] | undefined; + if (this.test("<")) { + genericParams = this.parseFnTyParams(); + } + if (!this.test("(")) { + this.report("expected '('"); + return this.stmt({ tag: "error" }, pos); + } + const params = this.parseFnParams(); + let returnTy: Ty | undefined; + if (this.test("->")) { + this.step(); + returnTy = this.parseTy(); + } + + if (!this.test("{")) { + this.report("expected block"); + return this.stmt({ tag: "error" }, pos); + } + const blockRes = this.parseBlock(); + if (!blockRes.ok) { + return this.stmt({ tag: "error" }, this.span()); + } + const body = blockRes.val; + return this.stmt({ + tag: "item", + item: this.item( + { + tag: "fn", + params, + returnTy, + body, + ...(genericParams + ? { generics: { params: genericParams } } + : {}), + }, + pos, + ident, + details.pub, + ), + }, pos); + } + + private parseFnTyParams(): GenericParam[] { + return this.parseDelimitedList(this.parseTyParam, ">", ","); + } + + private parseTyParam(_index: number): ParseRes { + const span = this.span(); + if (this.test("ident")) { + const ident = this.parseIdent(); + return Res.Ok({ ident, span }); + } + this.report("expected generic parameter"); + return Res.Err(undefined); + } + + private parseFnParams(): Param[] { + return this.parseDelimitedList(this.parseParam, ")", ","); + } + + private parseDelimitedList( + parseElem: (this: Parser, index: number) => ParseRes, + endToken: string, + delimiter: string, + ): T[] { + this.step(); + if (this.test(endToken)) { + this.step(); + return []; + } + let i = 0; + const elems: T[] = []; + const elemRes = parseElem.call(this, i); + if (!elemRes.ok) { + return []; + } + elems.push(elemRes.val); + i += 1; + while (this.test(delimiter)) { + this.step(); + if (this.test(endToken)) { + break; + } + const elemRes = parseElem.call(this, i); + if (!elemRes.ok) { + return []; + } + elems.push(elemRes.val); + i += 1; + } + if (!this.test(endToken)) { + this.report(`expected '${endToken}'`); + return elems; + } + this.step(); + return elems; + } + + private parseParam(): ParseRes { + const begin = this.span(); + const pat = this.parsePat(); + if (!this.test(":")) { + this.report("expected ':'"); + return Res.Err(undefined); + } + this.step(); + const ty = this.parseTy(); + return Res.Ok({ + pat, + ty, + span: Span.fromto(begin, ty.span), + }); + } + + private parseLet(): Stmt { + const pos = this.span(); + this.step(); + const pat = this.parsePat(); + let ty: Ty | undefined = undefined; + if (this.test(":")) { + this.step(); + ty = this.parseTy(); + } + if (!this.test("=")) { + this.report("expected '='"); + return this.stmt({ tag: "error" }, pos); + } + this.step(); + const expr = this.parseExpr(); + return this.stmt({ tag: "let", pat, ty, expr }, pos); + } + + private parseTypeAlias(): Stmt { + const begin = this.span(); + this.step(); + if (!this.test("ident")) { + this.report("expected ident"); + return this.stmt({ tag: "error" }, begin); + } + const ident = this.parseIdent(); + if (!this.test("=")) { + this.report("expected '='"); + return this.stmt({ tag: "error" }, begin); + } + this.step(); + const ty = this.parseTy(); + return this.stmt({ + tag: "item", + item: this.item( + { + tag: "type_alias", + ty, + }, + Span.fromto(begin, ty.span), + ident, + false, + ), + }, begin); + } + + private parseAssign(): Stmt { + const pos = this.span(); + const subject = this.parseExpr(); + if (this.test("=") || this.test("+=") || this.test("-=")) { + const assignType = this.current().type as AssignType; + this.step(); + const value = this.parseExpr(); + return this.stmt({ + tag: "assign", + assignType, + subject, + value, + }, pos); + } + return this.stmt({ tag: "expr", expr: subject }, pos); + } + + private parseReturn(): Stmt { + const pos = this.span(); + this.step(); + if (this.test(";")) { + return this.stmt({ tag: "return" }, pos); + } + const expr = this.parseExpr(); + return this.stmt({ tag: "return", expr }, pos); + } + + private parseBreak(): Stmt { + const pos = this.span(); + this.step(); + if (this.test(";")) { + return this.stmt({ tag: "break" }, pos); + } + const expr = this.parseExpr(); + return this.stmt({ tag: "break", expr }, pos); + } + + private parseLoop(): Expr { + const pos = this.span(); + this.step(); + if (!this.test("{")) { + this.report("expected '{'"); + return this.expr({ tag: "error" }, pos); + } + const body = this.parseExpr(); + return this.expr({ tag: "loop", body }, pos); + } + + private parseWhile(): Expr { + const pos = this.span(); + this.step(); + const cond = this.parseExpr(); + if (!this.test("{")) { + this.report("expected '{'"); + return this.expr({ tag: "error" }, pos); + } + const body = this.parseExpr(); + return this.expr({ tag: "while", cond, body }, pos); + } + + private parseFor(): Expr { + const pos = this.span(); + this.step(); + + if (this.test("(")) { + return this.parseForClassicTail(pos); + } + + const pat = this.parsePat(); + + if (!this.test("in")) { + this.report("expected 'in'"); + return this.expr({ tag: "error" }, pos); + } + this.step(); + const expr = this.parseExpr(); + + if (!this.test("{")) { + this.report("expected '{'"); + return this.expr({ tag: "error" }, pos); + } + const body = this.parseExpr(); + return this.expr({ tag: "for", pat, expr, body }, pos); + } + + private parseForClassicTail(begin: Span): Expr { + this.step(); + let decl: Stmt | undefined; + if (!this.test(";")) { + decl = this.parseLet(); + } + if (!this.test(";")) { + this.report("expected ';'"); + return this.expr({ tag: "error" }, begin); + } + this.step(); + let cond: Expr | undefined; + if (!this.test(";")) { + cond = this.parseExpr(); + } + if (!this.test(";")) { + this.report("expected ';'"); + return this.expr({ tag: "error" }, begin); + } + this.step(); + let incr: Stmt | undefined; + if (!this.test(")")) { + incr = this.parseAssign(); + } + if (!this.test(")")) { + this.report("expected '}'"); + return this.expr({ tag: "error" }, begin); + } + this.step(); + + if (!this.test("{")) { + this.report("expected '{'"); + return this.expr({ tag: "error" }, begin); + } + const body = this.parseExpr(); + return this.expr( + { tag: "c_for", decl, cond, incr, body }, + Span.fromto(begin, body.span), + ); + } + + private parseArray(): Expr { + const pos = this.span(); + 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({ tag: "error" }, pos); + } + this.step(); + return this.expr({ tag: "array", exprs }, pos); + } + + private parseStruct(): Expr { + const pos = this.span(); + this.step(); + if (!this.test("{")) { + this.report("expected '{'"); + return this.expr({ tag: "error" }, pos); + } + this.step(); + const fields: ExprField[] = []; + if (!this.test("}")) { + const res = this.parseStructField(); + if (!res.ok) { + return this.expr({ tag: "error" }, this.span()); + } + fields.push(res.val); + while (this.test(",")) { + this.step(); + if (this.done() || this.test("}")) { + break; + } + const res = this.parseStructField(); + if (!res.ok) { + return this.expr({ tag: "error" }, this.span()); + } + fields.push(res.val); + } + } + if (!this.test("}")) { + this.report("expected '}'"); + return this.expr({ tag: "error" }, pos); + } + this.step(); + return this.expr({ tag: "struct", fields }, pos); + } + + private parseStructField(): ParseRes { + const span = this.span(); + if (!this.test("ident")) { + this.report("expected 'ident'"); + return Res.Err(undefined); + } + const ident = this.parseIdent(); + if (!this.test(":")) { + this.report("expected ':'"); + return Res.Err(undefined); + } + this.step(); + const expr = this.parseExpr(); + return Res.Ok({ ident, expr, span }); + } + + private parseIf(): Expr { + const pos = this.span(); + this.step(); + const cond = this.parseExpr(); + if (!this.test("{")) { + this.report("expected block"); + return this.expr({ tag: "error" }, pos); + } + const truthy = this.parseExpr(); + if (!this.test("else")) { + return this.expr({ tag: "if", cond, truthy }, pos); + } + //const elsePos = this.span(); + this.step(); + if (this.test("if")) { + const falsy = this.parseIf(); + return this.expr({ tag: "if", cond, truthy, falsy }, pos); + } + if (!this.test("{")) { + this.report("expected block"); + return this.expr({ tag: "error" }, pos); + } + const falsy = this.parseBlockExpr(); + return this.expr({ tag: "if", cond, truthy, falsy }, pos); + } + + private parseBinary(): Expr { + return this.parseOr(); + } + + private parseOr(): Expr { + const pos = this.span(); + let left = this.parseAnd(); + while (true) { + if (this.test("or")) { + left = this.parBinTail(left, pos, this.parseAnd, "or"); + } else { + break; + } + } + return left; + } + + private parseAnd(): Expr { + const pos = this.span(); + let left = this.parseEquality(); + while (true) { + if (this.test("and")) { + left = this.parBinTail(left, pos, this.parseEquality, "and"); + } else { + break; + } + } + return left; + } + + private parseEquality(): Expr { + const pos = this.span(); + const left = this.parseComparison(); + if (this.test("==")) { + return this.parBinTail(left, pos, this.parseComparison, "=="); + } + if (this.test("!=")) { + return this.parBinTail(left, pos, this.parseComparison, "!="); + } + return left; + } + + private parseComparison(): Expr { + const pos = this.span(); + const left = this.parseAddSub(); + if (this.test("<")) { + return this.parBinTail(left, pos, this.parseAddSub, "<"); + } + if (this.test(">")) { + return this.parBinTail(left, pos, this.parseAddSub, ">"); + } + if (this.test("<=")) { + return this.parBinTail(left, pos, this.parseAddSub, "<="); + } + if (this.test(">=")) { + return this.parBinTail(left, pos, this.parseAddSub, ">="); + } + return left; + } + + private parseAddSub(): Expr { + const pos = this.span(); + let left = this.parseMulDiv(); + while (true) { + if (this.test("+")) { + left = this.parBinTail(left, pos, this.parseMulDiv, "+"); + continue; + } + if (this.test("-")) { + left = this.parBinTail(left, pos, this.parseMulDiv, "-"); + continue; + } + break; + } + return left; + } + + private parseMulDiv(): Expr { + const pos = this.span(); + let left = this.parsePrefix(); + while (true) { + if (this.test("*")) { + left = this.parBinTail(left, pos, this.parsePrefix, "*"); + continue; + } + if (this.test("/")) { + left = this.parBinTail(left, pos, this.parsePrefix, "/"); + continue; + } + break; + } + return left; + } + + private parBinTail( + left: Expr, + span: Span, + parseRight: (this: Parser) => Expr, + binaryType: BinaryType, + ): Expr { + this.step(); + const right = parseRight.call(this); + return this.expr( + { tag: "binary", binaryType, left, right }, + span, + ); + } + + private parsePrefix(): Expr { + const pos = this.span(); + if (this.test("not") || this.test("-")) { + const unaryType = this.current().type as UnaryType; + this.step(); + const expr = this.parsePrefix(); + return this.expr({ tag: "unary", unaryType, expr }, pos); + } + if (this.test("&")) { + this.step(); + let refType: RefType = "ref"; + if (this.test("ptr")) { + this.step(); + refType = "ptr"; + } + let mut = false; + if (this.test("mut")) { + this.step(); + mut = true; + } + const expr = this.parsePrefix(); + return this.expr({ tag: "ref", expr, mut, refType }, pos); + } + if (this.test("*")) { + this.step(); + const expr = this.parsePrefix(); + return this.expr({ tag: "deref", expr }, pos); + } + return this.parsePostfix(); + } + + private parsePostfix(): Expr { + let subject = this.parseOperand(); + while (true) { + if (this.test(".")) { + subject = this.parseFieldTail(subject); + continue; + } + if (this.test("[")) { + subject = this.parseIndexTail(subject); + continue; + } + if (this.test("(")) { + subject = this.parseCallTail(subject); + continue; + } + break; + } + return subject; + } + + private parseFieldTail(expr: Expr): Expr { + const pos = this.span(); + this.step(); + if (!this.test("ident")) { + this.report("expected ident"); + return this.expr({ tag: "error" }, pos); + } + const ident = this.parseIdent(); + return this.expr({ tag: "field", expr, ident }, pos); + } + + private parseIndexTail(expr: Expr): Expr { + const pos = this.span(); + this.step(); + const index = this.parseExpr(); + if (!this.test("]")) { + this.report("expected ']'"); + return this.expr({ tag: "error" }, pos); + } + this.step(); + return this.expr({ tag: "index", expr, index }, pos); + } + + private parseCallTail(expr: Expr): Expr { + const pos = this.span(); + const args = this.parseDelimitedList( + this.parseExprArg, + ")", + ",", + ); + return this.expr({ tag: "call", expr, args }, pos); + } + + private parseExprArg(): ParseRes { + return Res.Ok(this.parseExpr()); + } + + private parseOperand(): Expr { + const pos = this.span(); + if (this.test("ident")) { + const pathRes = this.parsePath(); + if (!pathRes.ok) { + return this.expr({ tag: "error" }, pos); + } + if (this.test("{")) { + this.step(); + const fields = this.parseDelimitedList( + this.parseExprField, + "}", + ",", + ); + return this.expr( + { tag: "struct", path: pathRes.val, fields }, + pathRes.val.span, + ); + } + return this.expr({ tag: "path", path: pathRes.val }, pos); + } + if (this.test("int")) { + const value = this.current().intValue!; + this.step(); + return this.expr({ tag: "int", value }, pos); + } + if (this.test("str")) { + const value = this.current().stringValue!; + this.step(); + return this.expr({ tag: "str", value }, pos); + } + if (this.test("false")) { + this.step(); + return this.expr({ tag: "bool", value: false }, pos); + } + if (this.test("true")) { + this.step(); + return this.expr({ tag: "bool", value: true }, pos); + } + if (this.test("null")) { + this.step(); + return this.expr({ tag: "null" }, pos); + } + if (this.test("(")) { + this.step(); + const expr = this.parseExpr(); + if (!this.test(")")) { + this.report("expected ')'"); + return this.expr({ tag: "error" }, pos); + } + this.step(); + return this.expr({ tag: "group", expr }, pos); + } + if (this.test("[")) { + return this.parseArray(); + } + if (this.test("struct")) { + return this.parseStruct(); + } + if (this.test("{")) { + return this.parseBlockExpr(); + } + if (this.test("if")) { + return this.parseIf(); + } + if (this.test("loop")) { + return this.parseLoop(); + } + + this.report(`expected expr, got '${this.current().type}'`, pos); + this.step(); + return this.expr({ tag: "error" }, pos); + } + + private parseExprField(): ParseRes { + if (!this.test("ident")) { + this.report("expected 'ident'"); + return Res.Err(undefined); + } + const ident = this.parseIdent(); + if (!this.test(":")) { + this.report("expected ':'"); + return Res.Err(undefined); + } + this.step(); + const expr = this.parseExpr(); + return Res.Ok({ + ident, + expr, + span: Span.fromto(ident.span, expr.span), + }); + } + + private parsePat(): Pat { + const pos = this.span(); + if (this.test("ident")) { + const ident = this.parseIdent(); + return this.pat({ tag: "bind", ident, mut: false }, ident.span); + } + if (this.test("mut")) { + this.step(); + if (!this.test("ident")) { + this.report("expected 'ident'"); + return this.pat({ tag: "error" }, pos); + } + const ident = this.parseIdent(); + return this.pat({ tag: "bind", ident, mut: false }, pos); + } + this.report(`expected pattern, got '${this.current().type}'`, pos); + this.step(); + return this.pat({ tag: "error" }, pos); + } + + private parseTy(): Ty { + const pos = this.span(); + if (["null", "int", "bool", "str"].includes(this.current().type)) { + const tag = this.current().type as + | "null" + | "int" + | "bool" + | "str"; + this.step(); + return this.ty({ tag }, pos); + } + if (this.test("ident")) { + const pathRes = this.parsePath(); + if (!pathRes.ok) { + return this.ty({ tag: "error" }, pos); + } + return this.ty({ tag: "path", path: pathRes.val }, pos); + } + if (this.test("[")) { + this.step(); + const ty = this.parseTy(); + if (this.test(";")) { + this.step(); + const length = this.parseExpr(); + if (!this.test("]")) { + this.report("expected ']'", pos); + return this.ty({ tag: "error" }, pos); + } + this.step(); + return this.ty({ tag: "array", ty, length }, pos); + } + if (!this.test("]")) { + this.report("expected ']' or ';'", pos); + return this.ty({ tag: "error" }, pos); + } + this.step(); + return this.ty({ tag: "slice", ty }, pos); + } + if (this.test("struct")) { + this.step(); + if (!this.test("{")) { + this.report("expected '{'"); + return this.ty({ tag: "error" }, pos); + } + const fields = this.parseAnonFieldDefs(); + return this.ty({ tag: "anon_struct", fields }, pos); + } + if (this.test("&")) { + this.step(); + let mut = false; + if (this.test("mut")) { + this.step(); + mut = true; + } + const ty = this.parseTy(); + return this.ty({ tag: "ref", ty, mut }, pos); + } + if (this.test("*")) { + this.step(); + let mut = false; + if (this.test("mut")) { + this.step(); + mut = true; + } + const ty = this.parseTy(); + return this.ty({ tag: "ptr", ty, mut }, pos); + } + this.report("expected type"); + return this.ty({ tag: "error" }, pos); + } + + private parseAnonFieldDefs(): AnonFieldDef[] { + this.step(); + if (this.test("}")) { + this.step(); + return []; + } + const params: AnonFieldDef[] = []; + const paramResult = this.parseAnonFieldDef(); + if (!paramResult.ok) { + return []; + } + params.push(paramResult.val); + while (this.test(",")) { + this.step(); + if (this.test("}")) { + break; + } + const paramResult = this.parseAnonFieldDef(); + if (!paramResult.ok) { + return []; + } + params.push(paramResult.val); + } + if (!this.test("}")) { + this.report("expected '}'"); + return params; + } + this.step(); + return params; + } + + private parseAnonFieldDef(): ParseRes { + const begin = this.span(); + const identRes = this.eatIdent(); + if (!identRes.ok) return Res.Err(undefined); + const ident = identRes.val; + if (!this.test(":")) { + this.report("expected ':'"); + return Res.Err(undefined); + } + this.step(); + const ty = this.parseTy(); + return Res.Ok({ + ident, + ty, + span: Span.fromto(begin, ty.span), + }); + } + + private parsePath(): ParseRes { + const begin = this.span(); + let end = begin; + const segments: PathSegment[] = []; + const identRes = this.eatIdent(); + if (!identRes.ok) return Res.Err(undefined); + const ident = identRes.val; + segments.push({ ident, span: Span.fromto(begin, end) }); + while (this.test("::")) { + this.step(); + if (!this.test("ident")) { + this.report("expected 'ident'"); + return Res.Err(undefined); + } + end = this.span(); + const ident = this.parseIdent(); + let genericArgs: Ty[] | undefined = undefined; + if (this.test("::")) { + this.step(); + if (!this.test("<")) { + this.report("expected '<'"); + return Res.Err(undefined); + } + genericArgs = this.parseDelimitedList( + this.parseTyRes, + ">", + ",", + ); + } + segments.push({ + ident, + genericArgs, + span: Span.fromto(begin, end), + }); + } + return Res.Ok({ segments, span: Span.fromto(begin, end) }); + } + + private parseTyRes(): ParseRes { + return Res.Ok(this.parseTy()); + } + + private eatIdent(): ParseRes { + if (!this.test("ident")) { + this.report("expected 'ident'"); + return Res.Err(undefined); + } + return Res.Ok(this.parseIdent()); + } + + private parseIdent(): Ident { + const tok = this.current(); + this.step(); + return { id: tok.identId!, text: tok.identText!, span: tok.span }; + } + + private step() { + this.currentToken = this.lexer.next(); + } + + private done(): boolean { + return this.currentToken == null; + } + + private current(): Token { + return this.currentToken!; + } + + private lastSpan?: Span; + private span(): Span { + if (this.done()) { + return this.lastSpan!; + } + return this.lastSpan = this.current().span; + } + + private test(type: string): boolean { + return !this.done() && this.current().type === type; + } + + private report(msg: string, span = this.span()) { + this.ctx.report({ + severity: "error", + origin: "parser", + msg, + file: this.file, + span, + }); + } + + private stmt(kind: StmtKind, span: Span): Stmt { + return this.cx.stmt(kind, span); + } + + private item( + kind: ItemKind, + span: Span, + ident: Ident, + pub: boolean, + ): Item { + return this.cx.item(kind, span, ident, pub); + } + + private expr(kind: ExprKind, span: Span): Expr { + return this.cx.expr(kind, span); + } + + private pat(kind: PatKind, span: Span): Pat { + return this.cx.pat(kind, span); + } + + private ty(kind: TyKind, span: Span): Ty { + return this.cx.ty(kind, span); + } +} diff --git a/slige/compiler/parse/token.ts b/slige/compiler/parse/token.ts new file mode 100644 index 0000000..cfc0667 --- /dev/null +++ b/slige/compiler/parse/token.ts @@ -0,0 +1,31 @@ +import { Span } from "../diagnostics.ts"; +import { IdentId } from "../ids.ts"; + +export type Token = { + type: string; + span: Span; + length: number; + identId?: IdentId; + identText?: string; + intValue?: number; + stringValue?: string; +}; + +export interface TokenIter { + next(): Token | null; +} + +export class SigFilter implements TokenIter { + public constructor(private iter: TokenIter) {} + + next(): Token | null { + const token = this.iter.next(); + if (token === null) { + return token; + } + if (token?.type === "whitespace" || token?.type === "comment") { + return this.next(); + } + return token; + } +} diff --git a/slige/compiler/program.slg b/slige/compiler/program.slg new file mode 100644 index 0000000..a79216a --- /dev/null +++ b/slige/compiler/program.slg @@ -0,0 +1,12 @@ + +fn add(lhs: int, rhs: int) -> int { + lhs + rhs +} + +fn main() { + let a = 5; + let b = 7; + let c = add(a, b); +} + + diff --git a/slige/compiler/resolve/cx.ts b/slige/compiler/resolve/cx.ts new file mode 100644 index 0000000..f2bd84e --- /dev/null +++ b/slige/compiler/resolve/cx.ts @@ -0,0 +1,131 @@ +import * as ast from "../ast/mod.ts"; +import { IdBase, IdentId, IdMap } from "../ids.ts"; +import { Res } from "../util.ts"; + +export interface Syms { + getVal(ident: ast.Ident): Resolve; + getTy(ident: ast.Ident): Resolve; + + defVal(ident: ast.Ident, kind: ResolveKind): Res; + defTy(ident: ast.Ident, kind: ResolveKind): Res; +} + +export type Resolve = { + ident: ast.Ident; + kind: ResolveKind; +}; + +export type LocalId = IdBase & { readonly _: unique symbol }; + +export type ResolveKind = + | { tag: "error" } + | { tag: "fn"; item: ast.Item; kind: ast.FnItem } + | { tag: "local"; id: LocalId }; + +export const ResolveError = (ident: ast.Ident): Resolve => ({ + ident, + kind: { tag: "error" }, +}); + +export type Redef = { + ident: ast.Ident; +}; + +export class SymsOneNsTab { + private defs = new IdMap(); + + public get(ident: ast.Ident): Resolve | undefined { + return this.defs.get(ident.id)!; + } + + public def(ident: ast.Ident, kind: ResolveKind): Res { + if (this.defs.has(ident.id)) { + return Res.Err({ ident: this.defs.get(ident.id)!.ident }); + } + this.defs.set(ident.id, { ident, kind }); + return Res.Ok(undefined); + } +} + +export class SymsNsTab { + private vals = new SymsOneNsTab(); + private tys = new SymsOneNsTab(); + + public getVal(ident: ast.Ident): Resolve | undefined { + return this.vals.get(ident); + } + public getTy(ident: ast.Ident): Resolve | undefined { + return this.tys.get(ident); + } + + public defVal(ident: ast.Ident, kind: ResolveKind): Res { + return this.vals.def(ident, kind); + } + public defTy(ident: ast.Ident, kind: ResolveKind): Res { + return this.tys.def(ident, kind); + } +} + +export class RootSyms implements Syms { + private syms = new SymsNsTab(); + + getVal(ident: ast.Ident): Resolve { + return this.syms.getVal(ident) || ResolveError(ident); + } + getTy(ident: ast.Ident): Resolve { + return this.syms.getTy(ident) || ResolveError(ident); + } + + defVal(ident: ast.Ident, kind: ResolveKind): Res { + return this.syms.defVal(ident, kind); + } + defTy(ident: ast.Ident, kind: ResolveKind): Res { + return this.syms.defTy(ident, kind); + } +} + +export class FnSyms implements Syms { + private syms = new SymsNsTab(); + + public constructor( + private parent: Syms, + ) {} + + getVal(ident: ast.Ident): Resolve { + const res = this.syms.getVal(ident) || this.parent.getVal(ident); + if (res.kind.tag === "local") { + return ResolveError(ident); + } + return res; + } + getTy(ident: ast.Ident): Resolve { + return this.syms.getTy(ident) || this.parent.getTy(ident); + } + defVal(ident: ast.Ident, kind: ResolveKind): Res { + return this.syms.defVal(ident, kind); + } + defTy(ident: ast.Ident, kind: ResolveKind): Res { + return this.syms.defTy(ident, kind); + } +} + +export class LocalSyms implements Syms { + private syms = new SymsNsTab(); + + public constructor( + private parent: Syms, + ) {} + + getVal(ident: ast.Ident): Resolve { + return this.syms.getVal(ident) || this.parent.getVal(ident); + } + getTy(ident: ast.Ident): Resolve { + return this.syms.getTy(ident) || this.parent.getTy(ident); + } + defVal(ident: ast.Ident, kind: ResolveKind): Res { + return this.syms.defVal(ident, kind); + } + defTy(ident: ast.Ident, kind: ResolveKind): Res { + return this.syms.defTy(ident, kind); + } +} diff --git a/slige/compiler/resolve/resolver.ts b/slige/compiler/resolve/resolver.ts new file mode 100644 index 0000000..22a0ddb --- /dev/null +++ b/slige/compiler/resolve/resolver.ts @@ -0,0 +1,197 @@ +import * as ast from "../ast/mod.ts"; +import { Ctx, File } from "../ctx.ts"; +import { AstId, IdMap, Ids } from "../ids.ts"; +import { exhausted, todo } from "../util.ts"; +import { + FnSyms, + LocalId, + LocalSyms, + Resolve, + ResolveError, + RootSyms, + Syms, +} from "./cx.ts"; +export { type LocalId } from "./cx.ts"; + +export class Resols { + public constructor( + private exprResols: IdMap, + ) {} + + public exprRes(id: AstId): Resolve { + if (!this.exprResols.has(id)) { + throw new Error(); + } + return this.exprResols.get(id)!; + } +} + +export class Resolver implements ast.Visitor { + private currentFile!: File; + private rootSyms = new RootSyms(); + private syms: Syms = this.rootSyms; + + private exprResols = new IdMap(); + + private localIds = new Ids(); + + public constructor( + private ctx: Ctx, + private entryFileAst: ast.File, + ) {} + + public resolve(): Resols { + ast.visitFile(this, this.entryFileAst); + return new Resols( + this.exprResols, + ); + } + + visitFile(file: ast.File): ast.VisitRes { + this.currentFile = file.file; + ast.visitStmts(this, file.stmts); + this.visitFnBodies(); + return "stop"; + } + + visitLetStmt(stmt: ast.Stmt, kind: ast.LetStmt): ast.VisitRes { + kind.ty && ast.visitTy(this, kind.ty); + kind.expr && ast.visitExpr(this, kind.expr); + this.syms = new LocalSyms(this.syms); + ast.visitPat(this, kind.pat); + return "stop"; + } + + visitModBlockItem(item: ast.Item, kind: ast.ModBlockItem): ast.VisitRes { + todo(); + } + + visitModFileItem(item: ast.Item, kind: ast.ModFileItem): ast.VisitRes { + ast.visitFile(this, kind.ast!); + todo(); + } + + visitEnumItem(item: ast.Item, kind: ast.EnumItem): ast.VisitRes { + todo(); + } + + visitStructItem(item: ast.Item, kind: ast.StructItem): ast.VisitRes { + todo(); + } + + private fnBodiesToCheck: [ast.Item, ast.FnItem][] = []; + + visitFnItem(item: ast.Item, kind: ast.FnItem): ast.VisitRes { + this.syms.defVal(item.ident, { tag: "fn", item, kind }); + this.fnBodiesToCheck.push([item, kind]); + return "stop"; + } + + private visitFnBodies() { + for (const [_item, kind] of this.fnBodiesToCheck) { + const outerSyms = this.syms; + this.syms = new FnSyms(this.syms); + this.syms = new LocalSyms(this.syms); + for (const param of kind.params) { + ast.visitParam(this, param); + } + this.syms = outerSyms; + } + this.fnBodiesToCheck = []; + } + + visitPathExpr(expr: ast.Expr, kind: ast.PathExpr): ast.VisitRes { + if (kind.path.segments.length === 1) { + const res = this.syms.getVal(kind.path.segments[0].ident); + switch (res.kind.tag) { + case "error": + return "stop"; + case "fn": + this.exprResols.set(expr.id, res); + return "stop"; + case "local": + this.exprResols.set(expr.id, res); + return "stop"; + } + exhausted(res.kind); + } + const pathRes = this.resolveInnerPath(kind.path); + switch (pathRes.kind.tag) { + case "error": + todo(); + return "stop"; + case "fn": + todo(); + return "stop"; + case "local": + todo(); + return "stop"; + } + exhausted(pathRes.kind); + } + + visitUseItem(item: ast.Item, kind: ast.UseItem): ast.VisitRes { + todo(); + } + + visitTypeAliasItem(item: ast.Item, kind: ast.TypeAliasItem): ast.VisitRes { + todo(); + } + + visitBindPat(pat: ast.Pat, kind: ast.BindPat): ast.VisitRes { + const res = this.syms.defVal(kind.ident, { + tag: "local", + id: this.localIds.nextThenStep(), + }); + if (!res.ok) { + const text = this.ctx.identText(kind.ident.id); + this.ctx.report({ + severity: "error", + file: this.currentFile, + span: kind.ident.span, + msg: `redefinition of value '${text}'`, + }); + } + return "stop"; + } + + visitPathPat(pat: ast.Pat, kind: ast.PathPat): ast.VisitRes { + todo(); + } + + visitBlock(block: ast.Block): ast.VisitRes { + ast.visitStmts(this, block.stmts); + this.visitFnBodies(); + block.expr && ast.visitExpr(this, block.expr); + return "stop"; + } + + private resolveInnerPath(path: ast.Path): Resolve { + const res = path.segments.slice(1, path.segments.length) + .reduce((innerRes, seg) => { + const k = innerRes.kind; + switch (k.tag) { + case "error": + return innerRes; + case "fn": + this.ctx.report({ + severity: "error", + file: this.currentFile, + span: seg.ident.span, + msg: "function, not pathable", + }); + return ResolveError(seg.ident); + case "local": + this.ctx.report({ + severity: "error", + file: this.currentFile, + span: seg.ident.span, + msg: "local variable, not pathable", + }); + return ResolveError(seg.ident); + } + exhausted(k); + }, this.syms.getTy(path.segments[0].ident)); + return res; + } +} diff --git a/slige/compiler/test_diagnostics.ts b/slige/compiler/test_diagnostics.ts new file mode 100644 index 0000000..fb7d7dc --- /dev/null +++ b/slige/compiler/test_diagnostics.ts @@ -0,0 +1,64 @@ +import { Ctx } from "./ctx.ts"; +import { prettyPrintReport } from "./diagnostics.ts"; + +const ctx = new Ctx(); + +const text = ` +make an error here +`; + +const biggerText = ` +dont make error here +not here but start error here +and here +also here but not here +or here +` + +const file = ctx.addFile( + "root", + "path/file.ts", + "path/file.ts", + undefined, + text, +); + +const biggerFile = ctx.addFile( + "root", + "path/file.ts", + "path/file.ts", + undefined, + biggerText, +); + +prettyPrintReport(ctx, { + file, + msg: "an error", + severity: "fatal", + origin: "compiler", + span: { + begin: { idx: 5, line: 2, col: 5 }, + end: { idx: 13, line: 2, col: 13 }, + }, +}); + +prettyPrintReport(ctx, { + file: biggerFile, + msg: "an error", + severity: "error", + origin: "compiler", + span: { + begin: { idx: 6, line: 3, col: 14 }, + end: { idx: 13, line: 5, col: 13 }, + }, +}); + +prettyPrintReport(ctx, { + file, + msg: "an error", + severity: "warning", + origin: "compiler", + pos: { + idx: 6, line: 2, col: 8 + }, +}); diff --git a/slige/compiler/ty/to_string.ts b/slige/compiler/ty/to_string.ts new file mode 100644 index 0000000..0f91c7f --- /dev/null +++ b/slige/compiler/ty/to_string.ts @@ -0,0 +1,26 @@ +import { Ctx } from "../ctx.ts"; +import { exhausted } from "../util.ts"; +import { Ty } from "./ty.ts"; + +export function tyToString(ctx: Ctx, ty: Ty): string { + const k = ty.kind; + switch (k.tag) { + case "error": + return ``; + case "unknown": + return ``; + case "null": + return `null`; + case "int": + return `int`; + case "fn": { + const identText = ctx.identText(k.item.ident.id); + const params = k.params + .map((param) => tyToString(ctx, param)) + .join(", "); + const reTy = tyToString(ctx, k.returnTy); + return `fn ${identText}(${params}) -> ${reTy}`; + } + } + exhausted(k); +} diff --git a/slige/compiler/ty/ty.ts b/slige/compiler/ty/ty.ts new file mode 100644 index 0000000..c5c79a5 --- /dev/null +++ b/slige/compiler/ty/ty.ts @@ -0,0 +1,20 @@ +import * as ast from "../ast/mod.ts"; + +export type Ty = { + kind: TyKind; +}; + +export const Ty = (kind: TyKind): Ty => ({ kind }); + +export type TyKind = + | { tag: "error" } + | { tag: "unknown" } + | { tag: "null" } + | { tag: "int" } + | { + tag: "fn"; + item: ast.Item; + kind: ast.FnItem; + params: Ty[]; + returnTy: Ty; + }; diff --git a/slige/compiler/util.ts b/slige/compiler/util.ts new file mode 100644 index 0000000..750eb36 --- /dev/null +++ b/slige/compiler/util.ts @@ -0,0 +1,63 @@ +export function todo(...args: unknown[]): T { + const argsStr = args.map((a) => JSON.stringify(a)).join(", "); + class NotImplemented extends Error { + constructor() { + super(`todo(${argsStr})`); + this.name = "NotImplemented"; + } + } + throw new NotImplemented(); +} + +export function exhausted(...args: never[]): T { + const argsStr = args.map((a) => JSON.stringify(a)).join(", "); + class Unexhausted extends Error { + constructor() { + super(`exhausted(${argsStr})`); + this.name = "Unexhausted"; + } + } + throw new Unexhausted(); +} + +export type Res = Ok | Err; +export type Ok = { ok: true; val: V }; +export type Err = { ok: false; val: E }; + +export const Ok = (val: V): Ok => ({ ok: true, val }); +export const Err = (val: E): Err => ({ ok: false, val }); + +export const Res = { Ok, Err } as const; + +export type ControlFlow< + R = undefined, + V = undefined, +> = Break | Continue; + +export type Break = { break: true; val: R }; +export type Continue = { break: false; val: V }; + +export const ControlFlow = { + Break: (val: R): Break => ({ break: true, val }), + Continue: (val: V): Continue => ({ break: false, val }), +} as const; + +export const range = (length: number) => (new Array(length).fill(0)); + +export const strictEq = (a: T, b: T): boolean => a === b; + +export function arrayEq( + a: T[], + b: T[], + elemCmp: (a: T, b: T) => boolean = strictEq, +): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; ++i) { + if (!elemCmp(a[i], b[i])) { + return false; + } + } + return true; +}