How osen evaluates

The tour teaches you to write osen. This is the machinery underneath it: what the machine is doing while your line runs. Read the tour first.

The registers

The machine is one bundle containing named bundles, called registers, and evaluation uses five of them: the input, /_i, the program waiting to be run; the control, /_c, the instruction running now; the stack, /_s, where values and results appear; the environment, /_e, the names in scope; and the dump, /_d, one frame per call in progress. The overview lists every register.

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 a register its first segment names, then in the environment, then in the host register, /_x, and the first place that has it answers; the registers page gives the search order in detail.

You can ask where a name actually came from:

/x : 1, ("/x") /o/whence
# → : "/_e"
("/o/add") /o/whence
# → : "/_x"

A miss in the register 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 the tour.

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 lists names the function expects to find where it runs; they are read there, as an undeclared name is, so declaring one changes no answer:

/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

  1. 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.
  2. /o/if selects 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.
  3. A register is not readable by its bare name. (65536, "/xx") /o/make/register then /xx says “not bound”; you read through the prefix, /xx/thing.
  4. 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.
  5. “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

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:

  1. binds the arguments, captures and ambient names into a fragment, and prepends it to /_e — so they shadow what is already there
  2. saves /_i, the stack below the call and /_c onto /_d as one element, a bundle whose header marks it as a frame and records which registers it holds; a register that was empty is left out — that is a frame
  3. brackets /_e with the two () scope markers you saw under The registers
  4. runs the body
  5. on return, restores what the frame holds, and cuts /_e back to the caller’s part, which the markers delimit

The frame does not copy the environment. The caller’s bindings stay where they are, in /_e behind the markers, and the markers are the record of where the caller’s environment starts: on return, everything the call put in front of them goes, and the markers with it.

A frame is one element of /_d per call still in progress, and (m, k) /o/frame reads frame k of a machine m.

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 rule under Scope 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

  1. There are two applies, at two layers. /!/funcall is ose’s; /!/o/exec is osen’s. Reaching for the wrong one does not fail: /!/funcall on a quotation hands back the unevaluated program rather than running it.
  2. A { … } has to become an element before it can run, via /!/toelem/fromblob. Converting one that is already an element is refused.
  3. A computed assignment inside a quotation appears in the result only if the name is NEW. Quotations, above, 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/toreg PREPENDS 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.

  4. /o/assign takes 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 elements
    

    That is the difference from /o/assign/toreg, which names a register to write into – ("/_e", 42, "/x") /o/assign/toreg binds /x for real. One hands you a value, the other changes the environment, and the names do not say which.