commit 1298b693880f77bb054b60ab2c778bf11d15226a Author: sfja Date: Fri Apr 10 02:13:26 2026 +0200 init diff --git a/demo.hlcsl b/demo.hlcsl new file mode 100644 index 0000000..21a9019 --- /dev/null +++ b/demo.hlcsl @@ -0,0 +1,47 @@ + +(def nand (a b vin) (r) ( + (let (a_and_b) (relay_default_off a b)) + (set r (relay_default_on a_and_b vin)) +)) + +(def not (a) (r) ( + (set r (nand a a)) +)) + +(def and (a b) (r) ( + (set r (not (nand a b))) +)) + +(def or (a b) (r) ( + (set r (nand (not a) (not b))) +)) + +(def xor (a b) (r) ( + (set r (and (or a b) (not (and a b)))) +)) + +(def xor_optimized (a b) (r) ( + (let (c) (nand a b)) + (set r (nand (nand a c) (nand b c))) +)) + +(def half_add (a b) (r0 r1) ( + (set r0 (xor a b)) + (set r1 (and a b)) +)) + +(def add (a b carry) (r0 r1) ( + (let (d0 d1) (half_add a b)) + (let (e0 e1) (half_add carry d0)) + (set r0 e0) + (set r1 (or d1 e1)) +)) + +(def add2 (a0 a1 b0 b1 carry_in) (r0 r1 carry_out) ( + (let (d0 d1) (add a0 b0 carry_in)) + (let (e0 e1) (add a1 b1 d1)) + (set r0 d0) + (set r1 e0) + (set carry_out e1) +)) + diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..7ebf61b --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,5 @@ +{ + "fmt": { + "indentWidth": 4 + } +} \ No newline at end of file diff --git a/src/ast.ts b/src/ast.ts new file mode 100644 index 0000000..071dd4e --- /dev/null +++ b/src/ast.ts @@ -0,0 +1,79 @@ +export interface Visitor { + visitDef?(def: Def): void | "stop"; + visitStmt?(stmt: Stmt): void | "stop"; + visitExpr?(expr: Expr): void | "stop"; +} + +export class Def { + constructor( + public line: number, + public ident: string, + public inputs: string[], + public outputs: string[], + public stmts: Stmt[], + ) {} + + visit(v: Visitor) { + if (v.visitDef?.(this) === "stop") return; + for (const stmt of this.stmts) { + stmt.visit(v); + } + } +} + +export class Stmt { + constructor( + public line: number, + public kind: StmtKind, + ) {} + + visit(v: Visitor) { + if (v.visitStmt?.(this) === "stop") return; + this.visitSubtree(v); + } + + visitSubtree(v: Visitor) { + const k = this.kind; + switch (k.tag) { + case "Set": + k.subject.visit(v); + k.expr.visit(v); + break; + case "Let": + k.expr.visit(v); + break; + default: + k satisfies never; + } + } +} + +export type StmtKind = + | { tag: "Set"; subject: Expr; expr: Expr } + | { tag: "Let"; idents: string[]; expr: Expr }; + +export class Expr { + constructor( + public line: number, + public kind: ExprKind, + ) {} + + visit(v: Visitor) { + if (v.visitExpr?.(this) === "stop") return; + const k = this.kind; + switch (k.tag) { + case "Ident": + break; + case "Call": + k.callee.visit(v); + k.args.map((e) => e.visit(v)); + break; + default: + k satisfies never; + } + } +} + +export type ExprKind = + | { tag: "Ident"; ident: string } + | { tag: "Call"; callee: Expr; args: Expr[] }; diff --git a/src/front.ts b/src/front.ts new file mode 100644 index 0000000..f2748f0 --- /dev/null +++ b/src/front.ts @@ -0,0 +1,308 @@ +import { Def, Expr, Stmt } from "./ast.ts"; + +function appended(vs: T[], v: T): T[] { + vs.push(v); + return vs; +} + +export type Tok = { + ty: string; + text: string; + line: number; +}; + +export function tokenize(text: string): Tok[] { + return text + .replace(/\/\/[^\n]*/g, "") + .replace(/[\(\)\n]/g, " $& ") + .split(/[ \t\r]+/) + .filter((tok) => tok !== "") + .reduce<[{ tok: string; line: number }[], number]>( + ([toks, line], tok) => + tok != "\n" + ? [appended(toks, { tok, line }), line] + : [toks, line + 1], + [[], 1], + )[0] + .map(({ tok, line }) => ({ ty: tok, text: tok, line })) + .map((tok) => { + if (/^[a-zA-Z_][a-zA-Z_0-9]*$/.test(tok.text)) { + tok.ty = "ident"; + } + return tok; + }); +} + +export type SExpr = { + line: number; + ty: "Ident" | "List"; + ident?: string; + exprs?: SExpr[]; +}; + +function parseSExpr(toks: Tok[]): [Tok[], SExpr | null] { + if (toks.length == 0) { + return [toks, null]; + } + const line = toks[0].line; + if (toks[0].ty === "ident") { + const ident = toks[0].text; + return [toks.slice(1), { line, ty: "Ident", ident }]; + } else if (toks[0].ty === "(") { + toks = toks.slice(1); + const exprs: SExpr[] = []; + while (toks.length != 0 && toks[0].ty !== ")") { + let expr: SExpr | null; + [toks, expr] = parseSExpr(toks); + if (!expr) { + throw new Error(`expected expression on line ${line}`); + } + exprs.push(expr); + } + if (toks.length == 0 || toks[0].ty != ")") { + throw new Error( + `expected ')' on line ${ + toks.at(0)?.line ?? "(eof)" + }, '(' on line ${line}`, + ); + } + return [toks.slice(1), { line, ty: "List", exprs }]; + } else { + throw new Error(`malformed on line ${line}`); + } +} + +export function parseSExprs(toks: Tok[]): SExpr[] { + const exprs: SExpr[] = []; + while (toks.length != 0) { + let expr: SExpr | null; + [toks, expr] = parseSExpr(toks); + exprs.push(expr!); + } + return exprs; +} + +class SExprMatcher { + private failed = false; + private captures = new Map(); + + constructor(private s: SExpr) {} + + match(): Record | null { + return this.failed ? null : Object.fromEntries(this.captures.entries()); + } + + ident(val?: string): this { + if (this.s.ty !== "Ident" || val && this.s.ident! !== val) { + this.failed = true; + return this; + } + return this; + } + + list(length?: number): this { + if ( + this.s.ty !== "List" || (length && this.s.exprs!.length !== length) + ) { + this.failed = true; + return this; + } + return this; + } + + at(idx: number, func?: (s: SExprMatcher) => SExprMatcher): this { + if (this.s.ty !== "List" || idx >= this.s.exprs!.length) { + this.failed = true; + return this; + } + if (func) { + const inner = new SExprMatcher(this.s.exprs![idx]); + func(inner); + for (const [key, val] of inner.captures.entries()) { + this.captures.set(key, val); + } + this.failed = this.failed || inner.failed; + } + return this; + } + + all(func: (s: SExprMatcher) => SExprMatcher): this { + if (this.s.ty !== "List") { + this.failed = true; + return this; + } + for (const s1 of this.s.exprs!) { + const inner = new SExprMatcher(s1); + func(inner); + for (const [key, val] of inner.captures.entries()) { + this.captures.set(key, val); + } + } + return this; + } + + capture(id: string): this { + this.captures.set(id, this.s); + return this; + } +} + +export function parseAst(ss: SExpr[]): Def[] { + const defs: Def[] = []; + for (const s of ss) { + defs.push(parseDef(s)); + } + return defs; +} + +function parseDef(s: SExpr): Def { + const m = new SExprMatcher(s) + .list(5) + .at(0, (s) => s.ident("def")) + .at(1, (s) => s.ident().capture("ident")) + .at(2, (s) => s.all((s) => s.ident()).capture("inputs")) + .at(3, (s) => s.all((s) => s.ident()).capture("outputs")) + .at(4, (s) => s.list().capture("stmts")) + .match(); + if (!m) { + throw new Error(`malformed def on line ${s.line}`); + } + return new Def( + s.line, + m.ident.ident!, + m.inputs.exprs!.map((s) => s.ident!), + m.outputs.exprs!.map((s) => s.ident!), + m.stmts.exprs!.map((s) => parseStmt(s)), + ); +} + +function parseStmt(s: SExpr): Stmt { + const set_match = new SExprMatcher(s) + .list(3) + .at(0, (s) => s.ident("set")) + .at(1, (s) => s.ident().capture("subject")) + .at(2, (s) => s.capture("expr")) + .match(); + if (set_match) { + const m = set_match; + return new Stmt( + s.line, + { + tag: "Set", + subject: parseExpr(m.subject), + expr: parseExpr(m.expr), + }, + ); + } + const let_match = new SExprMatcher(s) + .list(3) + .at(0, (s) => s.ident("let")) + .at(1, (s) => s.all((s) => s.ident()).capture("idents")) + .at(2, (s) => s.capture("expr")) + .match(); + if (let_match) { + const m = let_match; + + return new Stmt( + s.line, + { + tag: "Let", + idents: m.idents.exprs!.map((s) => s.ident!), + expr: parseExpr(m.expr), + }, + ); + } + throw new Error(`malformed statement on line ${s.line}`); +} + +function parseExpr(s: SExpr): Expr { + if (s.ty === "Ident") { + return new Expr(s.line, { tag: "Ident", ident: s.ident! }); + } else if (s.ty === "List") { + if (s.exprs!.length === 0) { + throw new Error(`empty expression not allowed, on line ${s.line}`); + } + if (s.exprs![0].ty !== "Ident") { + throw new Error(`callee must be an identifier, on line ${s.line}`); + } + return new Expr( + s.line, + { + tag: "Call", + callee: parseExpr(s.exprs![0]), + args: s.exprs!.slice(1).map((s) => parseExpr(s)), + }, + ); + } else { + throw new Error(`expected expression, on line ${s.line}`); + } +} + +export function resolve(defs: Def[]): Map> { + const defSyms = new Map(); + for (const def of defs) { + defSyms.set(def.ident, def); + } + const resols = new Map>(); + for (const def of defs) { + const defResolver = new DefResolver(def, defSyms); + defResolver.resolve(); + resols.set(def, defResolver.resols); + } + return resols; +} + +export type Sym = + | { tag: "Builtin" } + | { tag: "Input"; idx: number } + | { tag: "Output"; idx: number } + | { tag: "Node"; stmt: Stmt; idx: number } + | { tag: "Def"; def: Def }; + +class DefResolver { + private syms = new Map([ + ["relay_default_off", { tag: "Builtin" }], + ["relay_default_on", { tag: "Builtin" }], + ]); + public resols = new Map(); + + constructor(private def: Def, private defSyms: Map) {} + + resolve() { + for (const [idx, ident] of this.def.inputs.entries()) { + this.syms.set(ident, { tag: "Input", idx }); + } + for (const [idx, ident] of this.def.outputs.entries()) { + this.syms.set(ident, { tag: "Output", idx }); + } + const { syms, defSyms, resols } = this; + this.def.visit({ + visitStmt(stmt) { + stmt.visitSubtree(this); + if (stmt.kind.tag === "Let") { + for (const [idx, ident] of stmt.kind.idents.entries()) { + syms.set(ident, { tag: "Node", stmt, idx }); + } + } + return "stop"; + }, + visitExpr(expr) { + if (expr.kind.tag === "Ident") { + const sym = syms.get(expr.kind.ident); + if (sym) { + resols.set(expr, sym); + return; + } + const defSym = defSyms.get(expr.kind.ident); + if (defSym) { + resols.set(expr, { tag: "Def", def: defSym }); + return; + } + throw new Error( + `unresolved identifier '${expr.kind.ident}', on line ${expr.line}`, + ); + } + }, + }); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..315dbfb --- /dev/null +++ b/src/main.ts @@ -0,0 +1,220 @@ +import * as front from "./front.ts"; +import * as ast from "./ast.ts"; + +class Ins { + constructor( + public line: number, + public kind: InsKind, + ) {} +} + +type InsKind = + | { tag: "Input"; idx: number } + | { tag: "Set"; idx: number; value: Ins } + | { tag: "RelayDefaultOff" | "RelayDefaultOn"; args: Ins[] } + | { tag: "Call"; def: ast.Def; args: Ins[] } + | { tag: "Elem"; value: Ins; idx: number }; + +class InsCx { + public insts: Ins[] = []; + + makeInput(line: number, idx: number): Ins { + return this.make(line, { tag: "Input", idx }); + } + makeSet(line: number, idx: number, value: Ins): Ins { + return this.make(line, { tag: "Set", idx, value }); + } + makeRelayDefaultOff(line: number, args: Ins[]): Ins { + return this.make(line, { tag: "RelayDefaultOff", args }); + } + makeRelayDefaultOn(line: number, args: Ins[]): Ins { + return this.make(line, { tag: "RelayDefaultOn", args }); + } + makeCall(line: number, def: ast.Def, args: Ins[]): Ins { + return this.make(line, { tag: "Call", def, args }); + } + makeElem(line: number, value: Ins, idx: number): Ins { + return this.make(line, { tag: "Elem", value, idx }); + } + + private make(line: number, kind: InsKind) { + const ins = new Ins(line, kind); + this.insts.push(ins); + return ins; + } + + pretty(): string { + let result = ""; + let regIds = 0; + const insRegs = new Map(); + + const r = (i: Ins): string => { + if (!insRegs.has(i)) { + insRegs.set(i, regIds++); + } + return `%${insRegs.get(i)!}`; + }; + + for (const ins of this.insts) { + switch (ins.kind.tag) { + case "Input": + result += ` ${r(ins)} = input ${ins.kind.idx}\n`; + break; + case "Set": + result += ` set ${ins.kind.idx}, ${r(ins.kind.value)}\n`; + break; + case "RelayDefaultOff": + result += ` ${r(ins)} = relay_default_off ${ + r(ins.kind.args[0]) + } ${r(ins.kind.args[1])}\n`; + break; + case "RelayDefaultOn": + result += ` ${r(ins)} = relay_default_on ${ + r(ins.kind.args[0]) + } ${r(ins.kind.args[1])}\n`; + break; + case "Call": + result += ` ${r(ins)} = call @${ins.kind.def.ident} ${ + r(ins.kind.args[0]) + } ${r(ins.kind.args[1])}\n`; + break; + case "Elem": + result += ` ${r(ins)} = elem ${ + r(ins.kind.value) + } ${ins.kind.idx}\n`; + break; + } + } + return result; + } +} + +class DefLowerer { + private symIns = new Map(); + private letIns = new Map(); + + constructor( + private cx: InsCx, + private def: ast.Def, + private resols: Map, + ) {} + + lower() { + for (const stmt of this.def.stmts) { + this.lowerStmt(stmt); + } + } + + lowerStmt(stmt: ast.Stmt) { + switch (stmt.kind.tag) { + case "Set": { + const re = this.resols.get(stmt.kind.subject)!; + switch (re.tag) { + case "Output": + this.cx.makeSet( + stmt.line, + re.idx, + this.lowerExpr(stmt.kind.expr), + ); + break; + case "Builtin": + case "Input": + case "Node": + case "Def": + throw new Error(); + } + break; + } + case "Let": { + const value = this.lowerExpr(stmt.kind.expr); + this.letIns.set( + stmt, + stmt.kind.idents.map((_, i) => + this.cx.makeElem(stmt.line, value, i) + ), + ); + break; + } + default: + stmt.kind satisfies never; + } + } + + lowerExpr(expr: ast.Expr): Ins { + switch (expr.kind.tag) { + case "Ident": { + const re = this.resols.get(expr)!; + switch (re.tag) { + case "Input": + return this.cx.makeInput(expr.line, re.idx); + case "Node": { + return this.letIns.get(re.stmt)![re.idx]; + } + case "Builtin": + case "Output": + case "Def": + throw new Error(); + } + break; + } + case "Call": { + const re = this.resols.get(expr.kind.callee)!; + switch (re.tag) { + case "Builtin": { + if (expr.kind.callee.kind.tag !== "Ident") { + throw new Error(); + } + switch (expr.kind.callee.kind.ident) { + case "relay_default_off": + return this.cx.makeRelayDefaultOff( + expr.line, + expr.kind.args.map((expr) => + this.lowerExpr(expr) + ), + ); + case "relay_default_on": + return this.cx.makeRelayDefaultOn( + expr.line, + expr.kind.args.map((expr) => + this.lowerExpr(expr) + ), + ); + default: + throw new Error(); + } + break; + } + case "Def": { + return this.cx.makeCall( + expr.line, + re.def, + expr.kind.args.map((expr) => this.lowerExpr(expr)), + ); + break; + } + case "Input": + case "Output": + case "Node": + throw new Error(); + } + break; + } + default: + expr.kind satisfies never; + } + throw new Error(); + } +} + +const text = await Deno.readTextFile(Deno.args[0]); +const toks = front.tokenize(text); +const sexprs = front.parseSExprs(toks); +const defs = front.parseAst(sexprs); +const resols = front.resolve(defs); + +for (const def of defs) { + console.log(`${def.ident}:`); + const cx = new InsCx(); + new DefLowerer(cx, def, resols.get(def)!).lower(); + console.log(cx.pretty()); +}