Files
WebMetal/frontend/src/machine/compileGraph.test.ts
T
2026-07-19 18:05:04 +02:00

125 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { WmEdge, WmNode } from '../editor/nodeTypes'
import { compileGraph } from './compileGraph'
let counter = 0
function node(
type: string,
name: string,
params: Record<string, string | number> = {},
): WmNode {
return {
id: `n${++counter}`,
type,
position: { x: 0, y: 0 },
data: { name, doc: '', params },
}
}
function pcToMemEdge(pc: WmNode, mem: WmNode): WmEdge {
return {
id: `e${++counter}`,
source: pc.id,
sourceHandle: 'out',
target: mem.id,
targetHandle: 'addr',
data: { kind: 'data' },
}
}
function fullGraph() {
const pc = node('pc', 'PC1', { width: 12 })
const mem = node('memory', 'MAIN', { size: 4096, width: 8 })
const acc = node('register', 'ACC', { width: 8 })
const rf = node('registerFile', 'R', { count: 4, width: 8 })
const flags = node('flags', 'FLAGS1', { flags: 'Z, N, C' })
const alu = node('alu', 'ALU1', { width: 8 })
const note = node('comment', 'a note!', { text: 'hello' })
return {
nodes: [pc, mem, acc, rf, flags, alu, note],
edges: [pcToMemEdge(pc, mem)],
}
}
describe('compileGraph', () => {
it('compiles a full graph into a valid machine model', () => {
const { nodes, edges } = fullGraph()
const { model, diagnostics } = compileGraph(nodes, edges)
expect(diagnostics.filter((d) => d.severity === 'error')).toEqual([])
expect(model).not.toBeNull()
if (!model) return
expect(model.banks).toEqual([
{ name: 'ACC', width: 8, count: 1, sourceNodeId: expect.any(String) },
{ name: 'R', width: 8, count: 4, sourceNodeId: expect.any(String) },
])
expect(model.memories).toEqual([
{
name: 'MAIN',
size: 4096,
width: 8,
sourceNodeId: expect.any(String),
},
])
expect(model.flags).toEqual(['Z', 'N', 'C'])
expect(model.pc).toEqual({ width: 12 })
expect(model.programMemory).toBe('MAIN')
})
it('is deterministic regardless of node order', () => {
const { nodes, edges } = fullGraph()
const forward = compileGraph(nodes, edges).model
const backward = compileGraph([...nodes].reverse(), edges).model
// sourceNodeIds differ per construction run, so compare shapes without them
const strip = (m: typeof forward) =>
JSON.parse(
JSON.stringify(m, (k, v: unknown) =>
k === 'sourceNodeId' ? undefined : v,
),
) as unknown
expect(strip(backward)).toEqual(strip(forward))
})
it('returns no model when validation errors exist', () => {
const { model, diagnostics } = compileGraph([], [])
expect(model).toBeNull()
expect(diagnostics.some((d) => d.severity === 'error')).toBe(true)
})
it('resolves the program memory via the PC wire when there are several memories', () => {
const { nodes, edges } = fullGraph()
nodes.push(node('memory', 'DATA', { size: 256, width: 8 }))
const { model } = compileGraph(nodes, edges)
expect(model?.programMemory).toBe('MAIN')
})
it('errors when the program memory is ambiguous', () => {
const { nodes } = fullGraph()
nodes.push(node('memory', 'DATA', { size: 256, width: 8 }))
const { model, diagnostics } = compileGraph(nodes, []) // no PC wire
expect(model).toBeNull()
expect(diagnostics.map((d) => d.message).join()).toContain(
'cannot determine the program memory',
)
})
it('rejects bad flag names', () => {
const { nodes, edges } = fullGraph()
const flags = nodes.find((n) => n.type === 'flags')
if (flags) flags.data.params = { flags: 'Z,9bad,Z' }
const { model, diagnostics } = compileGraph(nodes, edges)
expect(model).toBeNull()
const text = diagnostics.map((d) => d.message).join()
expect(text).toContain('not a valid identifier')
expect(text).toContain('duplicate flag name')
})
it('clamps out-of-range params into the schema range', () => {
const { nodes, edges } = fullGraph()
const acc = nodes.find((n) => n.data.name === 'ACC')
if (acc) acc.data.params = { width: 99 }
const { model } = compileGraph(nodes, edges)
expect(model?.banks.find((b) => b.name === 'ACC')?.width).toBe(32)
})
})