How osen evaluates
LANGUAGE.md is the guided tour and teaches you to write osen. This is the machinery underneath it: what the machine is doing while your line runs. Read that one first. Every example below was run and its output pasted.
The registers
The machine is one bundle containing several named bundles, called registers. The five that matter to evaluation:
/_i |
input — the program still to be run |
/_c |
control — the instruction being run right now |
/_s |
stack — values, and where results appear |
/_e |
environment — the names in scope |
/_d |
dump — saved state, one frame per call in progress |
They are ordinary bundles and you can look at them. />/… copies a register
out as a value:
/>/_e # → ( (), () )
/a : 1, /b : 2, />/_e # → ( /b : 2, /a : 1, (), () )
The environment is empty when you start. The whole standard library is
not in it — /o/add and friends live in the host register, /_x. So
“the environment” is only ever the names you have bound yourself.
The two () are scope markers; what a call does, under Implementation,
says what they are for.
Names
A name is looked up in four places, in order, and the first hit wins:
- a register named by the address’s first segment, if one is live
- the environment
/_e - the host register,
/_x - a read-only vocabulary, if the host installed one
You can ask where a name actually came from:
/x : 1, ("/x") /o/whence
# → : "/_e"
("/o/add") /o/whence
# → : "/_x"
A miss at step 1 falls through rather than failing, which is what makes registers additive rather than a trap.
Strings that are instructions
An instruction is just a string that begins with a sigil. /!/ is apply,
/$/ is lookup, /@/ is assign, /'/ is quote. They are dispatched by the
machine below osen, so a string carrying one executes even if you thought you
were carrying data:
"/x" # → : "/x"
/a : 1, "/@/held" # → : []
The first has no sigil, so it stays a string. The second begins with /@/,
so it was dispatched as an assignment and bound /held, which a prompt
reports as it does any binding.
To carry an instruction as data, quote it: "/'/@/held".
Functions
A function is a quotation plus a description of how to call it. /o/defn
builds one:
/g : ({ (/a, /b) /o/add }, ["/a", "/b"]) /o/defn, (1, 2) /g
# → : 3
/g : ({ (/a, /b) /o/add }, ["/a", "/b"]) /o/defn, /g
# → : <fn (/a, /b)>
The slots can be named instead, which is the canonical form:
/g : ( /body : { (/a, 1) /o/add }, /args : ["/a"] ) /o/defn,
(5) /g
# → : 6
There are five: /body, /args, /captured, /ambient, /name. An
unknown one is refused rather than ignored, including a misspelling.
The same { … } is both a quotation and a function’s body, and the
difference is one post-step: when a body’s results are finalised as a
function’s, named ones are copied into the environment; when they are
finalised as a quotation’s, they stay elements of the returned bundle. One
mechanism with and without a step, not two mechanisms.
Scope
By default a function sees its caller’s bindings. This is dynamic scope, and it is the one thing most likely to surprise you:
/x : 40,
/f : ({ (/x,1) /o/add }, []) /o/defn,
/g : ({ /x : 2, () /f }, []) /o/defn,
() /g
# → : 3
/f was written where /x was 40. Called from /g, it reads /g’s /x.
Called from somewhere that binds nothing, it answers 41. The control matters and is
in LANGUAGE.md.
A binding made in a call does not survive it:
/x : 40, /g : ({ /x : 2 }, []) /o/defn, (() /g, /x)
# → ( : 2, : 40 )
You can opt out one name at a time. /captured freezes the value where the
function was written; /ambient resolves the name where it runs:
/n : 5, /g : ( /body : { /n }, /captured : ["/n"] ) /o/defn, /n : 99, () /g
# → : 5
/n : 5, /g : ( /body : { /n }, /ambient : ["/n"] ) /o/defn, /n : 99, () /g
# → : 99
Written versus run. Same walk, asked at two moments.
And /name gives a function its own name inside its own body, so recursion
does not depend on what it is bound to outside — which is what lets it
survive being sent somewhere and bound under a different name:
/f : ({ ( { (/n, ((/n,1) /o/sub) /g) /o/mul }, { 1 }, (/n, 2) /o/lt ) /o/if },
["/n"], /name : "/g") /o/defn,
(5) /f
# → : 120
When a name is not found
/nope # → /nope is not bound. Did you mean /not?
That message is one host’s answer, not the language’s. An unresolved name is
dispatched to /unbound, and what happens there is the host’s choice:
o.se reports it; a host that binds /unbound to its forwarding verb halts the
computation and sends it somewhere that might have the name. That is why an
unbound name is a mechanism rather than an error — it is the hook migration
hangs on.
Things that surprise people
- A
/o/defn‘d function is called as(5) /f, not/!/f. The second gives/_p is not bound, which names an internal register rather than the mistake. /o/ifselects its branch by index, so the first branch runs when the condition is false. Backwards, the program runs and gives a wrong answer rather than complaining.- A register is not readable by its bare name.
(65536, "/xx") /o/make/registerthen/xxsays “not bound”; you read through the prefix,/xx/thing. - A quotation prints as
<thunk>in a box, and as its instructions at a prompt. The printer is naming what the value declares, not what you wrote. - “The environment” is not what a newcomer will assume. It does not contain the standard library; it contains only your own names. Almost every intuition about cost follows from this and it is easy to miss.
Implementation — for embedders and module authors
Implementation
A program is a bundle, and that is not a metaphor
Everything the machine holds is OpenSoundControl. A value is a message, a program is a bundle. When you type
/r : (/q, 2) /o/add
the parser does not build a syntax tree. It builds an OSC bundle, and the pieces you wrote turn into string items inside it:
/r … "/q" "/!/$" … "/o/add" "/!/$" "/!/o/apply"
Two things are worth seeing there. /r became a message address — that
is what an assignment looks like. And /q became a string followed by
"/!/$", which is the instruction look this name up. Reading a name and
binding one are different shapes in the bundle, which is why a tool can
tell them apart without re-parsing anything.
One line, start to finish
(1, 2) /o/add # → : 3
The cycle is: take the next element from /_i, put it in /_c, dispatch on
its address, drop it, repeat. Dispatching (1, 2) pushes a bundle on the
stack; dispatching /o/add’s lookup-and-apply pair finds the function and
runs it; the answer is left on /_s, and the REPL prints what is there.
Quotations — { … }
A quotation is a deferred bundle expression, evaluated in the ambient environment, whose results become the elements of the bundle it returns. It reads dynamically and writes nowhere. These are its four properties.
It does not run when you write it:
/q : { (1,2) /o/add }, /q
A box prints : { : <thunk> }; a prompt prints the instructions inside the
braces instead.
To run one, convert it to a bundle element and execute it:
/q : { (1,2) /o/add },
(/q /!/toelem/fromblob) /!/o/exec
# → ( ( : 3 ) )
It reads where it runs, not where it was written:
/x : 42, /q : { /x }, /x : 99,
(/q /!/toelem/fromblob) /!/o/exec
# → ( ( : 99 ) )
Its bindings do not escape, but they are elements of what it returns:
/g : { /z : 5 }, (/g /!/toelem/fromblob) /!/o/exec, /z
# → /z is not bound
/g : { /z : 5 }, (/g /!/toelem/fromblob) /!/o/exec
# → ( ( /z : 5 ) )
/h : { /a : 1, /b : 2, 99 }, (/h /!/toelem/fromblob) /!/o/exec
# → ( ( /a : 1, /b : 2, : 99 ) )
Named results keep their names, anonymous ones stay anonymous, in order. The
nearest thing in another language is Nix’s { … } or a Jsonnet object: a
record constructor that can see the surrounding scope.
What a call does
Applying a function:
- binds the arguments, captures and ambient names into a fragment, and
prepends it to
/_e— so they shadow what is already there - saves
/_i,/_e,/_sand/_conto/_d— that is a frame - brackets
/_ewith the two()scope markers you saw in section 2 - runs the body
- on return, restores the four from the frame; the markers tell the finaliser which part of the environment was the caller’s
The markers and the saved copy are two halves of one mechanism: the copy is
the record of where the caller’s environment started, and replacing /_e
with it on return is also the only thing that removes the markers.
A frame is four elements of /_d, one set per call still in progress.
Why any of this is shaped this way
Because a computation can stop and move. The five registers are a machine;
/continue/after snapshots it, hands it to the host, and the host puts it
back later — possibly on a different machine. Everything above is arranged so
that what is running is a value that can be written to a socket: no
pointers in it, no state outside the bundle, nothing that only makes sense in
this process.
It is also why the scope rule in section 8 is worth reading twice. A function that reads its caller’s bindings means something different depending on who called it, and “who called it” may be on another machine.
More things that surprise people
- There are two applies, at two layers.
/!/funcallisose’s;/!/o/execis osen’s. Reaching for the wrong one does not fail:/!/funcallon a quotation hands back the unevaluated program rather than running it. - A
{ … }has to become an element before it can run, via/!/toelem/fromblob. Doing it twice trips an assertion inose_getNthPayloadItem()and takes the prompt down. -
A computed assignment inside a quotation appears in the result only if the name is NEW. Section 5 says a quotation’s bindings become elements of what it returns, and the finaliser does not care whether a name got there by syntax or by a verb – it collects what was written inside the scope markers.
/o/assign/toregPREPENDS when the name is new, which lands inside them, and OVERWRITES IN PLACE when the name already exists outside, which does not:{ ("/_e", 42, "/x") /o/assign/toreg, /done : 1 } exec → ( : [], /done : 1, /x : 42 ) /x : 9, { ("/_e", 42, "/x") /o/assign/toreg, /done : 1 } exec → ( : [], /done : 1 )The difference is invisible in the source: it depends on what was bound before the quotation was run.
-
/o/assigntakes a BUNDLE, a value and an address, and it is a pure function. Not the two-argument verb its name suggests, and not a mutation at all: it answers a new bundle with the binding added.( (), 42, "/x" ) /o/assign → ( /x : 42 ) ( (/a : 1), 42, "/x" ) /o/assign → ( /a : 1, /x : 42 ) ("/x", 42) /o/assign → wrong number of elementsThat is the difference from
/o/assign/toreg, which names a register to write into –("/_e", 42, "/x") /o/assign/toregbinds/xfor real. One hands you a value, the other changes the environment, and the names do not say which.