What a handler owes, and what it may keep

A reporter does not consume the record. It places the record, raises, and the handler owns everything after that.

That is not a balance of concerns, it is structural: a handler that recovers needs the record intact. You cannot both consume the record and let the handler read its contents to rewrite control and continue, so consuming forecloses recovery by construction and is not available as an option.

What an error looks like

Every raise that happens during an instruction leaves the operands, the diagnostic, and the instruction that failed, together, in that order:

/nope             [ "/nope", 65550, "/nope is not bound. Did you mean /not?", "/!/$" ]
(1) /o/add        [ (), ( : 1 ), <fn /o/add (/a, /b)>, 1, "/!/o/apply" ]
("a", 1) /o/add   [ 1, "a", "/!/_l/APPLY" ]
() /o/first       [ (), (), <fn /o/first (/seq)>, "/!/o/apply" ]

A parse error is a different kind and leaves no instruction, correctly: parsing is not something the machine was in the middle of, so there is nothing to re-run and the record carries a diagnostic for a person instead. A handler that treats the two alike will be wrong about one of them.

Asking whether a name is bound

/o/exists asks without raising, so it leaves no fault behind: the answer is 1 or 0, and nothing else happens.

("/nope") /o/exists
# → : 0
("/o/add") /o/exists
# → : 1

What a handler owes

  1. The handler owns cleanup. Nothing else will tidy up after it. That is the price of forwarding, and two hosts’ handlers do it two ways, either of them right.

  2. Handled means the machine is fit to continue. Either the fault was repaired and control rewritten to retry, or the computation was abandoned and the stack left as the next line expects to find it. Printing is not handling; a handler that prints and leaves the record in place has told somebody and decided nothing.

  3. Abandoning is a choice, not a default. A prompt that clears the stack after an error is one handler electing to abandon. A handler could offer a debugger first: a machine is bytes, and a faulted machine can be handed into a larger one as a value, the same way one crosses a socket.

Implementation — for embedders and module authors

Implementation

Telling a diagnostic from a value

/!/is/error answers 1 or 0 and consumes nothing.

It asks about the MACHINE, not about a value handed to it, and that is forced rather than chosen: binding a message to a name overwrites its address – an ose binding is a message named for the binding – so a record passed as an argument arrives without the /error that identifies it. The question a handler actually has is “did something raise here”.

The errno int32 beside the record

The machine pushes an errno int32 on every raise. ose_vm.c:

ose_moveElem(vm_c, vm_s);                        /* the failing instruction */
ose_pushInt32(vm_s, e);                          /* the errno */
ose_pushString(vm_c, "/!" OSE_ADDR_EXCEPTION);   /* dispatch the handler */

And ose_error_push() writes /error/code as an int32 carrying the same value. So it IS the same fact twice – but only when a record exists, and most raises have none:

/!/drop on an empty stack   /!/is/error -> 0    no record; the errno is all there is
/$/nope                     /!/is/error -> 1    osekit_lookup pushed one

A record is written by a PRODUCER – osekit_lookup, the parser – and every producer is above libose. A raise from libose itself carries no record at all, so the int32 is the machine’s universal signal and the record is an optional addition on top of it. A host with no osen has only libose-level failures, and the int32 is all it gets.

What the prompt sees

/!/is/error answers inside the machine that raised, and not after the value comes back. The three elements a raise leaves – the operand, the /error record, the failing instruction – reach the prompt as one anonymous message carrying all their items:

: [ "/nope", 65550, "/nope is not bound. Did you mean /not?", "/!/$" ]

The items survive; the /error address does not. osen_finalizeElem gathers the stack into one element for the caller to carry on with, and the address is what a record is found by, so on the stack a line that raised cannot be told from one that answered. The machine can tell: that is the fault register.

Recovery kits

Read those as recovery kits rather than as debris. /nope says: bind the name, re-run /!/$. The arity error says: fix the argument bundle, re-run /!/o/apply – and hands over the lambda and the arity it wanted so that a handler can decide what “fix” means.

Two handlers

/!/clear in repl-core.ose and /!/drop paired with /!/error in o.se.lib.max.ose are the two handlers in the tree, and the prompt’s /!/clear is the one that elects to abandon.

/_h, the fault register

osevm_init() creates a sixth register, /_h, after /_d. It holds one int32: the errno of the fault this machine is in, or nothing. libose sets it at both raise sites – osevm_run() and the step-driven path – immediately before it clears the status word.

The two are not duplicates; they have different lifetimes.

  means cleared by
status word (ose_errno_set) the LAST OPERATION failed the run loop, before dispatching /!/exception
/_h this MACHINE is faulted osevm_clearFault(), and nothing else

The status word has to be one-shot: every ose_ call may write it, and the handler about to run makes calls of its own. That is the right lifetime for a call’s return signal and the wrong one for a machine’s condition, and until now there was only the one.

libose never clears /_h. What counts as handled is a property of the language being interpreted, not of the machine – the same argument ose_vm.h makes about naming the registers. The interpreting client calls osevm_clearFault() when it decides a fault is dealt with.

A handler that has dealt with a fault clears it, and a prompt asks the machine rather than the stack. /!/fault reads it:

("/nope") /o/exists, /!/fault    ->    : 0
                                       : 0        <- not faulted
(1, 2) /o/add, /!/fault          ->    : 3
                                       : 0        <- the control

/o/exists asks without raising, so it leaves no fault. No osen verb calls osevm_clearFault(): libose sets /_h and the language does not read it, and /_h does not travel with a machine, which is packed from the six fixed registers of osevm_decl_registers[].