The demo below shows a small expression tree. Each node can be in check mode (the type flows in from outside) or synthesis mode (the type must be computed from inside). Click a node to toggle whether it has an annotation. The algorithm propagates types and highlights which nodes still need one.
<!-- Interactive bidirectional type checking demo -->
<div class="toolbar">
<span class="label">Expression tree — click annotable nodes to toggle annotations</span>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="tree-container"></div>
<div id="legend" class="legend">
<span class="badge check">⇐ Check mode</span>
<span class="badge synth">⇒ Synthesis mode</span>
<span class="badge error">⚠ Error</span>
</div>
<div id="status" class="status"></div>
<p class="hint">Click the top node to toggle its annotation. When the annotation is on, the whole tree is in check mode. When it is off, lambdas without a known argument type become errors.</p>
/* Bidirectional type demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; padding: .6rem; color: #222; }
.toolbar { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; }
.label { font-size: .85rem; color: #555; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border-radius: 7px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
#tree-container { overflow-x: auto; padding-bottom: .4rem; }
/* Tree layout */
.tree { display: flex; flex-direction: column; align-items: flex-start; gap: 0; }
.node-row { display: flex; align-items: center; gap: 0; }
.indent { display: inline-block; width: 18px; flex-shrink: 0; }
.connector { display: inline-block; width: 18px; color: #aaa; font-size: 13px;
flex-shrink: 0; user-select: none; }
.node { display: inline-flex; align-items: center; gap: 5px; padding: .28rem .55rem;
border-radius: 7px; border: 1.5px solid #ccc; font: 500 13px ui-monospace, monospace;
cursor: pointer; user-select: none; transition: background .12s, border-color .12s;
margin: 2px 0; }
.node:hover { filter: brightness(.94); }
.node.check { background: #dbeafe; border-color: #3b82f6; }
.node.synth { background: #dcfce7; border-color: #22c55e; }
.node.error { background: #fee2e2; border-color: #ef4444; }
.node .kind { font-size: 10px; opacity: .7; }
.node .annot { font-size: 11px; background: #fff; border: 1px solid #999; border-radius: 4px;
padding: 0 4px; margin-left: 3px; color: #444; }
.badge { font-size: .78rem; padding: .2rem .5rem; border-radius: 6px; }
.badge.check { background: #dbeafe; color: #1d4ed8; }
.badge.synth { background: #dcfce7; color: #15803d; }
.badge.error { background: #fee2e2; color: #dc2626; }
.legend { display: flex; gap: .6rem; flex-wrap: wrap; margin: .5rem 0; }
.status { font: 600 .9rem system-ui; min-height: 1.3em; margin: .3rem 0; }
.status.ok { color: #15803d; }
.status.err { color: #dc2626; }
.hint { font-size: .82rem; color: #666; margin: .4rem 0 0; line-height: 1.4; }
// Bidirectional type checking demo — two modes:
// check (<=): verify expression e against known type A
// synth (=>): compute type A from the structure of e
// Rules: Var=>synth, Lam<=check, App=>synth, Ann:check then synth
// Nodes with annotations always switch to check mode for their body.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Expression tree: let result: Int = add 3 4
// let-binding node wrapping a function application
const EXPR = {
id: 'let',
kind: 'let',
label: 'let result =',
annot: 'Int',
annotable: true,
children: [
{
id: 'app',
kind: 'app',
label: 'add 3 4',
annotable: false,
children: [
{ id: 'fn_add', kind: 'var', label: 'add', annotable: false, children: [] },
{ id: 'arg1', kind: 'lit', label: '3', annotable: false, children: [] },
{ id: 'arg2', kind: 'lit', label: '4', annotable: false, children: [] }
]
}
]
};
// annotation toggle state: true = annotation present
const annotOn = { let: true };
// Type environment: known function and variable types
const ENV = { fn_add: 'Int -> Int -> Int', result: 'Int', let: 'Int' };
// infer(node, expected): synthesize or check based on mode
// synthDirect: compute type without re-entering the Ann rule
function synthDirect(node) {
if (node.kind === 'lit') return { type: 'Int', ok: true };
if (node.kind === 'var' || node.kind === 'let') {
const t = ENV[node.id] || null;
return t ? { type: t, ok: true } : { type: '?', ok: false };
}
if (node.kind === 'app') {
const fn = synthDirect(node.children[0] || {});
if (!fn.ok || !fn.type.includes('->')) return { type: '?', ok: false };
const retType = fn.type.split(' -> ').slice(1).join(' -> ');
return { type: retType, ok: true };
}
return { type: '?', ok: false };
}
function infer(node, expected) {
const hasAnnot = annotOn[node.id] && node.annotable && node.annot;
if (hasAnnot) {
// Ann rule: annotated node checks body against its declared type
return check(node, node.annot);
}
if (node.kind === 'lit') {
// Lit rule: integer literals synthesize Int
return { mode: 'synth', type: 'Int', ok: true };
}
if (node.kind === 'var' || node.kind === 'let') {
const t = ENV[node.id] || null;
return t ? { mode: 'synth', type: t, ok: true }
: { mode: 'synth', type: '?', ok: false };
}
if (node.kind === 'app') {
// App rule: synthesize function type, check argument
const fnResult = infer(node.children[0] || {}, null);
if (!fnResult.ok || !fnResult.type.includes('->')) {
return { mode: 'synth', type: '?', ok: false };
}
const parts = fnResult.type.split(' -> ');
const argType = parts[0];
const retType = parts.slice(1).join(' -> ');
(node.children || []).forEach((ch, i) => {
if (i > 0) infer(ch, argType);
});
return { mode: 'synth', type: retType, ok: true };
}
if (node.kind === 'lam') {
// Lam<=check rule: check lambda body against expected return type
if (expected && expected.includes('->')) {
const parts = expected.split(' -> ');
const retType = parts.slice(1).join(' -> ');
infer(node.children[0] || {}, retType);
return { mode: 'check', type: expected, ok: true };
}
// Lam without expected type: error — cannot synthesize argument type
return { mode: 'error', type: '?', ok: false };
}
return { mode: 'synth', type: '?', ok: false };
}
function check(node, expected) {
if (node.kind === 'lam') {
if (expected && expected.includes('->')) {
const parts = expected.split(' -> ');
const retType = parts.slice(1).join(' -> ');
infer(node.children[0] || {}, retType);
return { mode: 'check', type: expected, ok: true };
}
return { mode: 'error', type: '?', ok: false };
}
// check fallback: synthesize then verify the type matches
const r = synthDirect(node);
if (!r.ok) return { mode: 'error', type: '?', ok: false };
const match = !expected || r.type === expected || r.type === '?';
return { mode: 'check', type: expected || r.type, ok: match };
}
// walk the tree, threading expected types downward
const nodeResults = {};
function walk(node, expected) {
const r = infer(node, expected);
nodeResults[node.id] = r;
(node.children || []).forEach(ch => walk(ch, r.mode === 'check' ? r.type : null));
}
function runAlgorithm() {
Object.keys(nodeResults).forEach(k => delete nodeResults[k]);
walk(EXPR, null);
}
// Render the expression tree with mode badges
function renderTree(node, depth, isLast) {
const container = document.getElementById('tree-container');
if (depth === 0) container.innerHTML = '<div class="tree"></div>';
const tree = container.querySelector('.tree');
const r = nodeResults[node.id] || { mode: 'synth', type: '?', ok: true };
const modeClass = r.ok ? r.mode : 'error';
const row = document.createElement('div');
row.className = 'node-row';
for (let i = 0; i < depth; i++) {
const ind = document.createElement('span');
ind.className = 'indent';
row.appendChild(ind);
}
if (depth > 0) {
const conn = document.createElement('span');
conn.className = 'connector';
conn.textContent = isLast ? '└─' : '├─';
row.appendChild(conn);
}
const nd = document.createElement('div');
nd.className = 'node ' + modeClass;
nd.title = (r.mode === 'check' ? '⇐ ' : r.mode === 'error' ? '⚠ ' : '⇒ ') + (r.type || '?');
const kindSpan = document.createElement('span');
kindSpan.className = 'kind';
kindSpan.textContent = node.kind;
nd.appendChild(kindSpan);
const lbl = document.createElement('span');
lbl.textContent = ' ' + node.label;
nd.appendChild(lbl);
if (node.annotable && annotOn[node.id]) {
const a = document.createElement('span');
a.className = 'annot';
a.textContent = ': ' + node.annot;
nd.appendChild(a);
}
if (node.annotable) {
nd.addEventListener('click', () => {
annotOn[node.id] = !annotOn[node.id];
runAlgorithm();
rerenderAll();
updateStatus();
});
}
row.appendChild(nd);
tree.appendChild(row);
(node.children || []).forEach((ch, i) => {
const last = i === node.children.length - 1;
renderNode(ch, depth + 1, last, tree);
});
}
function renderNode(node, depth, isLast, tree) {
const r = nodeResults[node.id] || { mode: 'synth', type: '?', ok: true };
const modeClass = r.ok ? r.mode : 'error';
const row = document.createElement('div');
row.className = 'node-row';
for (let i = 0; i < depth; i++) {
const ind = document.createElement('span');
ind.className = 'indent';
row.appendChild(ind);
}
if (depth > 0) {
const conn = document.createElement('span');
conn.className = 'connector';
conn.textContent = isLast ? '└─' : '├─';
row.appendChild(conn);
}
const nd = document.createElement('div');
nd.className = 'node ' + modeClass;
nd.title = (r.mode === 'check' ? '⇐ ' : r.mode === 'error' ? '⚠ ' : '⇒ ') + (r.type || '?');
const kindSpan = document.createElement('span');
kindSpan.className = 'kind';
kindSpan.textContent = node.kind;
nd.appendChild(kindSpan);
const lbl = document.createElement('span');
lbl.textContent = ' ' + node.label;
nd.appendChild(lbl);
if (node.annotable && annotOn[node.id]) {
const a = document.createElement('span');
a.className = 'annot';
a.textContent = ': ' + node.annot;
nd.appendChild(a);
}
if (node.annotable) {
nd.addEventListener('click', () => {
annotOn[node.id] = !annotOn[node.id];
runAlgorithm();
rerenderAll();
updateStatus();
});
}
row.appendChild(nd);
tree.appendChild(row);
(node.children || []).forEach((ch, i) => {
const last = i === node.children.length - 1;
renderNode(ch, depth + 1, last, tree);
});
}
function rerenderAll() {
const container = document.getElementById('tree-container');
container.innerHTML = '<div class="tree"></div>';
const tree = container.querySelector('.tree');
renderNode(EXPR, 0, true, tree);
(EXPR.children || []).forEach((ch, i) => {
const last = i === EXPR.children.length - 1;
renderNode(ch, 1, last, tree);
});
}
function countErrors() {
return Object.values(nodeResults).filter(r => !r.ok).length;
}
function updateStatus() {
const s = document.getElementById('status');
const errs = countErrors();
if (errs === 0) {
s.textContent = '✓ All nodes typed correctly.';
s.className = 'status ok';
} else {
s.textContent = i18nFmt('✗ %n% node(s) need an annotation or have a type error.', { n: errs });
s.className = 'status err';
}
}
function fullRender() {
runAlgorithm();
const container = document.getElementById('tree-container');
container.innerHTML = '<div class="tree"></div>';
const tree = container.querySelector('.tree');
function rec(node, depth, isLast) {
const r = nodeResults[node.id] || { mode: 'synth', type: '?', ok: true };
const modeClass = r.ok ? r.mode : 'error';
const row = document.createElement('div');
row.className = 'node-row';
for (let i = 0; i < depth; i++) {
const ind = document.createElement('span');
ind.className = 'indent';
row.appendChild(ind);
}
if (depth > 0) {
const conn = document.createElement('span');
conn.className = 'connector';
conn.textContent = isLast ? '└─' : '├─';
row.appendChild(conn);
}
const nd = document.createElement('div');
nd.className = 'node ' + modeClass;
nd.title = (r.mode === 'check' ? '⇐ ' : r.mode === 'error' ? '⚠ ' : '⇒ ') + (r.type || '?');
const kindSpan = document.createElement('span');
kindSpan.className = 'kind';
kindSpan.textContent = node.kind;
nd.appendChild(kindSpan);
const lbl = document.createElement('span');
lbl.textContent = ' ' + node.label;
nd.appendChild(lbl);
if (node.annotable && annotOn[node.id]) {
const a = document.createElement('span');
a.className = 'annot';
a.textContent = ': ' + node.annot;
nd.appendChild(a);
}
if (node.annotable) {
nd.addEventListener('click', () => {
annotOn[node.id] = !annotOn[node.id];
runAlgorithm();
fullRender();
updateStatus();
});
}
row.appendChild(nd);
tree.appendChild(row);
(node.children || []).forEach((ch, i) => rec(ch, depth + 1, i === node.children.length - 1));
}
rec(EXPR, 0, true);
updateStatus();
}
document.getElementById('btn-reset').addEventListener('click', () => {
Object.keys(annotOn).forEach(k => delete annotOn[k]);
annotOn['let'] = true;
fullRender();
});
fullRender();
<!-- Interactive bidirectional type checking demo -->
<div class="toolbar">
<span class="label">Expression tree — click annotable nodes to toggle annotations</span>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="tree-container"></div>
<div id="legend" class="legend">
<span class="badge check">⇐ Check mode</span>
<span class="badge synth">⇒ Synthesis mode</span>
<span class="badge error">⚠ Error</span>
</div>
<div id="status" class="status"></div>
<p class="hint">Click the top node to toggle its annotation. When the annotation is on, the whole tree is in check mode. When it is off, lambdas without a known argument type become errors.</p>
/* Bidirectional type demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; padding: .6rem; color: #222; }
.toolbar { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; }
.label { font-size: .85rem; color: #555; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border-radius: 7px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
#tree-container { overflow-x: auto; padding-bottom: .4rem; }
/* Tree layout */
.tree { display: flex; flex-direction: column; align-items: flex-start; gap: 0; }
.node-row { display: flex; align-items: center; gap: 0; }
.indent { display: inline-block; width: 18px; flex-shrink: 0; }
.connector { display: inline-block; width: 18px; color: #aaa; font-size: 13px;
flex-shrink: 0; user-select: none; }
.node { display: inline-flex; align-items: center; gap: 5px; padding: .28rem .55rem;
border-radius: 7px; border: 1.5px solid #ccc; font: 500 13px ui-monospace, monospace;
cursor: pointer; user-select: none; transition: background .12s, border-color .12s;
margin: 2px 0; }
.node:hover { filter: brightness(.94); }
.node.check { background: #dbeafe; border-color: #3b82f6; }
.node.synth { background: #dcfce7; border-color: #22c55e; }
.node.error { background: #fee2e2; border-color: #ef4444; }
.node .kind { font-size: 10px; opacity: .7; }
.node .annot { font-size: 11px; background: #fff; border: 1px solid #999; border-radius: 4px;
padding: 0 4px; margin-left: 3px; color: #444; }
.badge { font-size: .78rem; padding: .2rem .5rem; border-radius: 6px; }
.badge.check { background: #dbeafe; color: #1d4ed8; }
.badge.synth { background: #dcfce7; color: #15803d; }
.badge.error { background: #fee2e2; color: #dc2626; }
.legend { display: flex; gap: .6rem; flex-wrap: wrap; margin: .5rem 0; }
.status { font: 600 .9rem system-ui; min-height: 1.3em; margin: .3rem 0; }
.status.ok { color: #15803d; }
.status.err { color: #dc2626; }
.hint { font-size: .82rem; color: #666; margin: .4rem 0 0; line-height: 1.4; }
// Bidirectional type checking demo — two modes:
// check (<=): verify expression e against known type A
// synth (=>): compute type A from the structure of e
// Rules: Var=>synth, Lam<=check, App=>synth, Ann:check then synth
// Nodes with annotations always switch to check mode for their body.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Expression tree: let result: Int = add 3 4
// let-binding node wrapping a function application
const EXPR = {
id: 'let',
kind: 'let',
label: 'let result =',
annot: 'Int',
annotable: true,
children: [
{
id: 'app',
kind: 'app',
label: 'add 3 4',
annotable: false,
children: [
{ id: 'fn_add', kind: 'var', label: 'add', annotable: false, children: [] },
{ id: 'arg1', kind: 'lit', label: '3', annotable: false, children: [] },
{ id: 'arg2', kind: 'lit', label: '4', annotable: false, children: [] }
]
}
]
};
// annotation toggle state: true = annotation present
const annotOn = { let: true };
// Type environment: known function and variable types
const ENV = { fn_add: 'Int -> Int -> Int', result: 'Int', let: 'Int' };
// infer(node, expected): synthesize or check based on mode
// synthDirect: compute type without re-entering the Ann rule
function synthDirect(node) {
if (node.kind === 'lit') return { type: 'Int', ok: true };
if (node.kind === 'var' || node.kind === 'let') {
const t = ENV[node.id] || null;
return t ? { type: t, ok: true } : { type: '?', ok: false };
}
if (node.kind === 'app') {
const fn = synthDirect(node.children[0] || {});
if (!fn.ok || !fn.type.includes('->')) return { type: '?', ok: false };
const retType = fn.type.split(' -> ').slice(1).join(' -> ');
return { type: retType, ok: true };
}
return { type: '?', ok: false };
}
function infer(node, expected) {
const hasAnnot = annotOn[node.id] && node.annotable && node.annot;
if (hasAnnot) {
// Ann rule: annotated node checks body against its declared type
return check(node, node.annot);
}
if (node.kind === 'lit') {
// Lit rule: integer literals synthesize Int
return { mode: 'synth', type: 'Int', ok: true };
}
if (node.kind === 'var' || node.kind === 'let') {
const t = ENV[node.id] || null;
return t ? { mode: 'synth', type: t, ok: true }
: { mode: 'synth', type: '?', ok: false };
}
if (node.kind === 'app') {
// App rule: synthesize function type, check argument
const fnResult = infer(node.children[0] || {}, null);
if (!fnResult.ok || !fnResult.type.includes('->')) {
return { mode: 'synth', type: '?', ok: false };
}
const parts = fnResult.type.split(' -> ');
const argType = parts[0];
const retType = parts.slice(1).join(' -> ');
(node.children || []).forEach((ch, i) => {
if (i > 0) infer(ch, argType);
});
return { mode: 'synth', type: retType, ok: true };
}
if (node.kind === 'lam') {
// Lam<=check rule: check lambda body against expected return type
if (expected && expected.includes('->')) {
const parts = expected.split(' -> ');
const retType = parts.slice(1).join(' -> ');
infer(node.children[0] || {}, retType);
return { mode: 'check', type: expected, ok: true };
}
// Lam without expected type: error — cannot synthesize argument type
return { mode: 'error', type: '?', ok: false };
}
return { mode: 'synth', type: '?', ok: false };
}
function check(node, expected) {
if (node.kind === 'lam') {
if (expected && expected.includes('->')) {
const parts = expected.split(' -> ');
const retType = parts.slice(1).join(' -> ');
infer(node.children[0] || {}, retType);
return { mode: 'check', type: expected, ok: true };
}
return { mode: 'error', type: '?', ok: false };
}
// check fallback: synthesize then verify the type matches
const r = synthDirect(node);
if (!r.ok) return { mode: 'error', type: '?', ok: false };
const match = !expected || r.type === expected || r.type === '?';
return { mode: 'check', type: expected || r.type, ok: match };
}
// walk the tree, threading expected types downward
const nodeResults = {};
function walk(node, expected) {
const r = infer(node, expected);
nodeResults[node.id] = r;
(node.children || []).forEach(ch => walk(ch, r.mode === 'check' ? r.type : null));
}
function runAlgorithm() {
Object.keys(nodeResults).forEach(k => delete nodeResults[k]);
walk(EXPR, null);
}
// Render the expression tree with mode badges
function renderTree(node, depth, isLast) {
const container = document.getElementById('tree-container');
if (depth === 0) container.innerHTML = '<div class="tree"></div>';
const tree = container.querySelector('.tree');
const r = nodeResults[node.id] || { mode: 'synth', type: '?', ok: true };
const modeClass = r.ok ? r.mode : 'error';
const row = document.createElement('div');
row.className = 'node-row';
for (let i = 0; i < depth; i++) {
const ind = document.createElement('span');
ind.className = 'indent';
row.appendChild(ind);
}
if (depth > 0) {
const conn = document.createElement('span');
conn.className = 'connector';
conn.textContent = isLast ? '└─' : '├─';
row.appendChild(conn);
}
const nd = document.createElement('div');
nd.className = 'node ' + modeClass;
nd.title = (r.mode === 'check' ? '⇐ ' : r.mode === 'error' ? '⚠ ' : '⇒ ') + (r.type || '?');
const kindSpan = document.createElement('span');
kindSpan.className = 'kind';
kindSpan.textContent = node.kind;
nd.appendChild(kindSpan);
const lbl = document.createElement('span');
lbl.textContent = ' ' + node.label;
nd.appendChild(lbl);
if (node.annotable && annotOn[node.id]) {
const a = document.createElement('span');
a.className = 'annot';
a.textContent = ': ' + node.annot;
nd.appendChild(a);
}
if (node.annotable) {
nd.addEventListener('click', () => {
annotOn[node.id] = !annotOn[node.id];
runAlgorithm();
rerenderAll();
updateStatus();
});
}
row.appendChild(nd);
tree.appendChild(row);
(node.children || []).forEach((ch, i) => {
const last = i === node.children.length - 1;
renderNode(ch, depth + 1, last, tree);
});
}
function renderNode(node, depth, isLast, tree) {
const r = nodeResults[node.id] || { mode: 'synth', type: '?', ok: true };
const modeClass = r.ok ? r.mode : 'error';
const row = document.createElement('div');
row.className = 'node-row';
for (let i = 0; i < depth; i++) {
const ind = document.createElement('span');
ind.className = 'indent';
row.appendChild(ind);
}
if (depth > 0) {
const conn = document.createElement('span');
conn.className = 'connector';
conn.textContent = isLast ? '└─' : '├─';
row.appendChild(conn);
}
const nd = document.createElement('div');
nd.className = 'node ' + modeClass;
nd.title = (r.mode === 'check' ? '⇐ ' : r.mode === 'error' ? '⚠ ' : '⇒ ') + (r.type || '?');
const kindSpan = document.createElement('span');
kindSpan.className = 'kind';
kindSpan.textContent = node.kind;
nd.appendChild(kindSpan);
const lbl = document.createElement('span');
lbl.textContent = ' ' + node.label;
nd.appendChild(lbl);
if (node.annotable && annotOn[node.id]) {
const a = document.createElement('span');
a.className = 'annot';
a.textContent = ': ' + node.annot;
nd.appendChild(a);
}
if (node.annotable) {
nd.addEventListener('click', () => {
annotOn[node.id] = !annotOn[node.id];
runAlgorithm();
rerenderAll();
updateStatus();
});
}
row.appendChild(nd);
tree.appendChild(row);
(node.children || []).forEach((ch, i) => {
const last = i === node.children.length - 1;
renderNode(ch, depth + 1, last, tree);
});
}
function rerenderAll() {
const container = document.getElementById('tree-container');
container.innerHTML = '<div class="tree"></div>';
const tree = container.querySelector('.tree');
renderNode(EXPR, 0, true, tree);
(EXPR.children || []).forEach((ch, i) => {
const last = i === EXPR.children.length - 1;
renderNode(ch, 1, last, tree);
});
}
function countErrors() {
return Object.values(nodeResults).filter(r => !r.ok).length;
}
function updateStatus() {
const s = document.getElementById('status');
const errs = countErrors();
if (errs === 0) {
s.textContent = '✓ All nodes typed correctly.';
s.className = 'status ok';
} else {
s.textContent = i18nFmt('✗ %n% node(s) need an annotation or have a type error.', { n: errs });
s.className = 'status err';
}
}
function fullRender() {
runAlgorithm();
const container = document.getElementById('tree-container');
container.innerHTML = '<div class="tree"></div>';
const tree = container.querySelector('.tree');
function rec(node, depth, isLast) {
const r = nodeResults[node.id] || { mode: 'synth', type: '?', ok: true };
const modeClass = r.ok ? r.mode : 'error';
const row = document.createElement('div');
row.className = 'node-row';
for (let i = 0; i < depth; i++) {
const ind = document.createElement('span');
ind.className = 'indent';
row.appendChild(ind);
}
if (depth > 0) {
const conn = document.createElement('span');
conn.className = 'connector';
conn.textContent = isLast ? '└─' : '├─';
row.appendChild(conn);
}
const nd = document.createElement('div');
nd.className = 'node ' + modeClass;
nd.title = (r.mode === 'check' ? '⇐ ' : r.mode === 'error' ? '⚠ ' : '⇒ ') + (r.type || '?');
const kindSpan = document.createElement('span');
kindSpan.className = 'kind';
kindSpan.textContent = node.kind;
nd.appendChild(kindSpan);
const lbl = document.createElement('span');
lbl.textContent = ' ' + node.label;
nd.appendChild(lbl);
if (node.annotable && annotOn[node.id]) {
const a = document.createElement('span');
a.className = 'annot';
a.textContent = ': ' + node.annot;
nd.appendChild(a);
}
if (node.annotable) {
nd.addEventListener('click', () => {
annotOn[node.id] = !annotOn[node.id];
runAlgorithm();
fullRender();
updateStatus();
});
}
row.appendChild(nd);
tree.appendChild(row);
(node.children || []).forEach((ch, i) => rec(ch, depth + 1, i === node.children.length - 1));
}
rec(EXPR, 0, true);
updateStatus();
}
document.getElementById('btn-reset').addEventListener('click', () => {
Object.keys(annotOn).forEach(k => delete annotOn[k]);
annotOn['let'] = true;
fullRender();
});
fullRender();
Comments
Loading comments...