please, give me any children from any background, and i will put on the blackboard one, two, three, four, five, six, seven, eight, and underneath, the even numbers--two, four, six, eight, ten, and i will ask the child: how can there be as many even numbers as there are numbers? and i will try to have the nerve to let the silence settle, because it is out of the silence that there may come, very slowly, tentatively, god knows, the spark of the question, of the wonder, of the exasperation, which will lead, believe it or not, to that word--the word isn't important--but it will lead to the notion which descartes says is a proof of god in us: the notion of the infinite, because only that notion can help you understand why these two series are homological: one to one to one, and there will be in that room children for whom a fire will start, and it need never go out, if they are properly taught and loved, if they are not condescending to them, and they don't need to know that cantor's transfinite cardinal numbers are a damn difficult concept far beyond me--they don't need to know that--that's how his demonstration starts, and we can start there and stop at a much earlier moment with the sheer joy of it--the sheer animal joy of understanding something infinitely deep.
-- george steiner
if you are reading this, you're witnessing and participating in a miraculous feat achieved only very recently in human history. written text is not built into language, but an invented technology--language is based on sound, and recording the vibrations of our vocal cords as symbols, then decoding them with our eyes, is not something we are born to do.
almost all humans who have ever lived were illiterate. for millennia, humans passed stories and wisdom from generation to generation verbally. only a few places invented writing independently, and only the privileged could master this technology. how insane it would have sounded to them that nowadays an ordinary person can shame politicians and people in power for misspelling a word or mispronouncing a character. how far we have come as a civilization--and most of that achieved within the last hundred years!
what if this is just the beginning of something far greater, and we are nowhere near realizing our true potential as a species? what if we continue the effort to encourage ordinary people to distill, externalize, and preserve their mental processes? not just the utterance of words, but the formal, logical, and mathematical thinking, and in particular, computational thinking.
"that's absurd," you might say. "very few could grasp such abstract concepts!"
and yet, consider how writing was once the exclusive domain of priests, scribes, and officials. programming today is much the same: programmers are treated as priests in the church of computation, a special class of people granted direct access to sacred knowledge, an indispensable intermediary for the rest of the populace.
"STEM!" they say. but i say: "let's do away with the priesthood!"
consider another kind of abstract symbols: sheet music. what is a 1/128 note? what is "sforzando piano"? and yet, there is no corner of the world where humans have walked that does not hear music. we must distinguish between sheet music--a technology for preserving and communicating music--and the entirely separate, universal, innate human capacity to comprehend and create music.
we understand rhythm intimately: our hearts beat at a regular interval, we breathe, in, out, in, out, and we walk, left, right, left, right. computation is no different. it too is a music within us, woven into the cadence of all of our actions. this is how you open a door--repeat the following: if my hand can reach the handle, stop; otherwise, left foot, right foot. if the door is locked, unlock it. with the door unlocked, turn the handle. just like language and music, computation unfolds over time and permeates our existence. is it not possible that we are born with the capacity for computational thinking, just as we are born with vocal cords and tongue muscles to make distinct sounds that carry meaning? and just as you don't need to be literate to be a good orator, and you don't need to read sheet music to be a good singer, you also don't need to parse english-like text filled with cryptic symbols to be a good computationalist. we are all born with the potential to be an orator, a musician, and a computationalist.
so what's to be done?
first of all, our current approach to teaching programming, and more broadly, mathematics, is fundamentally flawed. you wouldn't teach a person who has never heard music before by starting with sheet music notation. instead, you'd probably show them what sounds exist in the world, and how to make them by pressing a key, plucking a string, or blowing into a cavity. you'd let them compare pitches and timbres, and try to sequence and layer them. they'd be immediately intrigued, because the experience is visceral.
perhaps the same applies to programming education. instead of teaching beginners commands, keywords, subroutines, classes, and all kinds of syntactic and semantic constructs, maybe we could start with a video game that provides an extremely simplified view of what's actually going on in a computer's circuitry, using a grid of registers where you can drag things around and combine them. you could then record and play back what you've just performed, and further combine those recordings into richer, more interesting sequences--like live looping, where you progressively add more elements. we could even extend the live looping metaphor to include recording and playing back the action of recording and playing back itself--a meta-recording! in this way, we can introduce, simultaneously, at a lower abstraction level, registers and instructions, and at a higher level, recursion and composition. no symbols. no syntax. once they are comfortable with this process, the final reveal:
"you have been programming this whole time!"
here is the instrument: a spreadsheet-inspired harvard architecture with 2d addressable data memory.
this section describes how the piano is built, not how it is played. learners never see these internals--nevertheless, the interactions described later are designed to instill an understanding of the machine's fundamental operations, so that if this formalism is ever revealed, it will feel natural rather than daunting.
dmem[row, col]: N x N -> R // data memory
imem[addr]: N -> instruction // instruction memory
bp_stack: Stack(N) // base pointer stack
ra_stack: Stack(N) // return address stack
fn_stack: Stack(N) // function address stack
pc: N // program counter
bp = top(bp_stack)
direct: [i,j] → dmem[bp+i, j]
indirect: [[i,j]] → dmem[decode(dmem[bp+i, j])]
encode: N x N -> N
decode: N -> N x N
encode(0,0) = 0 decode(0) = (0,0)
encode(0,1) = 1 decode(1) = (0,1)
encode(1,0) = 2 decode(2) = (1,0)
encode(1,1) = 4 decode(4) = (1,1)
encode(2,0) = 3 decode(3) = (2,0)
1. loadi [i,j] r // dmem[bp+i,j] = r
2. loadi [[i,j]] r // dmem[decode(dmem[bp+i,j])] = r
3. mov [i,j] [k,l] // dmem[bp+i,j] = dmem[bp+k,l]
4. mov [[i,j]] [k,l] // dmem[decode(dmem[bp+i,j])] = dmem[bp+k,l]
5. mov [i,j] [[k,l]] // dmem[bp+i,j] = dmem[decode(dmem[bp+k,l])]
6. mov [[i,j]] [[k,l]] // dmem[decode(dmem[bp+i,j])] = dmem[decode(dmem[bp+k,l])]
note: mod is defined only when the second operand is an integer; otherwise, the behavior is undefined.
7. add [i,j] [k,l] // dmem[bp+i,j] += dmem[bp+k,l]
8. add [[i,j]] [k,l] // dmem[decode(dmem[bp+i,j])] += dmem[bp+k,l]
9. add [i,j] [[k,l]] // dmem[bp+i,j] += dmem[decode(dmem[bp+k,l])]
10. add [[i,j]] [[k,l]] // dmem[decode(dmem[bp+i,j])] += dmem[decode(dmem[bp+k,l])]
11. sub [i,j] [k,l] // dmem[bp+i,j] -= dmem[bp+k,l]
12. sub [[i,j]] [k,l] // dmem[decode(dmem[bp+i,j])] -= dmem[bp+k,l]
13. sub [i,j] [[k,l]] // dmem[bp+i,j] -= dmem[decode(dmem[bp+k,l])]
14. sub [[i,j]] [[k,l]] // dmem[decode(dmem[bp+i,j])] -= dmem[decode(dmem[bp+k,l])]
15. mul [i,j] [k,l] // dmem[bp+i,j] *= dmem[bp+k,l]
16. mul [[i,j]] [k,l] // dmem[decode(dmem[bp+i,j])] *= dmem[bp+k,l]
17. mul [i,j] [[k,l]] // dmem[bp+i,j] *= dmem[decode(dmem[bp+k,l])]
18. mul [[i,j]] [[k,l]] // dmem[decode(dmem[bp+i,j])] *= dmem[decode(dmem[bp+k,l])]
19. div [i,j] [k,l] // dmem[bp+i,j] /= dmem[bp+k,l]
20. div [[i,j]] [k,l] // dmem[decode(dmem[bp+i,j])] /= dmem[bp+k,l]
21. div [i,j] [[k,l]] // dmem[bp+i,j] /= dmem[decode(dmem[bp+k,l])]
22. div [[i,j]] [[k,l]] // dmem[decode(dmem[bp+i,j])] /= dmem[decode(dmem[bp+k,l])]
23. mod [i,j] [k,l] // dmem[bp+i,j] %= dmem[bp+k,l]
24. mod [[i,j]] [k,l] // dmem[decode(dmem[bp+i,j])] %= dmem[bp+k,l]
25. mod [i,j] [[k,l]] // dmem[bp+i,j] %= dmem[decode(dmem[bp+k,l])]
26. mod [[i,j]] [[k,l]] // dmem[decode(dmem[bp+i,j])] %= dmem[decode(dmem[bp+k,l])]
2d addressing requires encoding (row, col) pairs into single values for indirection.
27. ptr [i,j] [k,l] // dmem[bp+i,j] = encode(bp+k, l)
28. ptradd [i,j] [k,l] // n = dmem[bp+k,l]
// (r,c) = decode(dmem[bp+i,j])
// dmem[bp+i,j] = encode(r, c+n)
29. blt [i,j] [k,l] addr // if dmem[bp+i,j] < dmem[bp+k,l], pc = addr
30. blt [[i,j]] [k,l] addr // if dmem[decode(dmem[bp+i,j])] < dmem[bp+k,l], pc = addr
31. blt [i,j] [[k,l]] addr // if dmem[bp+i,j] < dmem[decode(dmem[bp+k,l])], pc = addr
32. blt [[i,j]] [[k,l]] addr // if dmem[decode(dmem[bp+i,j])] < dmem[decode(dmem[bp+k,l])], pc = addr
33. beq [i,j] [k,l] addr // if dmem[bp+i,j] == dmem[bp+k,l], pc = addr
34. beq [[i,j]] [k,l] addr // if dmem[decode(dmem[bp+i,j])] == dmem[bp+k,l], pc = addr
35. beq [i,j] [[k,l]] addr // if dmem[bp+i,j] == dmem[decode(dmem[bp+k,l])], pc = addr
36. beq [[i,j]] [[k,l]] addr // if dmem[decode(dmem[bp+i,j])] == dmem[decode(dmem[bp+k,l])], pc = addr
37. br addr // pc = addr
38. call label // push(ra_stack, pc+1)
// push(bp_stack, bp + imem[top(fn_stack)])
// push(fn_stack, label)
// pc = label+1
39. ret // pop(fn_stack)
// pop(bp_stack)
// pc = pop(ra_stack)
imem[addr]: frame_size (N)
imem[addr+1]: instruction_1
imem[addr+2]: instruction_2
...
variables may be placed in any cell. multiple variables may occupy the same row. arrays must grow horizontally across columns:
col0 col1 col2 col3 col4 col5
row0 [x:5] [y:3] [sum:8] // multiple scalars in one row
row1 [arr0] [arr1] [arr2] [arr3] [arr4] // array grows horizontally
row2 [i:0] [ptr] [temp] // loop variables
bp_stack = {0}
ra_stack = {}
fn_stack = {0}
pc = 1
while not halted:
instruction = imem[pc]
execute(instruction)
if not branched:
pc = pc + 1
parameters are placed in the caller's last row (caller's frame_size, 2 in this example):
mov [2,0], [0,5] # param0 = local_var
loadi [2,1], 10.0 # param1 = 10
ptr [2,2], [1,0] # param2 = &array[0]
call func # call with 3 parameters
callee reads parameters from row 0:
# inside callee
mov [1,0], [0,0] # local = param0
add [1,1], [0,1] # local += param1
mov [[0,2]], [1,2] # *param2 = local
callee writes return value to row 0:
# inside callee
loadi [0,3], 42.0 # return 42 in column 3
ret
caller reads from its last row after call:
call func
mov [1,0], [2,3] # result = return_value
when main (frame size 2) calls f (frame size 3), which in turn calls g (frame size 4)
dmem rows 0-1: main (bp=0)
dmem rows 2-4: f (bp=2)
dmem rows 5-8: g (bp=5)
bp_stack = {0, 2, 5}
ra_stack = {3, 102}
fn_stack = {0, 100, 200}
print(f"current: {name(fn_stack[top])} at imem {pc}")
for i in reversed(range(len(ra_stack))):
print(f" from {name(fn_stack[i])} at imem {ra_stack[i]-1}")
example output during factorial(3) execution:
current: fact at imem 109
from fact at imem 106
from fact at imem 106
from main at imem 2
main:
imem[0]: 0 // // frame size
imem[1]: loadi [0,0] 5.0 // n = 5;
imem[2]: mov [0,1] [0,0] // param 0 = n;
imem[3]: call 100 // fact(n);
imem[4]: halt // result in [0,1]
fact:
imem[100]: 2 // // frame size
imem[101]: loadi [1,0] 1.0 // temp = 1;
imem[102]: blt [0,0] [1,0] 114 // if (n < 1) goto base;
imem[103]: beq [0,0] [1,0] 114 // if (n == 1) goto base;
imem[104]: mov [2,0] [0,0] // param 0 = n;
imem[105]: loadi [1,0] 1.0 // temp = 1;
imem[106]: sub [2,0] [1,0] // param 0 = n - 1;
imem[107]: call 100 // fact(n-1);
imem[108]: loadi [0,1] 0.0 // result = 0;
imem[109]: add [0,1] [0,0] // result = n;
imem[110]: mul [0,1] [2,1] // result = n * fact(n-1);
imem[111]: ret // return result;
base:
imem[114]: loadi [0,1] 1.0 // result = 1;
imem[115]: ret // return result;
main:
imem[0]: 2 // // frame size
imem[1]: loadi [0,0] 10.0 // array[0] = 10;
imem[2]: loadi [0,1] 20.0 // array[1] = 20;
imem[3]: loadi [0,2] 30.0 // array[2] = 30;
imem[4]: loadi [0,3] 40.0 // array[3] = 40;
imem[5]: loadi [0,4] 50.0 // array[4] = 50;
imem[6]: loadi [0,5] 60.0 // array[5] = 60;
imem[7]: loadi [0,6] 70.0 // array[6] = 70;
imem[8]: loadi [0,7] 80.0 // array[7] = 80;
imem[9]: loadi [0,8] 90.0 // array[8] = 90;
imem[10]: loadi [0,9] 100.0 // array[9] = 100;
imem[11]: ptr [2,0] [0,0] // param 0 = &array[0];
imem[12]: loadi [2,1] 10.0 // param 1 = 10;
imem[13]: call 100 // prefix_sum(ptr, len);
imem[14]: halt
prefix_sum:
imem[100]: 3 // // frame size
imem[101]: loadi [1,0] 1.0 // i = 1;
imem[102]: mov [2,0] [0,0] // prev = arr_ptr;
imem[103]: mov [2,1] [0,0] // curr = arr_ptr;
imem[104]: loadi [1,1] 1.0 // temp = 1;
imem[105]: ptradd [2,1] [1,1] // curr++;
loop:
imem[106]: add [[2,1]] [[2,0]] // *curr += *prev;
imem[107]: loadi [1,1] 1.0 // temp = 1;
imem[108]: ptradd [2,0] [1,1] // prev++;
imem[109]: ptradd [2,1] [1,1] // curr++;
imem[110]: add [1,0] [1,1] // i++;
imem[111]: blt [1,0] [0,1] 106 // if (i < len) goto loop;
imem[112]: ret
here is the stage: a syntax-free, keyboard-free programming environment where students construct programs through direct manipulation of a spreadsheet-like grid and control flow graphs.
experiencing computation through traditional programming obscures the most crucial experiential factor: the actual state evolution resulting from code execution, which is imperceptible unless it carries side effects. in addition, high-level block-based languages involve syntactic constructs that only appear natural because we are trained to read them. they bring other implications, such as the semantics of nested lexical environments and closures in languages like python and javascript, which confuse even professional programmers.
by operating at a slightly lower level comparable to compiler intermediate representations, the grid representation of data and cfg representation of code eliminate these barriers. there are no syntax errors because the interface only permits valid operations. control flow is self-explanatory, memory state is transparent, and the stack trace is perpetually available--debugging is free.
the main grid visually represents dmem, while cfgs represent function instructions stored in imem. each cfg node represents one instruction. pc always points to an empty node after the last executed instruction. each cfg has a text label on its root node--collapsed view shows only the label, expanded view shows the complete graph.
a reset button clears the grid and creates a new empty cfg. users initialize parameter cells ([0,0], [0,1], ...) with representative concrete values, then press record to start recording mode. each action replaces the empty node at pc with an instruction node, executes it immediately, spawns a new empty node after it, and advances pc.
loadi: click a cell and adjust a slider to pick the value (range 0-100)mov,add,sub,mul,div,ptr,ptradd: drag the second cell into the first, then select the operation from a wheel. for pointers, visual arrows and highlight frames indicate the referent. the system remembers when a cell is used as a pointer--all subsequent operations (exceptptradd) use indirect addressing[[]]blt,beq: drag from the first cell to the second, then select<or=. initially, only the taken branch is defined. when execution reaches an undefined branch, it pauses with the actual data that triggered that path--students design the response to those specific values. this naturally guides them to test with different initial values to discover and handle all casesbr: drag the empty node atpcback to an earlier nodecall: drag the cfg to a row below the topmost stack frame, which belongs to the function being edited. a horizontal divider marks the end of this frame--you can slide it up and down to control theframe_sizeof the function. the last row of this frame (rowframe_size) becomes the first row of the new frame, where parameters and return values are exchanged. the subroutine cfg executes immediately on the new stack frame.pcenters the cfg's entry node and eventually returns to a newly created empty node right after the call instruction node on the caller cfg.
students develop iteratively by creating instructions through drag gestures and observing immediate results. pc acts like a cursor, prompting the user to drag on either the grid or cfg to create the next instruction to be executed. when the program encounters loops or subroutine calls, execution continues automatically. students can pause at any time, then step forward and backward to reposition the cursor, insert or delete instructions, and resume execution. the machine pauses again when it reaches an unexplored conditional branch (including loop exits), returning control to the user. this creates a natural cycle of dragging, observing, and refining. testing involves clearing the spreadsheet, initializing cells with different values, and exercising different control paths. working cfgs can be saved to a library with descriptive labels, becoming reusable building blocks for more complex programs.
specific cells are designated as motor controls, sensor inputs, or display outputs. students program a simulated robot through goal-oriented challenges with immediate feedback (navigate while avoiding obstacles, for example). this introduces control flow with no pointer indirection.
students implement multi-digit operations using only single-digit primitives, building a calculator that handles arbitrarily large numbers. multi-digit numbers are represented as arrays of single-digit integers occupying sequential cells in rows. when students try hard-coding operations for each position, they discover this doesn't scale--naturally motivating pointers (and more broadly, iterators).
the four operations create a natural progression of increasing complexity: addition and subtraction motivate conditionals (carrying/borrowing) and iteration, multiplication motivates nested iteration and subroutines, and long division motivates subroutines and recursion.
once students become proficient at navigating the virtual world, the same model extends to physical robots by mapping cells to their i/o.
this is not a prescription, but an example to illustrate how computation can be lived before it is written down, just as music is lived before it is scored. to experience water is to stick our hand into a chilly stream and let it flow through our fingers--not stare at a formula and recite: dihydrogen monoxide! i believe that once we break free from conflating experiencing computation with programming, the possibilities are limitless. the symbols, the syntax, the abstract constructs--these remain our destination, the sheet music that crystallizes what we've composed. but first, we must learn to hear the sounds.
so why am i writing this?
for centuries, scripture remained in latin, forbidden to be rendered in common tongues, accessible only to those who mastered the sacred language and devoted themselves to the cloth. today, they decree: do not engage with computation unless you revere its arcane symbols and devote yourself to the craft.
we do not forbid a child from touching piano keys because they cannot play like beethoven. we do not forbid someone from picking up a pen because they cannot write like shakespeare.
yet we do precisely this with computation. by equating the experience of computation with the act of programming, we deny ordinary people the right to think computationally at all. we write birthday cards without being shakespeare. we hum melodies without being beethoven. why should computation demand total devotion or none at all?
we are all made to create. but today, the most seductive creation of the high priests of computation whispers the oldest lie: “you will not toil. you need not understand. speak, and you shall create as gods create.” but in reaching for this temptation, we forsake our humanity. for too long, computation has been shrouded behind the veil, kept in the temple, its mysteries handed down as laws but never directly witnessed. it must descend--become incarnate in our hands, our gestures, our bodies. the word must become flesh.
the music plays in silence. hear it. it shall be heard.
navier-stokes frankot-chellappa path rendering volumetric path tracing recursive ray tracing cylindrical map projection hillshading terrain 2d waves 3d waves ct slices particles mesh distortion ball-and-stick