|
x-engine-c v0.2.31
The C engine for x-lang
|
Evaluator object and interface – x-expr's base object plus the environment, control-flow, I/O, and metadata fields the evaluator needs, plus the central x_eval entry point. More...
Go to the source code of this file.
Macros | |
| #define | X_OBJ_FLAG_COV X_OBJ_FLAG_2 |
| #define | X_OBJ_FLAG_TRACE 0x400 |
| #define | X_OBJ_EVALS_SHIFT 11 |
| #define | X_OBJ_EVALS_BITS 20 |
| #define | X_OBJ_EVALS_ONE ((x_int_t)1 << X_OBJ_EVALS_SHIFT) |
| #define | X_OBJ_EVALS_MAX (((x_int_t)1 << X_OBJ_EVALS_BITS) - 1) |
| #define | x_obj_evals(X) ((x_obj_flags(X) >> X_OBJ_EVALS_SHIFT) & X_OBJ_EVALS_MAX) |
| #define | x_error_handler_jmp(H) x_ptrval(x_firstobj(H)) |
| #define | x_error_handler_saved_env(H) x_001(H) |
| #define | x_error_handler_error(H) x_011(H) |
| #define | X_READ_BUF_SIZE 65536 |
Argument Access Macros | |
| #define | x_eval_arg_exp(X) x_0((X)) |
Typedefs | |
| typedef char | x_assert_trace_above_flags[X_OBJ_FLAG_MASK< X_OBJ_FLAG_TRACE ? 1 :-1] |
| typedef char | x_assert_evals_above_trace[(x_int_t) X_OBJ_FLAG_TRACE< X_OBJ_EVALS_ONE ? 1 :-1] |
| typedef char | x_assert_evals_below_sign[X_OBJ_EVALS_SHIFT+X_OBJ_EVALS_BITS<=31 ? 1 :-1] |
Functions | |
| x_obj_t * | x_eval_make (x_obj_t *p_base, x_obj_t *p_args) |
| x_obj_t * | x_eval_type_alist_extend (x_obj_t *p_base, x_obj_t *p_args) |
| x_obj_t * | x_eval_type_alist_assoc (x_obj_t *p_base, x_obj_t *p_args) |
| x_obj_t * | x_eval_buffer_push (x_obj_t *p_base, x_obj_t *p_buffer) |
| x_obj_t * | x_eval_load (x_obj_t *p_base, x_obj_t *p_args) |
| void | x_eval_error (x_obj_t *p_base, x_char_t *message, x_obj_t *p_obj) |
| x_obj_t * | x_eval (x_obj_t *p_base, x_obj_t *p_args) |
Evaluation Entry Points | |
| x_obj_t * | x_eval_list (x_obj_t *p_base, x_obj_t *p_args) |
| void | x_eval_spine_guard (x_obj_t *p_base, x_obj_t *p_obj) |
| x_obj_t * | x_eval_spine_first (x_obj_t *p_base, x_obj_t *p_pos) |
| x_obj_t * | x_eval_body (x_obj_t *p_base, x_obj_t *p_args) |
| x_obj_t * | x_eval_body_tco (x_obj_t *p_base, x_obj_t *p_args) |
| x_obj_t * | x_eval_tco_trampoline (x_obj_t *p_base, x_obj_t *p_args) |
| x_obj_t * | x_eval_op_body (x_obj_t *p_base, x_obj_t *p_args) |
Variables | |
| x_satom_t | x_eval_obj |
Evaluator object and interface – x-expr's base object plus the environment, control-flow, I/O, and metadata fields the evaluator needs, plus the central x_eval entry point.
The base object is a pair tree. x-expr provides the skeleton (io-fields, meta-fields, profile, hooks, heap-fields); this layer fills the environment/control half it leaves nil and appends a few project fields (booleans, eval-list, token-cache, GC hooks, sigint).
Layout (base = x_base(X)):
first: env + ctrl (x-expr leaves nil; filled here) env env (the current environment), env-root ctrl save-stack, error-handler, tco-expr, tco-env rest: io + meta (x-expr skeleton) io type-alist, line, true, false meta profile counters, eval-list, token-cache, sigint (GC hook + root lists – mark-hooks, free-hooks, mark-roots – now live in x-expr's heap-fields; register via x_heap_{mark,free}_hook_add() and x_heap_mark_root_add().)
Each leaf is a stack cell (current . saved); read the current value with x_firstobj(). Direct-value exceptions (the slot is the value, no wrapping): save-stack, env, env-root.
An ENVIRONMENT is a first-class value: one pair, (bindings . parent). The root's parent is nil and its bindings are a tree (x-alist.c's BST, for the size of a loaded library); every other environment's bindings are an alist of (name . value) cells and its parent is the environment it was made in. A procedure call makes a child of the closure's environment; an operative body runs in a child of its static environment and receives the caller's environment as a value; def binds in the current environment and eval with an environment makes that one current. The whole of scope is those four sentences, and every save/restore in the evaluator is one pointer, the current environment. The environment operations are x-env.h; the save and restore are x-tco.h; the top-level bracket is x-toplevel.h.
The error handler is itself a pair tree, navigated by x_error_handler_*: (jmp-ptr (saved-env . nil) error-value . line)
| #define x_eval_arg_exp | ( | X | ) | x_0((X)) |
Extract the expression from eval args.
| #define x_obj_evals | ( | X | ) | ((x_obj_flags(X) >> X_OBJ_EVALS_SHIFT) & X_OBJ_EVALS_MAX) |
| #define X_OBJ_EVALS_BITS 20 |
| #define X_OBJ_EVALS_MAX (((x_int_t)1 << X_OBJ_EVALS_BITS) - 1) |
| #define X_OBJ_EVALS_ONE ((x_int_t)1 << X_OBJ_EVALS_SHIFT) |
| #define X_OBJ_EVALS_SHIFT 11 |
The EVAL COUNT – under X_PROFILE, how many times evaluation has reached an object. It counts exactly where COV marks: every expression x_eval evaluates, a trampolined tail included, and every body cell x_eval_body and x_eval_body_tco step onto. So a procedure's first body cell counts its calls, and the counts through its body are the evaluation it did itself.
It lives in the FLAGS WORD, above x-expr's bits and the TRACE bit, so it costs no allocation and needs no initializing: x_obj_alloc writes the whole word, and every object is born at zero. Read it with x_obj_evals(). Reflective x-lang code reads the same bits through obj-evals-shift and obj-evals-bits in tools/contract/obj-layout.x.
20 BITS, AND IT SATURATES. x-expr's sweep clears the mark with flags &= ~mark, and the mark is an x_obj_flag_t, an unsigned int, so on a 64-bit host the complement zero-extends and every collection clears bits 32 and up of every object it keeps. Bits 11 to 30 survive that, and they keep the word non-negative where bit 31 is a 32-bit host's sign. A count therefore stops at X_OBJ_EVALS_MAX (1048575) instead of wrapping: a node reading the maximum was reached at least that often.
| #define X_OBJ_FLAG_COV X_OBJ_FLAG_2 |
Expression flags. COV – an expression has been evaluated (coverage tracking).
Three flags lived beside it and are gone with the environment model they served: SHADOW, a bit on the interned symbol that marked a local binding; FRAME, a bit on an env spine cell that marked it as part of a local frame; and FNFRAME, FRAME's refinement for procedure activations. An environment is an object now, so a frame is a value and needs no mark. Flag bits 3 and 4 are free at this layer.
| #define X_OBJ_FLAG_TRACE 0x400 |
The TRACE bit – the first bit above x-expr's, and the one a caller borrows to mark objects of its own. (heap tree-mark!), (heap chain-clear!) and (image write!) take the flag as an argument; every caller, here and in x-lang, passes this one. The engine sets it nowhere itself. It is named so that nothing of the engine's lands on it: the eval count below, on this bit, would make every object evaluated an odd number of times look traced to an image write.
| typedef char x_assert_evals_above_trace[(x_int_t) X_OBJ_FLAG_TRACE< X_OBJ_EVALS_ONE ? 1 :-1] |
| typedef char x_assert_evals_below_sign[X_OBJ_EVALS_SHIFT+X_OBJ_EVALS_BITS<=31 ? 1 :-1] |
| typedef char x_assert_trace_above_flags[X_OBJ_FLAG_MASK< X_OBJ_FLAG_TRACE ? 1 :-1] |
Evaluate an expression in the current environment (TCO trampoline).
Evaluate an expression with tail-call optimization.
Dispatches to the expression's type-level eval handler. If the handler sets a TCO tail expression on p_base, the trampoline loop re-evaluates without growing the C stack. On exit, restores the environment from the saved TCO snapshot.
The expression travels to the type's eval handler in a pair list built here, whose first element is a cell holding it: the trampoline stores each tail expression in that cell, and a handler answers the list to ask for another pass.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Argument run: (expression) |
Outermost detection. The local trampolining flag starts at 0. When a TCO tail expression is first detected on p_base, this x_eval instance sets trampolining = 1, claiming ownership of the trampoline loop. Any nested x_eval called during handler dispatch will see tco_expr as cleared (this instance clears it before goto) and will therefore NOT enter the trampoline – it returns normally and its result is discarded in favor of the deferred tail expression.
tco_expr / tco_env lifecycle.
tco_env cleared: Each iteration clears tco_env on p_base after snapshotting it into the local p_tco_env_save. This prevents nested x_eval calls from seeing stale env state.
p_tco_env_save snapshot.
Used only at exit: the outermost x_eval makes it current.
Nested x_eval calls do NOT restore env. Only the instance where trampolining == 1 executes the env restore block. This is critical: a recursive x_eval (e.g. from evaluating a sub-expression inside a primitive) must not interfere with the outer trampoline's env management.
Evaluate a body (sequence of expressions), returning the last result.
Evaluate a body (list of expressions) sequentially, returning the last result.
Each expression is rooted on the eval-list before evaluation so the GC does not collect the remaining body. No tail-call optimization.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Argument run: (body), the list of body expressions |
Evaluate a body with TCO, setting up a trampoline for the tail call.
Evaluate a body with full tail-call optimization.
Non-tail expressions are evaluated normally. The tail (last) expression is stored in the TCO expr slot instead of being evaluated directly, and the caller's saved environment is captured in tco-env so the trampoline can restore it after the tail call.
On early exit (nil tail) or empty body, pops the save-stack and makes the environment it held current again.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Argument run: (body), the list of body expressions |
Save-stack protocol. The caller (fn/let dispatch) pushes the environment it is leaving onto save_stack BEFORE calling this function (x_tco_env_save), so it can be made current again after the tail call completes.
tco_env capture. When the tail expression is reached (last element of body), this function checks whether tco_env is still nil. If so, it copies the save-stack top into tco_env, providing the env snapshot that x_eval's trampoline will use for restoration. If tco_env is already set (by a prior TCO iteration), the existing value is kept.
Save-stack pop. After capturing tco_env (or on early exit), the save-stack is popped. On the normal tail-call path this is a simple pop (the trampoline in x_eval handles restore). On early exit (nil tail or empty body), this function does a full restore from the popped frame before returning, since no trampoline iteration will follow.
Push a buffer onto the input buffer stack.
Push a buffer onto the buffer stack.
| p_base | x_obj_t* – Base (execution context) |
| p_buffer | x_obj_t* – Buffer object to push |
Signal an error with the given message and irritant object.
Signal an error with a message and optional object context.
If an error handler is installed (via guard), builds a combined error string with line number, restores the saved environment, and longjmps to the handler. Otherwise, writes the error to stderr via the low-level x_error function.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Argument run: (message, object) – the error message string and the object associated with the error (may be nil) |
Zero-allocation error path. When a handler is installed, the message string pointer is stored directly in a static atom (no malloc, no x_mkstrown). Message strings from C callers are always string literals (static storage), so they survive the longjmp. The guard handler in x-lang receives the bare message; x-lang code can add line/symbol context via (base) if needed.
longjmp protocol. The error value is stored in the handler's error slot, then the environment the handler saved at guard installation time is made current again. Finally, longjmp transfers control to the setjmp site in x_prim_guard. This unwinds all C frames between the error site and the guard – any local state in those frames is lost.
error). Returning instead would resume the raising primitive mid-operation with a garbage value – at boot, where no guard is installed yet and the harness discards stderr, that silently corrupted the load (the class-call trap).Evaluate an argument list, returning a list of results.
Evaluate each element of a list, returning a new list of results.
Recursively evaluates through the eval slot, rooting the tail on the eval-list GC root so the garbage collector does not free remaining arguments while evaluating the current one.
| p_base | x_obj_t* – Base (execution context) |
| p_vector | x_obj_t* – Argument run: (args), the list of unevaluated expressions |
GC rooting protocol. Before evaluating the current element, the entire remaining arg list is pushed onto eval_list (a GC root on p_base) as a stack-allocated pair. This prevents the collector from freeing the rest of the list while the evaluation runs (which may trigger GC). After evaluation, the root is popped. The push/pop is O(1) per element, but the recursion itself is O(n) in C stack depth – one frame per list element. This is acceptable for argument lists (typically short) but would overflow on very long lists.
Read and evaluate all expressions from the current buffer.
Loops calling x_token_read until EOF, evaluating each expression via x_eval. Returns the result of the last expression.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Unused |
Reads from the buffer at the top of the buffer stack (x_base_field_buffer). The caller is responsible for pushing the desired buffer before calling this function (via x_eval_buffer_push) and popping it afterward. Each read expression is wrapped in a stack-allocated (atom . nil) eval-args pair and passed to x_eval, which runs the full evaluator including the TCO trampoline. The result of each expression is discarded except the last.
cat lib/x.x - | ./x-bin). The core loop reads through the buffer/fd stack; the optional include primitive (X_INCLUDE, x-cli.c) additionally opens files via x_sys_open and pushes them onto the same stack.Build the interpreter object (x-expr base object extended).
Create and initialize a full x-lang base object atop x-expr.
Calls x_base_make (x-expr layer) with default file descriptors and the hooks for its slot vector, then fills in the type-system-specific fields: the env fields (the current environment and the root), the ctrl fields (save-stack, error-handler, TCO slots), io-state (line counter, boolean caches), extended profile counters, and project extras (eval-list, token-cache, mark-roots).
| p_base | x_obj_t* – Parent base (or NULL for root) |
| p_args | x_obj_t* – Unused |
x-expr vs x-lang layers. x_base_make (x-expr) allocates the base tree skeleton: heap fields (pools, GC state), file descriptors, buffer stack, type-alist slot, profile head (1 counter for GC cycles), and the slot vector. It leaves env, ctrl, io-state, and extras as nil. This function fills all of those in, giving the base its full evaluator personality.
Base tree nodes carry X_OBJ_FLAG_SHARED (set by x-expr's x_base_make). The SHARED flag tells the GC mark phase that these spine nodes are allocated from the base's own pool and must be marked but never freed – they are structurally permanent for the lifetime of the base.
Env fields layout:
env-root: the base's root environment, whose bindings are a tree and whose parent is nil
Ctrl fields layout:
Profile counters (9 additional beyond x-expr's GC counter): evals, TCO hits, lookups, BST lookups, and internal metrics.
p_base is non-NULL (child base), boolean caches (#t/#f) are inherited from the parent so all bases in a tree share the same singleton boolean objects.Defer an operative body's tail to the outer trampoline: evaluate the non-tail forms, then set tco_expr to the tail and tco_env to the caller's environment, which the trampoline restores after the tail.
Defer an operative body's tail to the outer trampoline (TCO).
Evaluates the non-tail body forms synchronously, then stores the tail form in tco_expr and the caller's environment in tco_env. Deliberately does NOT push the save-stack – operatives stay invisible to it, so a procedure whose tail is an operative call still owns its own restore. The trampoline keeps the outermost environment it is handed and makes it current at exit, which for an operative is the caller's: a def the body evaluated in the caller's environment is IN that environment, so there is nothing to decide about keeping or shedding a chain head.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Argument run: (body, caller). The body is the operative's sequence of forms; the caller is the caller's environment, current on return |
Read the argument at a peeked spine position, guarding a dotted tail (#487).
Read the argument at an already-navigated spine position.
| p_base | Base (execution context). |
| p_pos | A spine position (the result of an x_1/x_11 peek). |
p_pos, or nil if the list ended there.The raw positional macros (x_011 and friends) navigate first/rest UNCHECKED, which is their contract – so a caller peeking PAST the arity a guarded walk covered used to read the atom a dotted tail ends with as a pair (#487). This is that peek, done safely: nil when the list ended, the element when the position is a cell, and the ruled catchable raise on anything else.
Raise unless a spine position is a cell first/rest may navigate (#69, #487).
Raise unless p_obj is a spine cell that first/rest may navigate.
| p_base | Base (execution context). |
| p_obj | The spine position about to be walked (never nil). |
Improper-spine guard (#69, ruled). A first/rest walk is only meaningful for an object whose TYPE DECLARES pair units – the same structural contract the collector's payload walk trusts (x_type_prim_heap_mark). The test is STRUCTURAL, not a type-identity list: any reader personality's spine type participates by declaring pair units (the reader and the evaluator need not be symmetric), and two layouts are cells by construction – raw stack cells (NULL type slot) and heap pairs labelled with the built-in pair static (the x_mkspair product; #296). The static's own type slot is NULL, so the registered-type probe could never accept it – omitting it made every C-built spine handed to an applicative in a minimal base raise spuriously. A dotted tail lands here as a non-cell and raises a catchable error in place of the segfault it replaces – (list 1 . 5), and bare-x-core (f 1.5) where the float module is absent and 1.5 reads as a dotted pair; the tail atom is atom-labelled or registered-typed, so neither layout re-admits it.
Execute the TCO trampoline loop until a non-TCO result is produced.
TCO trampoline: repeatedly evaluate deferred tail expressions.
After a TCO-aware body defers its tail expression, this loop evaluates it. If that evaluation itself defers another tail call, the loop continues until no more TCO expressions remain.
On exit, makes the environment saved in tco-env current again.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Argument run: (result), the initial result (from non-tail evaluation) |
Look up a type in the base type alist.
Look up a type struct in the base's type alist by name.
Searches for a (name . type_struct) entry matching the first element of p_args. Returns the bare type struct (unwrapped from the alist entry), or NULL if not found.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Pair whose first is the type name to look up |
Extend the type alist with a new type entry.
Add a type struct to the base's type alist.
Wraps the type struct as a (name . type_struct) pair for alist keying and prepends it to the type alist.
| p_base | x_obj_t* – Base (execution context) |
| p_args | x_obj_t* – Type struct to register |
|
extern |
The interpreter object: the base object specialized into this project's execution context. Serves as the type label for base/interp objects.
Static atom for the interpreter object – this project's root base object (sentinel label "BASE").