function deepCopy(obj) { if (typeof obj === 'object') { const newObj = {} for (const prop in obj) { newObj[prop] = deepCopy(obj[prop]) } return newObj } else { return obj } } function LambdaDrawer(id) { const graph = document.getElementById(id) const ctx = graph.getContext('2d') const defaultAlign = 'center' ctx.font = '25px Arial' ctx.textAlign = defaultAlign ctx.textBaseline = 'middle' // config setting const r = 30 const layerGap = 40 const signWidth = 10 // functions const isId = (id) => id.id != null const isAbs = (abs) => abs.param != null const isApp = (app) => app.left != null const nodeBottom = (ast) => [ast.cx, ast.cy + r] const nodeTop = (ast) => [ast.cx, ast.cy - r] const astOperation = (ast, op) => { if (isId(ast)) { return op.id() } else if (isAbs(ast)) { return op.abs() } else if (isApp(ast)) { return op.app() } else { throw new Error('unknown ast type') } } const astToString = (ast) => { return astOperation(ast, { id: () => ast.name, abs: () => `λ${ast.param.name}.${astToString(ast.body)}`, app: () => `(${astToString(ast.left)} ${astToString(ast.right)})`, }) } const setSize = (ast) => { ast.str = astToString(ast) return astOperation(ast, { id() { // n ast.width = 2 * r ast.height = 2 * r }, abs() { setSize(ast.param) setSize(ast.body) // \param.(body) ast.width = ast.param.width + ast.body.width + 4 * signWidth ast.height = ast.body.height + 2 * r + layerGap }, app() { // (left,right) setSize(ast.left) setSize(ast.right) ast.width = ast.left.width + ast.right.width + 3 * signWidth ast.height = Math.max(ast.left.height, ast.right.height) + 2 * r + layerGap }, }) } const setPosition = (ast, x, y) => { ast.x = x ast.y = y return astOperation(ast, { id() { // n ast.cx = x + r ast.cy = y + r }, abs() { // \param.(body) const nextLayerY = y + 2 * r + layerGap setPosition(ast.param, x + signWidth, nextLayerY) setPosition(ast.body, x + ast.param.width + 3 * signWidth, nextLayerY) ast.cx = (ast.param.cx + ast.body.cx) / 2 ast.cy = y + r }, app() { // (left,right) const nextLayerY = y + 2 * r + layerGap setPosition(ast.left, x + signWidth, nextLayerY) setPosition(ast.right, x + ast.left.width + 2 * signWidth, nextLayerY) ast.cx = (ast.left.cx + ast.right.cx) / 2 ast.cy = y + r }, }) } const drawText = (value, cx, cy, align = defaultAlign) => { ctx.textAlign = align ctx.fillText(value, cx, cy) ctx.textAlign = defaultAlign } const drawLine = (sx, sy, fx, fy) => { ctx.beginPath() ctx.moveTo(sx, sy) ctx.lineTo(fx, fy) ctx.stroke() } const drawNode = (cx, cy, r, value) => { ctx.beginPath() ctx.arc(cx, cy, r, 0, 2 * Math.PI) ctx.stroke() ctx.fillText(value, cx, cy) } const _drawAst = (ast) => { astOperation(ast, { id() { const tag = ast.id >= 0 ? `${ast.name}:${ast.id}` : ast.name drawNode(ast.cx, ast.cy, r, tag) }, abs() { drawNode(ast.cx, ast.cy, r, 'Abs') _drawAst(ast.param) _drawAst(ast.body) const [sx, sy] = nodeBottom(ast) drawLine(sx, sy, ...nodeTop(ast.param)) drawLine(sx, sy, ...nodeTop(ast.body)) }, app() { drawNode(ast.cx, ast.cy, r, 'App') _drawAst(ast.left) _drawAst(ast.right) const [sx, sy] = nodeBottom(ast) drawLine(sx, sy, ...nodeTop(ast.left)) drawLine(sx, sy, ...nodeTop(ast.right)) }, }) } const drawAstStr = (ast) => { const x = ast.cx + r + signWidth / 2 astOperation(ast, { id() {}, abs() { drawText(ast.str, x, ast.cy, 'left') drawAstStr(ast.body) }, app() { drawText(ast.str, x, ast.cy, 'left') drawAstStr(ast.left) drawAstStr(ast.right) }, }) } const drawAstInfo = (ast) => { astOperation(ast, { id() { drawText(ast.name, ast.cx, 25) }, abs() { drawAstInfo(ast.param) drawAstInfo(ast.body) const signOffset = signWidth / 2 drawText('λ', ast.x + signOffset, 25) drawText('.', ast.param.cx + r + signOffset, 25) drawText('(', ast.body.x - signOffset, 25) drawText(')', ast.body.x + ast.body.width + signOffset, 25) }, app() { drawAstInfo(ast.left) drawAstInfo(ast.right) const signOffset = signWidth / 2 drawText('(', ast.x + signOffset, 25) drawText(',', ast.right.x - signOffset, 25) drawText(')', ast.right.x + ast.right.width + signOffset, 25) }, }) } const drawAst = (ast) => { console.log(astToString(ast)) console.log(ast) const margin = 50 ast = deepCopy(ast) setSize(ast) setPosition(ast, margin, margin) const [width, height] = [ast.width + margin * 2, ast.height + margin * 2] graph.width = width graph.height = height ctx.clearRect(0, 0, width, height) ctx.font = '25px Arial' ctx.textAlign = 'center' ctx.textBaseline = 'middle' _drawAst(ast) drawAstStr(ast) } const drawAtom = (ast) => { console.log(astToString(ast)) } const showSteps = (res) => { res.steps.forEach((ast) => { console.log(astToString(ast)) }) } return { drawAst, drawAtom, showSteps, } } const drawer = LambdaDrawer('graph') window._PLAYGROUND = { instance: drawer, draw: drawer.drawAst, }