The demo below simulates a cast boundary between a statically typed function and a dynamically typed caller. The typed function expects an integer; choose what value to pass and watch what happens.
<!-- Cast boundary demo — typed function zone on the left, untyped caller zone on the right -->
<div class="scene">
<div class="zone typed-zone">
<div class="zone-label">Typed region</div>
<div class="zone-desc">Statically typed function</div>
<div class="annotation">double(x: <span class="ann">Int</span>) → <span class="ann">Int</span></div>
</div>
<div class="boundary" title="Cast boundary — values are checked here at runtime">
<div class="boundary-arrow">→</div>
<div class="boundary-label">Cast boundary</div>
</div>
<div class="zone untyped-zone">
<div class="zone-label">Untyped region</div>
<div class="zone-desc">Dynamic caller</div>
<div class="value-picker">
<span>Send value:</span>
<button class="val-btn" data-val="42" data-type="int">42 (Int)</button>
<button class="val-btn" data-val="hello" data-type="str">"hello" (Str)</button>
<button class="val-btn" data-val="3.14" data-type="float">3.14 (Float)</button>
<button class="val-btn" data-val="null" data-type="null">null</button>
</div>
</div>
</div>
<div class="cast-box" id="castBox">
<div class="cast-title">Cast inspector</div>
<div class="cast-row"><span class="cast-key">Value sent:</span><span id="sentVal" class="cast-val">—</span></div>
<div class="cast-row"><span class="cast-key">Expected type:</span><span class="cast-val"><span class="ann">Int</span></span></div>
<div class="cast-row"><span class="cast-key">Cast result:</span><span id="castResult" class="cast-val">—</span></div>
</div>
<div class="blame-box" id="blameBox" style="display:none">
<div id="blameIcon" class="blame-icon"></div>
<div id="blameText" class="blame-text"></div>
</div>
<div class="output-box" id="outputBox" style="display:none">
<div class="output-label">Return value</div>
<div id="outputVal" class="output-val"></div>
</div>
<button id="resetBtn" class="reset-btn">Reset</button>
/* Gradual typing demo styles — two-zone layout with cast inspector panel */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.scene { display: flex; align-items: stretch; gap: 0; margin-bottom: 1rem; border-radius: 10px; overflow: hidden; border: 1px solid #cdd9e3; }
.zone { flex: 1; padding: .8rem; }
.typed-zone { background: #e8f5e9; }
.untyped-zone { background: #fff8e1; }
.zone-label { font-weight: 700; font-size: .85rem; margin-bottom: .25rem; }
.typed-zone .zone-label { color: #1b5e20; }
.untyped-zone .zone-label { color: #e65100; }
.zone-desc { font-size: .8rem; color: #555; margin-bottom: .5rem; }
.annotation { font-family: ui-monospace, monospace; font-size: .85rem; background: #c8e6c9; padding: .2rem .4rem; border-radius: 4px; display: inline-block; }
.ann { color: #1565c0; font-weight: 700; }
.boundary { display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 64px; flex-shrink: 0; background: #f5f5f5; border-left: 2px dashed #aaa; border-right: 2px dashed #aaa; padding: .4rem 0; }
.boundary-arrow { font-size: 1.4rem; color: #666; }
.boundary-label { font-size: .65rem; color: #888; text-align: center; margin-top: .2rem; text-transform: uppercase; letter-spacing: .04em; }
.value-picker { display: flex; flex-direction: column; gap: .35rem; margin-top: .3rem; }
.value-picker > span { font-size: .8rem; color: #555; }
.val-btn { font: 600 12px ui-monospace, monospace; padding: .3rem .6rem; border-radius: 6px; border: 1px solid #e65100; background: #fff3e0; color: #bf360c; cursor: pointer; text-align: left; transition: background .1s; }
.val-btn:hover { background: #ffe0b2; }
.cast-box { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 8px; padding: .7rem; margin-bottom: .6rem; }
.cast-title { font-weight: 700; font-size: .8rem; color: #1d3557; margin-bottom: .4rem; text-transform: uppercase; letter-spacing: .04em; }
.cast-row { display: flex; gap: .5rem; font-size: .85rem; margin-bottom: .15rem; }
.cast-key { color: #555; min-width: 120px; }
.cast-val { font-family: ui-monospace, monospace; font-weight: 600; }
.blame-box { border-radius: 8px; padding: .7rem; margin-bottom: .6rem; display: flex; align-items: flex-start; gap: .5rem; }
.blame-box.blame-ok { background: #e8f5e9; border: 1px solid #a5d6a7; }
.blame-box.blame-dynamic { background: #fff3e0; border: 1px solid #ffcc80; }
.blame-box.blame-static { background: #fce4ec; border: 1px solid #f48fb1; }
.blame-icon { font-size: 1.4rem; flex-shrink: 0; }
.blame-text { font-size: .88rem; line-height: 1.5; }
.output-box { background: #e3f2fd; border: 1px solid #90caf9; border-radius: 8px; padding: .7rem; margin-bottom: .6rem; }
.output-label { font-weight: 700; font-size: .8rem; color: #0d47a1; margin-bottom: .25rem; text-transform: uppercase; letter-spacing: .04em; }
.output-val { font-family: ui-monospace, monospace; font-size: 1rem; font-weight: 700; color: #1565c0; }
.reset-btn { font: 600 13px system-ui, sans-serif; padding: .4rem .9rem; border: 1px solid #1d3557; background: #fff; color: #1d3557; border-radius: 8px; cursor: pointer; }
.reset-btn:hover { background: #e8eef3; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gradual typing demo — simulates a cast boundary between a statically
// typed function (double: Int -> Int) and a dynamically typed caller.
// A failed cast pins blame on the untyped side (blame theorem, Wadler & Findler 2009).
// The statically typed function. In a real gradual type system
// its annotation 'double(x: Int) -> Int' is enforced by the cast at the boundary.
function typedDouble(x) {
return x * 2;
}
// Cast from dynamic type to Int. Returns ok:true on success, or ok:false with blame label.
function castToInt(value, fromLabel) {
if (typeof value === 'number' && Number.isInteger(value)) {
return { ok: true, value: value };
}
// Cast failure — the value is not an integer; blame goes to the caller (untyped side)
return { ok: false, blame: fromLabel };
}
const castBox = document.getElementById('castBox');
const blameBox = document.getElementById('blameBox');
const blameIcon = document.getElementById('blameIcon');
const blameText = document.getElementById('blameText');
const outputBox = document.getElementById('outputBox');
const sentVal = document.getElementById('sentVal');
const castResult = document.getElementById('castResult');
const outputVal = document.getElementById('outputVal');
function reset() {
// Reset display to initial state
sentVal.textContent = '—';
castResult.textContent = '—';
blameBox.style.display = 'none';
blameBox.className = 'blame-box';
outputBox.style.display = 'none';
}
document.querySelectorAll('.val-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
reset();
const rawVal = btn.dataset.val;
const type = btn.dataset.type;
// Parse the raw string value into the correct JS type for the demo
let jsValue;
if (type === 'int') jsValue = parseInt(rawVal, 10);
else if (type === 'float') jsValue = parseFloat(rawVal);
else if (type === 'null') jsValue = null;
else jsValue = rawVal;
sentVal.textContent = rawVal + ' (' + type + ')';
// Run the cast and show blame
const result = castToInt(jsValue, 'untyped caller');
if (result.ok) {
castResult.textContent = '✓ Pass — value is Int';
castResult.style.color = '#1b5e20';
blameBox.style.display = 'flex';
blameBox.className = 'blame-box blame-ok';
blameIcon.textContent = '✓';
blameText.textContent = 'Cast succeeded. The untyped caller sent a valid Int, so the typed function runs normally.';
const doubled = typedDouble(result.value);
outputBox.style.display = 'block';
outputVal.textContent = 'double(' + rawVal + ') = ' + doubled;
} else {
castResult.textContent = '✗ Fail — blame: untyped caller';
castResult.style.color = '#b71c1c';
blameBox.style.display = 'flex';
blameBox.className = 'blame-box blame-dynamic';
blameIcon.textContent = '⚠';
// Build blame message for the dynamic caller
blameText.textContent = fmt('Blame → untyped caller. Sent %rawVal% (%type%) but the typed boundary expected Int. The typed region is not at fault.', { rawVal: rawVal, type: type });
}
});
});
document.getElementById('resetBtn').addEventListener('click', reset);
<!-- Cast boundary demo — typed function zone on the left, untyped caller zone on the right -->
<div class="scene">
<div class="zone typed-zone">
<div class="zone-label">Typed region</div>
<div class="zone-desc">Statically typed function</div>
<div class="annotation">double(x: <span class="ann">Int</span>) → <span class="ann">Int</span></div>
</div>
<div class="boundary" title="Cast boundary — values are checked here at runtime">
<div class="boundary-arrow">→</div>
<div class="boundary-label">Cast boundary</div>
</div>
<div class="zone untyped-zone">
<div class="zone-label">Untyped region</div>
<div class="zone-desc">Dynamic caller</div>
<div class="value-picker">
<span>Send value:</span>
<button class="val-btn" data-val="42" data-type="int">42 (Int)</button>
<button class="val-btn" data-val="hello" data-type="str">"hello" (Str)</button>
<button class="val-btn" data-val="3.14" data-type="float">3.14 (Float)</button>
<button class="val-btn" data-val="null" data-type="null">null</button>
</div>
</div>
</div>
<div class="cast-box" id="castBox">
<div class="cast-title">Cast inspector</div>
<div class="cast-row"><span class="cast-key">Value sent:</span><span id="sentVal" class="cast-val">—</span></div>
<div class="cast-row"><span class="cast-key">Expected type:</span><span class="cast-val"><span class="ann">Int</span></span></div>
<div class="cast-row"><span class="cast-key">Cast result:</span><span id="castResult" class="cast-val">—</span></div>
</div>
<div class="blame-box" id="blameBox" style="display:none">
<div id="blameIcon" class="blame-icon"></div>
<div id="blameText" class="blame-text"></div>
</div>
<div class="output-box" id="outputBox" style="display:none">
<div class="output-label">Return value</div>
<div id="outputVal" class="output-val"></div>
</div>
<button id="resetBtn" class="reset-btn">Reset</button>
/* Gradual typing demo styles — two-zone layout with cast inspector panel */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.scene { display: flex; align-items: stretch; gap: 0; margin-bottom: 1rem; border-radius: 10px; overflow: hidden; border: 1px solid #cdd9e3; }
.zone { flex: 1; padding: .8rem; }
.typed-zone { background: #e8f5e9; }
.untyped-zone { background: #fff8e1; }
.zone-label { font-weight: 700; font-size: .85rem; margin-bottom: .25rem; }
.typed-zone .zone-label { color: #1b5e20; }
.untyped-zone .zone-label { color: #e65100; }
.zone-desc { font-size: .8rem; color: #555; margin-bottom: .5rem; }
.annotation { font-family: ui-monospace, monospace; font-size: .85rem; background: #c8e6c9; padding: .2rem .4rem; border-radius: 4px; display: inline-block; }
.ann { color: #1565c0; font-weight: 700; }
.boundary { display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 64px; flex-shrink: 0; background: #f5f5f5; border-left: 2px dashed #aaa; border-right: 2px dashed #aaa; padding: .4rem 0; }
.boundary-arrow { font-size: 1.4rem; color: #666; }
.boundary-label { font-size: .65rem; color: #888; text-align: center; margin-top: .2rem; text-transform: uppercase; letter-spacing: .04em; }
.value-picker { display: flex; flex-direction: column; gap: .35rem; margin-top: .3rem; }
.value-picker > span { font-size: .8rem; color: #555; }
.val-btn { font: 600 12px ui-monospace, monospace; padding: .3rem .6rem; border-radius: 6px; border: 1px solid #e65100; background: #fff3e0; color: #bf360c; cursor: pointer; text-align: left; transition: background .1s; }
.val-btn:hover { background: #ffe0b2; }
.cast-box { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 8px; padding: .7rem; margin-bottom: .6rem; }
.cast-title { font-weight: 700; font-size: .8rem; color: #1d3557; margin-bottom: .4rem; text-transform: uppercase; letter-spacing: .04em; }
.cast-row { display: flex; gap: .5rem; font-size: .85rem; margin-bottom: .15rem; }
.cast-key { color: #555; min-width: 120px; }
.cast-val { font-family: ui-monospace, monospace; font-weight: 600; }
.blame-box { border-radius: 8px; padding: .7rem; margin-bottom: .6rem; display: flex; align-items: flex-start; gap: .5rem; }
.blame-box.blame-ok { background: #e8f5e9; border: 1px solid #a5d6a7; }
.blame-box.blame-dynamic { background: #fff3e0; border: 1px solid #ffcc80; }
.blame-box.blame-static { background: #fce4ec; border: 1px solid #f48fb1; }
.blame-icon { font-size: 1.4rem; flex-shrink: 0; }
.blame-text { font-size: .88rem; line-height: 1.5; }
.output-box { background: #e3f2fd; border: 1px solid #90caf9; border-radius: 8px; padding: .7rem; margin-bottom: .6rem; }
.output-label { font-weight: 700; font-size: .8rem; color: #0d47a1; margin-bottom: .25rem; text-transform: uppercase; letter-spacing: .04em; }
.output-val { font-family: ui-monospace, monospace; font-size: 1rem; font-weight: 700; color: #1565c0; }
.reset-btn { font: 600 13px system-ui, sans-serif; padding: .4rem .9rem; border: 1px solid #1d3557; background: #fff; color: #1d3557; border-radius: 8px; cursor: pointer; }
.reset-btn:hover { background: #e8eef3; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gradual typing demo — simulates a cast boundary between a statically
// typed function (double: Int -> Int) and a dynamically typed caller.
// A failed cast pins blame on the untyped side (blame theorem, Wadler & Findler 2009).
// The statically typed function. In a real gradual type system
// its annotation 'double(x: Int) -> Int' is enforced by the cast at the boundary.
function typedDouble(x) {
return x * 2;
}
// Cast from dynamic type to Int. Returns ok:true on success, or ok:false with blame label.
function castToInt(value, fromLabel) {
if (typeof value === 'number' && Number.isInteger(value)) {
return { ok: true, value: value };
}
// Cast failure — the value is not an integer; blame goes to the caller (untyped side)
return { ok: false, blame: fromLabel };
}
const castBox = document.getElementById('castBox');
const blameBox = document.getElementById('blameBox');
const blameIcon = document.getElementById('blameIcon');
const blameText = document.getElementById('blameText');
const outputBox = document.getElementById('outputBox');
const sentVal = document.getElementById('sentVal');
const castResult = document.getElementById('castResult');
const outputVal = document.getElementById('outputVal');
function reset() {
// Reset display to initial state
sentVal.textContent = '—';
castResult.textContent = '—';
blameBox.style.display = 'none';
blameBox.className = 'blame-box';
outputBox.style.display = 'none';
}
document.querySelectorAll('.val-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
reset();
const rawVal = btn.dataset.val;
const type = btn.dataset.type;
// Parse the raw string value into the correct JS type for the demo
let jsValue;
if (type === 'int') jsValue = parseInt(rawVal, 10);
else if (type === 'float') jsValue = parseFloat(rawVal);
else if (type === 'null') jsValue = null;
else jsValue = rawVal;
sentVal.textContent = rawVal + ' (' + type + ')';
// Run the cast and show blame
const result = castToInt(jsValue, 'untyped caller');
if (result.ok) {
castResult.textContent = '✓ Pass — value is Int';
castResult.style.color = '#1b5e20';
blameBox.style.display = 'flex';
blameBox.className = 'blame-box blame-ok';
blameIcon.textContent = '✓';
blameText.textContent = 'Cast succeeded. The untyped caller sent a valid Int, so the typed function runs normally.';
const doubled = typedDouble(result.value);
outputBox.style.display = 'block';
outputVal.textContent = 'double(' + rawVal + ') = ' + doubled;
} else {
castResult.textContent = '✗ Fail — blame: untyped caller';
castResult.style.color = '#b71c1c';
blameBox.style.display = 'flex';
blameBox.className = 'blame-box blame-dynamic';
blameIcon.textContent = '⚠';
// Build blame message for the dynamic caller
blameText.textContent = fmt('Blame → untyped caller. Sent %rawVal% (%type%) but the typed boundary expected Int. The typed region is not at fault.', { rawVal: rawVal, type: type });
}
});
});
document.getElementById('resetBtn').addEventListener('click', reset);
Notice the pattern. When the static side receives a value that matches its annotation, the cast succeeds silently. When it receives a wrong type, the runtime raises a blame error pinned to the dynamic (untyped) side — it broke the contract. If the typed side itself had an incorrect annotation, blame would fall on it instead. That precision is the whole point: no more mysterious TypeError with no culprit.
Comments
Loading comments...