import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js"; import { Err, Ok, Result } from "@davidsouther/jiffies/lib/esm/result.js"; import { type Node, grammar as ohmGrammar } from "ohm-js"; import { ASSIGN, ASSIGN_ASM, ASSIGN_OP, COMMANDS, COMMANDS_ASM, COMMANDS_OP, isAssignAsm, isCommandAsm, JUMP, JUMP_ASM, JUMP_OP, } from "../cpu/alu.js"; import { KEYBOARD_OFFSET, SCREEN_OFFSET } from "../cpu/memory.js"; import { makeC } from "../util/asm.js"; import { baseSemantics, CompilationError, createError, grammars, makeParser, Span, span, } from "./base.js"; import asmGrammar from "./grammars/asm.ohm.js"; export const grammar = ohmGrammar(asmGrammar, grammars); export const asmSemantics = grammar.extendSemantics(baseSemantics); export interface Asm { instructions: AsmInstruction[]; } export type AsmInstruction = | AsmAInstruction | AsmCInstruction | AsmLabelInstruction; export type AsmAInstruction = AsmALabelInstruction | AsmAValueInstruction; export interface AsmALabelInstruction { type: "A"; label: string; span?: Span; } export interface AsmAValueInstruction { type: "A"; value: number; span?: Span; } export function isAValueInstruction( inst: AsmInstruction, ): inst is AsmAValueInstruction { return inst.type == "A" && (inst as AsmAValueInstruction).value !== undefined; } function isALabelInstruction( inst: AsmInstruction, ): inst is AsmALabelInstruction { return inst.type == "A" && (inst as AsmALabelInstruction).label !== undefined; } export interface AsmCInstruction { type: "C"; op: COMMANDS_OP; isM: boolean; store?: ASSIGN_OP; jump?: JUMP_OP; span?: Span; } export interface AsmLabelInstruction { type: "L"; label: string; span?: Span; } asmSemantics.addAttribute("root", { Root(_) { return this.asm; }, }); asmSemantics.addAttribute("asm", { ASM(asm, last) { const instructions = asm.children.map( (node) => node.intermediateInstruction as AsmInstruction, ) ?? []; return { instructions: last.child(0) ? [...instructions, last.child(0).instruction] : instructions, }; }, }); asmSemantics.addAttribute("intermediateInstruction", { intermediateInstruction(inst, _n) { return inst.instruction; }, }); function getAsmAssign(assignN: Node) { let assign = assignN.child(0)?.child(0)?.sourceString ?? ""; // The book (figure 4.5) specifies DM and ADM as the correct forms for destination, // but since the desktop simulators accept only MD and AMD we have decided to accept both */ if (assign == "DM") { assign = "MD"; } if (assign == "ADM") { assign = "AMD"; } if (assign != "" && !isAssignAsm(assign)) { const reversed = assign.split("").reverse().join(""); const suggestion = isAssignAsm(reversed) ? `. Did you mean ${reversed}?` : ""; throw createError( `Invalid ASM target: ${assign}${suggestion}`, span(assignN.source), ); } return assign; } function getAsmOp(opN: Node) { const op = opN.sourceString; if (!isCommandAsm(op)) { const reversed = op.split("").reverse().join(""); const suggestion = isCommandAsm(reversed) ? `. Did you mean ${reversed}?` : ""; throw createError( `Invalid ASM value: ${opN.sourceString}${suggestion}`, span(opN.source), ); } return op; } asmSemantics.addAttribute("instruction", { aInstruction(_at, name): AsmAInstruction { return A(name.value, span(this.source)); }, cInstruction(assignN, opN, jmpN): AsmCInstruction { const assign = getAsmAssign(assignN); const op = getAsmOp(opN) as COMMANDS_ASM; const jmp = (jmpN.child(0)?.child(1)?.sourceString ?? "") as JUMP_ASM; return C(assign as ASSIGN_ASM, op, jmp, span(this.source)); }, label(_o, { name }, _c): AsmLabelInstruction { return L(name, span(this.source)); }, }); export type Pointer = | "R0" | "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7" | "R8" | "R9" | "R10" | "R11" | "R12" | "R13" | "R14" | "R15" | "SP" | "LCL" | "ARG" | "THIS" | "THAT" | "SCREEN" | "KBD"; export function fillLabel( asm: Asm, symbolCallback?: (name: string, value: number, isVar: boolean) => void, ): Result { let nextLabel = 16; const symbols = new Map([ ["R0", 0], ["R1", 1], ["R2", 2], ["R3", 3], ["R4", 4], ["R5", 5], ["R6", 6], ["R7", 7], ["R8", 8], ["R9", 9], ["R10", 10], ["R11", 11], ["R12", 12], ["R13", 13], ["R14", 14], ["R15", 15], ["SP", 0], ["LCL", 1], ["ARG", 2], ["THIS", 3], ["THAT", 4], ["SCREEN", SCREEN_OFFSET], ["KBD", KEYBOARD_OFFSET], ]); function getLabelValue(label: string) { if (!symbols.has(label)) { symbols.set(label, nextLabel); symbolCallback?.(label, nextLabel, true); nextLabel += 1; } return assertExists(symbols.get(label), `Label not in symbols: ${label}`); } function transmuteAInstruction(instruction: AsmALabelInstruction) { const value = getLabelValue(instruction.label); (instruction as unknown as AsmAValueInstruction).value = value; delete (instruction as unknown as { label: undefined }).label; } const unfilled: AsmALabelInstruction[] = []; let line = 0; for (const instruction of asm.instructions) { if (instruction.type === "L") { if (symbols.has(instruction.label)) { return Err( createError(`Duplicate label ${instruction.label}`, instruction.span), ); } else { symbols.set(instruction.label, line); symbolCallback?.(instruction.label, line, false); } continue; } line += 1; if (instruction.type === "A") { if (isALabelInstruction(instruction)) { unfilled.push(instruction); } } } unfilled.forEach(transmuteAInstruction); return Ok(); } function writeCInst(inst: AsmCInstruction): string { return ( (inst.store ? `${ASSIGN.op[inst.store]}=` : "") + COMMANDS.op[inst.op] + (inst.jump ? `;${JUMP.op[inst.jump]}` : "") ); } export const AsmToString = (inst: AsmInstruction | string): string => { if (typeof inst === "string") return inst; switch (inst.type) { case "A": return isALabelInstruction(inst) ? `@${inst.label}` : `@${inst.value}`; case "L": return `(${inst.label})`; case "C": return writeCInst(inst); } }; export function translateInstruction(inst: AsmInstruction): number | undefined { if (inst.type === "A") { if (isALabelInstruction(inst)) { throw new Error(`ASM Emitting unfilled A instruction`); } return inst.value; } if (inst.type === "C") { return makeC( inst.isM, inst.op, (inst.store ?? 0) as ASSIGN_OP, (inst.jump ?? 0) as ASSIGN_OP, ); } return undefined; } export function emit(asm: Asm): number[] { return asm.instructions .map(translateInstruction) .filter((op): op is number => op !== undefined); } const A = (source: string | number, span?: Span): AsmAInstruction => typeof source === "string" ? { type: "A", label: source, span, } : { type: "A", value: source, span, }; const C = ( assign: ASSIGN_ASM, op: COMMANDS_ASM, jmp?: JUMP_ASM, span?: Span, ): AsmCInstruction => { const inst: AsmCInstruction = { type: "C", op: COMMANDS.getOp(op), isM: op.includes("M"), span, }; if (jmp) inst.jump = JUMP.asm[jmp]; if (assign) inst.store = ASSIGN.asm[assign]; return inst; }; const AC = ( source: string | number, assign: ASSIGN_ASM, op: COMMANDS_ASM, jmp?: JUMP_ASM, ) => [A(source), C(assign, op, jmp)]; const L = (label: string, span?: Span): AsmLabelInstruction => ({ type: "L", label, span, }); export const ASM = { grammar: asmGrammar, semantics: asmSemantics, parser: grammar, parse: makeParser(grammar, asmSemantics), passes: { fillLabel, emit, }, A, C, AC, L, };