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:

  1. a register named by the address’s first segment, if one is live
  2. the environment /_e
  3. the host register, /_x
  4. 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

  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, 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:

  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, /_e, /_s and /_c onto /_d — that is a frame
  3. brackets /_e with the two () scope markers you saw in section 2
  4. runs the body
  5. 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

  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. Doing it twice trips an assertion in ose_getNthPayloadItem() and takes the prompt down.
  3. 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/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.