Files
8bitworkshop/src/common/breakcond.ts
T

348 lines
13 KiB
TypeScript

// Breakpoint condition expressions: a small, safe expression language
// evaluated against the CPU state while the emulator is running.
// (No eval() -- a tiny recursive-descent parser compiles the expression
// to a closure, so bad input can only fail at compile time.)
//
// Values:
// numbers 123, $1a2f, 0x1a2f
// registers PC, A, X, Y, SP ... (numeric fields of the CPU state)
// symbols resolved from the debug symbol map (baked in at compile time)
// memory [expr] reads a byte at expr
// spaces #mem[expr] / #ram[expr] and #vram[expr] read a byte from the
// main or VRAM space; #mem16[expr], #vram16[expr], ... read a
// little-endian 16-bit word
// hardware #name reads a platform accessor (e.g. #scanline), namespaced
// behind '#' so it can never collide with a program symbol
// Operators (C-like precedence): ! ~ - + (unary), * / %, + -, << >>,
// < <= > >=, == !=, &, ^, |, &&, ||, and parentheses.
// The whole expression is true when it evaluates to non-zero.
// provided by the caller so this module stays dependency-free
export interface CondContext {
cpuFields: Set<string>; // valid register names (from getCPUState())
symbol: (name: string) => number | undefined; // debug symbol lookup
readMem?: (addr: number) => number | undefined;
readVRAM?: (addr: number) => number | undefined;
// platform accessors reachable with the '#' sigil (e.g. #scanline)
hw?: { [name: string]: () => number | undefined };
}
export type CondFn = (c: any) => boolean;
export type NumFn = (c: any) => number;
interface Token {
t: 'num' | 'id' | 'op';
v: string;
num?: number;
}
const OPERATORS2 = ['<<', '>>', '<=', '>=', '==', '!=', '&&', '||'];
const OPERATORS1 = '+-*/%<>!~&|^()[]#';
function tokenize(src: string): Token[] {
let toks: Token[] = [];
let i = 0;
while (i < src.length) {
let ch = src[i];
if (/\s/.test(ch)) { i++; continue; }
if (ch == '$' && /[0-9a-fA-F]/.test(src[i + 1] || '')) {
let j = i + 1;
while (j < src.length && /[0-9a-fA-F]/.test(src[j])) j++;
toks.push({ t: 'num', v: src.substring(i, j), num: parseInt(src.substring(i + 1, j), 16) });
i = j;
continue;
}
if (/[0-9]/.test(ch)) {
if (ch == '0' && (src[i + 1] == 'x' || src[i + 1] == 'X')) {
let j = i + 2;
while (j < src.length && /[0-9a-fA-F]/.test(src[j])) j++;
if (j == i + 2) throw new Error("bad hex number");
toks.push({ t: 'num', v: src.substring(i, j), num: parseInt(src.substring(i + 2, j), 16) });
i = j;
} else {
let j = i;
while (j < src.length && /[0-9]/.test(src[j])) j++;
toks.push({ t: 'num', v: src.substring(i, j), num: parseInt(src.substring(i, j), 10) });
i = j;
}
continue;
}
if (/[A-Za-z_.]/.test(ch)) {
let j = i;
while (j < src.length && /[A-Za-z0-9_.$]/.test(src[j])) j++;
toks.push({ t: 'id', v: src.substring(i, j) });
i = j;
continue;
}
let two = src.substring(i, i + 2);
if (OPERATORS2.includes(two)) {
toks.push({ t: 'op', v: two });
i += 2;
continue;
}
if (OPERATORS1.includes(ch)) {
toks.push({ t: 'op', v: ch });
i++;
continue;
}
throw new Error("unexpected character '" + ch + "'");
}
return toks;
}
type Node =
{ t: 'num', v: number } |
{ t: 'id', name: string } |
{ t: 'mem', a: Node } |
{ t: 'read', space: 'mem' | 'vram', width: 1 | 2, a: Node } |
{ t: 'hw', name: string } |
{ t: 'un', op: string, a: Node } |
{ t: 'bin', op: string, a: Node, b: Node };
class Parser {
toks: Token[];
pos = 0;
constructor(toks: Token[]) { this.toks = toks; }
peek(): Token { return this.toks[this.pos]; }
next(): Token { return this.toks[this.pos++]; }
atOp(...ops: string[]): string {
let t = this.peek();
return (t && t.t == 'op' && ops.includes(t.v)) ? t.v : null;
}
expect(v: string) {
let t = this.next();
if (!t || t.t != 'op' || t.v != v) throw new Error("expected '" + v + "'");
}
parse(): Node {
let n = this.parseOr();
if (this.pos != this.toks.length) throw new Error("unexpected '" + this.peek().v + "'");
return n;
}
parseOr(): Node {
let a = this.parseAnd();
while (this.atOp('||')) { this.next(); a = { t: 'bin', op: '||', a: a, b: this.parseAnd() }; }
return a;
}
parseAnd(): Node {
let a = this.parseBitOr();
while (this.atOp('&&')) { this.next(); a = { t: 'bin', op: '&&', a: a, b: this.parseBitOr() }; }
return a;
}
parseBitOr(): Node {
let a = this.parseBitXor();
while (this.atOp('|')) { this.next(); a = { t: 'bin', op: '|', a: a, b: this.parseBitXor() }; }
return a;
}
parseBitXor(): Node {
let a = this.parseBitAnd();
while (this.atOp('^')) { this.next(); a = { t: 'bin', op: '^', a: a, b: this.parseBitAnd() }; }
return a;
}
parseBitAnd(): Node {
let a = this.parseEquality();
while (this.atOp('&')) { this.next(); a = { t: 'bin', op: '&', a: a, b: this.parseEquality() }; }
return a;
}
parseEquality(): Node {
let a = this.parseRelational();
let op;
while (op = this.atOp('==', '!=')) { this.next(); a = { t: 'bin', op: op, a: a, b: this.parseRelational() }; }
return a;
}
parseRelational(): Node {
let a = this.parseShift();
let op;
while (op = this.atOp('<', '<=', '>', '>=')) { this.next(); a = { t: 'bin', op: op, a: a, b: this.parseShift() }; }
return a;
}
parseShift(): Node {
let a = this.parseAdditive();
let op;
while (op = this.atOp('<<', '>>')) { this.next(); a = { t: 'bin', op: op, a: a, b: this.parseAdditive() }; }
return a;
}
parseAdditive(): Node {
let a = this.parseMultiplicative();
let op;
while (op = this.atOp('+', '-')) { this.next(); a = { t: 'bin', op: op, a: a, b: this.parseMultiplicative() }; }
return a;
}
parseMultiplicative(): Node {
let a = this.parseUnary();
let op;
while (op = this.atOp('*', '/', '%')) { this.next(); a = { t: 'bin', op: op, a: a, b: this.parseUnary() }; }
return a;
}
parseUnary(): Node {
let op = this.atOp('-', '!', '~', '+');
if (op) { this.next(); return { t: 'un', op: op, a: this.parseUnary() }; }
return this.parsePrimary();
}
parsePrimary(): Node {
let t = this.next();
if (!t) throw new Error("unexpected end of expression");
if (t.t == 'num') return { t: 'num', v: t.num };
if (t.t == 'id') return { t: 'id', name: t.v };
if (t.t == 'op' && t.v == '(') {
let n = this.parseOr();
this.expect(')');
return n;
}
if (t.t == 'op' && t.v == '[') {
let n = this.parseOr();
this.expect(']');
return { t: 'mem', a: n };
}
if (t.t == 'op' && t.v == '#') {
let id = this.next();
if (!id || id.t != 'id') throw new Error("expected a name after '#'");
if (this.atOp('[')) {
this.next();
let n = this.parseOr();
this.expect(']');
// #mem/#ram and #vram, optionally with a 16-bit width suffix
let m = /^(mem|ram|vram)(16)?$/i.exec(id.v);
if (!m) throw new Error("unknown memory space '#" + id.v + "'");
let space: 'mem' | 'vram' = m[1].toLowerCase() == 'vram' ? 'vram' : 'mem';
return { t: 'read', space: space, width: m[2] ? 2 : 1, a: n };
}
return { t: 'hw', name: id.v };
}
throw new Error("unexpected '" + t.v + "'");
}
}
function compileNode(n: Node, ctx: CondContext): NumFn {
switch (n.t) {
case 'num': {
const v = n.v;
return () => v;
}
case 'id': {
const name = n.name;
if (ctx.cpuFields.has(name)) {
return (c) => {
const v = c[name];
return typeof v === 'number' ? v : 0;
};
}
let v = ctx.symbol(name);
if (typeof v === 'number') {
const val = v;
return () => val;
}
throw new Error("unknown identifier '" + name + "'");
}
case 'mem': {
const a = compileNode(n.a, ctx);
return (c) => {
if (!ctx.readMem) throw new Error("memory reads not supported");
const v = ctx.readMem(a(c) & 0xffff);
if (typeof v !== 'number') throw new Error("cannot read memory");
return v;
};
}
case 'read': {
const a = compileNode(n.a, ctx);
const space = n.space;
const width = n.width;
return (c) => {
const rd = space == 'vram' ? ctx.readVRAM : ctx.readMem;
if (!rd) throw new Error(space == 'vram' ? "VRAM reads not supported" : "memory reads not supported");
const addr = a(c) & 0xffff;
let v = rd(addr);
if (typeof v !== 'number') throw new Error("cannot read memory");
if (width == 2) {
let hi = rd((addr + 1) & 0xffff);
if (typeof hi !== 'number') throw new Error("cannot read memory");
v = (v | (hi << 8)) & 0xffff;
}
return v;
};
}
case 'hw': {
const name = n.name;
const acc = ctx.hw && ctx.hw[name];
if (!acc) throw new Error("unknown '#" + name + "'");
return () => {
const v = acc();
return typeof v === 'number' ? v : 0;
};
}
case 'un': {
const a = compileNode(n.a, ctx);
switch (n.op) {
case '-': return (c) => -a(c);
case '~': return (c) => ~a(c);
case '!': return (c) => a(c) ? 0 : 1;
case '+': return a;
}
break;
}
case 'bin': {
const a = compileNode(n.a, ctx);
const b = compileNode(n.b, ctx);
switch (n.op) {
case '||': return (c) => (a(c) || b(c)) ? 1 : 0;
case '&&': return (c) => (a(c) && b(c)) ? 1 : 0;
case '|': return (c) => a(c) | b(c);
case '^': return (c) => a(c) ^ b(c);
case '&': return (c) => a(c) & b(c);
case '==': return (c) => a(c) == b(c) ? 1 : 0;
case '!=': return (c) => a(c) != b(c) ? 1 : 0;
case '<': return (c) => a(c) < b(c) ? 1 : 0;
case '<=': return (c) => a(c) <= b(c) ? 1 : 0;
case '>': return (c) => a(c) > b(c) ? 1 : 0;
case '>=': return (c) => a(c) >= b(c) ? 1 : 0;
case '<<': return (c) => a(c) << b(c);
case '>>': return (c) => a(c) >> b(c);
case '+': return (c) => a(c) + b(c);
case '-': return (c) => a(c) - b(c);
case '*': return (c) => a(c) * b(c);
case '/': return (c) => { const d = b(c); return d === 0 ? 0 : (a(c) / d) | 0; };
case '%': return (c) => { const d = b(c); return d === 0 ? 0 : a(c) % d; };
}
break;
}
}
throw new Error("bad expression node");
}
// Compile a condition expression; throws on syntax errors or unknown identifiers.
// The returned function never throws: if evaluation fails (e.g. unreadable
// memory) it returns false so the emulator loop is never interrupted.
export function compileCondition(src: string, ctx: CondContext): CondFn {
let fn = compileExpression(src, ctx);
return (c) => {
try {
return fn(c) != 0;
} catch (e) {
return false;
}
};
}
// Compile an expression to its value, for watches and hovers. Unlike a
// condition, evaluating it throws when it fails, so the caller can say why.
export function compileExpression(src: string, ctx: CondContext): NumFn {
return compileNode(new Parser(tokenize(src)).parse(), ctx);
}
// Parse a breakpoint target: '$hex', '0xhex', decimal, or a symbol name.
export function parseTarget(target: string, symbol: (name: string) => number | undefined): { pc?: number, error?: string } {
let s = (target || '').trim();
if (!s) return { error: "empty address" };
let m;
if ((m = s.match(/^\$([0-9a-fA-F]+)$/)) || (m = s.match(/^0[xX]([0-9a-fA-F]+)$/))) {
return { pc: parseInt(m[1], 16) };
}
if (/^\d+$/.test(s)) {
return { pc: parseInt(s, 10) };
}
let v = symbol(s);
if (typeof v === 'number') return { pc: v };
return { error: "unknown symbol '" + s + "'" };
}