How osen code is called, and what happens to values
What a value goes through at a call, an assignment and a conversion. None
of it is a fact about any one /o/* name; all of it is a fact about
calling, and every rule was run.
1. The one rule everything else follows from
In OSC, a bundle has no address. A message does.
So the moment you need to name a bundle – to bind it, to route it, to send it anywhere addressed – it cannot stay a bundle. It has to become an item inside a message, and that message carries the address.
Nobody decided this. It falls out of the format, and most of what follows is a consequence rather than a convention.
/x : {1, 2} evaluates to /x : { : 1, : 2 } – a message addressed /x
whose single item is the bundle.
2. Assignment wraps
NAME : VALUE gives VALUE an address. If VALUE is a bundle, it is wrapped
in a message to receive one.
This is the trap. An expression that just produced a bundle element –
/o/bundleitem/tobundleelem, say – has its work undone by being assigned,
because assignment puts it back inside a message.
The working objects show the shape to use instead: pass the conversion
straight to whatever consumes it, and never bind it. From o.print.osen:
((/bndl) /o/bundleitem/tobundleelem, /xx/outlet) /o/outlet
Not:
/elem : (/bndl) /o/bundleitem/tobundleelem, /* wraps it again */
(/elem, /xx/outlet) /o/outlet
3. Arguments first, then the function
(1, 2) /o/add => : 3
4. (), {}, <()> and <{}> are all bundles
This is the part that, missed, makes osen look like a language with unfamiliar syntax rather than what it is.
They are four ways of writing a bundle. They are NOT overloaded as
language constructs – () is not “an argument list” and {} is not “a
function body”. There is one kind of thing here. The forms differ on two
independent axes, and the parser names both:
| written | parser flags | evaluated |
|---|---|---|
( … ) |
BUNDLE |
eagerly |
{ … } |
BUNDLE \| LAZY |
not yet |
<( … )> |
BUNDLE \| FULLYQUALIFIED |
eagerly |
<{ … }> |
BUNDLE \| LAZY \| FULLYQUALIFIED |
not yet |
From osen_parser.c, rules UnqualifiedEagerOSCBundle,
UnqualifiedLazyOSCBundle, FullyQualifiedEagerOSCBundle,
FullyQualifiedLazyOSCBundle.
<{1, 2}> /o/add => : 3. And {1, 2} /o/add =>
: [ { : 1, : 2 }, "/!/_l/APPLY" ] – the bundle sits there unevaluated
with the apply still pending, which is exactly what makes {} usable as a
function body without {} meaning “function body”.
4a. [ … ] is a list, and it is not a bundle
An OSC message has a data section. It may be empty, but it cannot not
exist. [ … ] is that list.
So these are the same message written two ways:
/foo : /foo : []
/foo : 1 /foo : [1]
All four print identically – /foo : and /foo : 1.
4b. Why it looks postfix
Because it is close to a transcript of what you send the machine.
(1, 2) /o/add is not “call add with arguments 1 and 2”. It is:
send a bundle and evaluate it in order – push a message with no address carrying 1, then push a message with no address carrying 2 – then send a message addressed
/o/add, and with it the implicit instruction to apply the top of the stack to the remainder.
OSC bundles evaluate in order. Everything that looks like syntax is a consequence of that, plus whatever restructuring the parser must add. The notation is trying to stay one-to-one with the bytes.
Understand this first. Once it is in place the rest stops being
arbitrary: assignment wraps a bundle because a bundle cannot hold an
address; /o/last is item-level because it works on real OSC structure and
not an abstract list; a name evaluates because a message addressed /foo
sent to the machine is a lookup.
4c. The words mean specific things
Four words mean specific things, and a function’s name should be read against them:
| word | means |
|---|---|
| items | the items of a message |
| elements | the elements of a bundle |
| size | the complete size of a thing, whatever it is |
| length | how many things are in it – may equal the size or be less |
4d. A bundle inside something has TWO representations
This is the piece that makes /o/bundleitem/tobundleelem make sense, and it
follows directly from §1.
As a bundle element. A bundle sitting directly inside another bundle.
This is what the notation produces: the parser’s lazy and eager bundle rules
both emit ose_pushBundle.
As a |-typed item. A bundle sitting inside a message’s data section,
laid out exactly like a blob. '|' is OSETT_BUNDLE; osekit_machine_save
builds the five registers this way with ose_setTypetag_impl(dest,
OSETT_BUNDLE).
Why two? Because of §1. A register has to be named – /_s is an address
– and a bundle cannot hold an address. So to name a bundle you put it in a
message as a | item, and the message carries the address.
/o/bundleitem/tobundleelem converts the second form back into the first,
which is the only reason it exists. When a machine arrives and you want to
work on its stack, you have a message /_s whose item is a bundle, and you
need a bundle.
Consequences worth knowing:
({1, 2})puts a genuine bundle element on top – not a message with a|item. Written notation gives you elements; machine registers give you|items.-
Ops therefore behave differently on the two, and the two counting functions are a symmetric pair rather than a confusion:
counts denotational spec count/elemsthe ELEMENTS of the bundle – things on the stack BB(..., A_K)-> push Kcount/itemsthe ITEMS of the TOPMOST ELEMENT – things in the top one A_K items(..., a_L)-> push LSo
({1,2}) /o/count/items=> 2 is correct: the top element is a bundle holding two things, and the questioncount/itemsasks is “how many things are in the top one”.The vocabulary of section 4c holds across all sixteen
Elem/Itemfunctions inose_stackops.c–sizeagainstlength,ElemagainstItem, incount,size,sizes,length,lengths, and the blob converters.
5. A bare name is a lookup; a trailing colon makes it an address
Written on its own, /foo parses as a string in the data section, with a
lookup command. It evaluates the binding at /foo; it does not denote the
address /foo.
[/a, /b, /c] returns three not bound exceptions, one per name,
suggesting /add, /abs and /lt.
To make /foo denote an address, force it into address position with a
trailing colon. The data section can then be left empty or given an
explicit empty list:
/foo : a message addressed /foo, empty data
/foo : [] the same message, written out
For the text of the name rather than a lookup or an address, use a string:
"/away/somewhere".
6. Where the incoming bundle is
Not /_s, which is the machine’s stack register.
A Max object’s inlets bind at /xx/in/N, N counting from 0, built by a
fold over /numinlets – see the worked comment in max/lib.osen.
Inside a method defined with /o/defn, the argument names given in the
declaration are bound instead:
/FullPacket : ( { ... }, ["/bndl", "/inlet"] ) /o/defn
binds /bndl and /inlet for that body.
7. Items versus elements, and why the ops split
A message holds items. A bundle holds elements. libose names the
three levels in its denotational specs – \BB bundle, \ela element,
\ita item, defined in libose/doc/latex_macros.tex – and the /o/*
wrappers inherit whichever level the underlying function works at.
Verified examples:
([1,2,3]) /o/last=>3– the last item of a message.([1,2,3]) /o/butlast=>[1, 2]– the message minus its last item.ose_push, from its header: “the second-to-last element is a bundle: the last element is pushed into it” – element into bundle.ose_pop, from its header: “pop the last item off the last element” – item out of element. It is not the inverse of push.
Choosing an op therefore needs two questions answered, not one: what does it do, and at which level.
8. Making a C function an /o/ verb
A function a module binds – ose_pushMessage(vm_s, "/hello", …,
OSETT_ALIGNEDPTR, my_hello) in its ose_main – is an instruction-level
verb: it takes the machine, finds its arguments on the stack, and is called
as /!/hello. Called from osen as () /!/hello, its result lands beside
the argument bundle and the finaliser applies the result as a name, which
fails. The idiom that makes it an ordinary function at the prompt is three
instructions, the same ones osen.ose uses for /time/now:
/,/s//!/hello
/,/s//o/hello
/!/o/unwrap
/o/unwrap takes the instruction name and the name to bind, and binds under
the second a wrapper that unpacks the argument bundle onto the stack, runs
the instruction, and leaves one result. From then on () /o/hello answers
: "Hello, World!" and (() /o/hello, "!") /o/concat/strings composes
like anything else. From the command line, the three follow the load:
./o.se -f o.se.osen/osen.ose '/,/s/./o.se.hello.so' /!/load '/<</_x' \
'/,/s//!/hello' '/,/s//o/hello' /!/o/unwrap