Shen/JIT semantics

The behaviour Shen/JIT implements. The executable contract examples check the main rules below, and their output under Shen/Scheme is recorded in contract/shen-scheme-0.54.txt.

Where Shen/JIT deliberately differs from Shen/Scheme, the rule says so, and where there is an example, it is marked as a deviation. Shen/Scheme's behaviour was observed by running KLambda through eval-kl, not inferred from its source.

1. Inputs

Input Version
Kernel The release that tools/ci-deps.sh names: its klambda/, and its tests
Stage 0 host The release of Shen/JIT that tools/ci-deps.sh names, or the release of Shen/Scheme it names, which make the same files
SLJIT Upstream commit a0020dd49a21: see vendor/sljit/UPSTREAM.md

tools/ci-deps.sh fetches the kernel, the earlier release of Shen/JIT and Shen/Scheme by hash, and generated.sha256 holds the hashes of what they make.

2. Values

All values are 64-bit words. The low bits are tags, with the spare pattern used for object headers:

Low bits Meaning
..00 Fixnum, 62-bit signed: -2^61 to 2^61-1.
011 Pair pointer. Pairs are two words with no header.
001 Pointer to an object with a header.
101 Pointer to a procedure (an object with a header).
110 Pointer to a symbol (an object with a header).
010 Special value; the payload is in the upper bits.
111 Object header. No value has this pattern.

A word at the start of a heap object is therefore either a header (111) or the head of a pair. The collector's scan and the image's relocation pass rely on this. Objects are 8-byte aligned and at least two words long.

Special values: the empty list, true, false, Prolog variables, the cleared stack value (also used for unbound cells) and the forwarding marker used during collection. The cleared value and the forwarding marker are never visible to Shen code. (fail) is the symbol shen.fail!, as defined by the kernel, not a special value.

A Prolog variable holds the offset of its binding in the Prolog vector, from 0 to 2^56-1. As an optimisation it is this immediate value and not the kernel's absvector, so it allocates nothing and absvector? is false of it. shen.pvar? is true of it and of nothing else, two are = when their offsets are, and it prints as the kernel's shen.pvar prints one: Var and the offset.

Object families, each identified by a type field in its header, which also states which fields hold values:

Family Contents
String Byte length and bytes; immutable.
Byte buffer Byte length and bytes, which can be stored to, and which the collector does not scan.
Symbol Name string, name hash, global cell index.
Real IEEE double.
Absvector Length and that many values.
Closure Code object and captured values; a defun is a closure with no captures. Raw native entry.
Foreign function A C function, its description and arity. Raw native entry and address.
Code object Bytecode, constant slot base, frame size, arity, name. Raw native code pointer.
Exception Message string, and a value that goes before it, or none. An error raised from C keeps the offending value apart; error-to-string builds the full message the first time it is asked.
Stream Kind, direction, open flag; raw OS handle.
Pointer Kind and what keeps it alive; raw address and length. Its header says whether it is read only.

There is no partial-application object: KLambda named calls are saturated (see section 9), so only closures and foreign functions are ever applied.

Symbols are ordinary heap objects. The hash is computed from the name bytes when the symbol is created, so it never depends on an address. The intern table is a heap object that holds symbols weakly: a collection drops a symbol that nothing else reaches, that has no value or function, and that no compiled code calls by name, and its cell is reused. Interning the name again makes a new symbol, which nothing can tell from the old one. (intern "true") and (intern "false") return the Boolean specials.

Global cells (a value word and a function word per symbol) and constant slots live in root arrays outside the heap. The arrays grow by adding fixed-size chunks, so no cell moves. The heap refers to cells only by index; emitted code embeds cell addresses. A code object owns its range of constant slots: the collector copies them with it, and reuses them once it is dead (heap.c).

3. Numbers

4. Strings, symbols and characters

Strings are immutable byte sequences, and every string primitive works in bytes; source text and files pass through as UTF-8 bytes without decoding.

5. Pairs and absvectors

The kernel catches pos past the end in shen.string->byte and value of an unbound symbol in bound?, and programs catch such errors too, so they must be cheap to raise and catch.

6. Equality and hashing

(= A B) is true when A and B are numbers with the same mathematical value, strings or byte buffers with the same bytes, pairs whose heads and tails are =, absvectors of the same length whose elements are =, pointers of the same kind and address that are both read only or both not, or the same symbol, special, procedure, stream or exception. Closures compare by identity.

= follows a term as deep as it goes, without overflowing the C stack, and it cannot raise. On two absvectors that contain themselves, which have no end, it ends the process with the message shen-jit: =: the term is cyclic or too large. So does a term that, below its first 256 levels of nesting, needs about 1 GB of comparisons waiting or 2^31 steps.

(hash X N), for a positive N, returns an integer within the bound of 0 to N that the kernel's port performance guide states, in fact from 1 to N-1, or 1 when N is 1, and looks at a structure's first 256 values. It is consistent with =: numbers hash by mathematical value (8, 8.0 and -0.0/0.0 agree), strings by bytes, symbols by their stored name hash, pairs and absvectors by their elements, byte buffers as strings of the same bytes, and pointers by kind and address. Procedures, streams and exceptions hash by family, since a moving collector gives them no stable address. Deviation: Shen/Scheme's hash distinguishes 8 from 8.0.

7. Globals

KLambda keeps separate value and function bindings. (set S X) requires a symbol and returns X. (value S) of a symbol with no value raises a catchable error; a non-symbol raises an error.

8. Errors and handlers

9. Control forms and application

10. Definitions, redefinition and sealing

11. eval-kl

(eval-kl X) lowers X in guest code and installs the result. A defun returns its name; any other form returns its value. Malformed forms raise an error. eval-kl never enters C recursively. X that is not a list is its own value, returned without lowering.

A definition's lambda form, which fn returns, is made the first time fn asks for it, not when the definition is read; shen.*gensym* still moves as if it had been made then. Deviation: until then, reading the shen.lambda-form property directly gives a placeholder, an absvector tagged shen-jit.lazy-form, where other ports give the closure.

12. Streams, files and time

13. Stage 1 order and extensions

Stage 1 runs, in order: install the stage 0 definitions; set *os*, *language*, *implementation*, *release*, *port* and *porters*; call shen.initialise; call shen.x.features.initialise with the feature list [shen/shen-jit], then shen.x.programmable-pattern-matching.initialise and shen.x.namespaces.initialise, and shen-jit.initialise, which registers the port's functions and defines thread; write the image, in which file streams are closed. make stlib loads the standard library later, into a running image, and calls stlib.initialise there.

This follows Shen/Scheme's build. The factorise-defun, expand-dynamic and type-annotations extensions are not initialized. As built, what define produces is evaluated factorised by the kernel's shen.factor, as Shen/Scheme's compiler factorises definitions; ps shows the definition as define produced it, and shen.*factorise?* stays false, as in Shen/Scheme. The command line is the kernel's launcher extension: main passes the process arguments to shen.x.launcher.main, which provides the repl, script and eval commands Shen/Scheme also uses, so Shen/JIT needs none in C beyond --seal and --save, which come before those arguments.

14. Memory, stack and compilation failures

15. Image format

An image is a header followed by sections and a trailer, all little-endian:

Loading reads the heap into a region at least five times its size, adds the base to every pointer in one linear pass over objects and pairs, does the same for the root arrays, and then applies the reconstruction rules in How Shen/JIT works: native entries from each procedure's kind, the standard streams rebound in place, and file streams left closed.