prototype of alternative forth
| src | first commit | |
| Makefile | first commit | |
| QUICKREF | first commit | |
| README | first commit | |
Kinship Interpreter — Reference Implementation =============================================== This is a tree-walking interpreter for the Kinship S4X64M ISA. It provides a shared library (libkinship) for embedding and a standalone binary (kinship) for running scripts or interactive use. Building -------- make # builds kinship, libkinship.a, libkinship.so make install # installs to /usr/local (set DESTDIR= to override) Requires: a C11-capable compiler (gcc or clang), make, ar. Usage ----- kinship file Run a source file kinship < file Run source from stdin kinship Interactive REPL (when stdin is a terminal) kinship -h Print usage Language notes -------------- Numbers Any token that parses as a decimal or 0x-prefixed hex integer is pushed onto the active data stack. ASCII character literals use a leading tick: 'A → 65 '\n → 10 (the character after the tick is taken literally) Comments A backslash and everything after it on the same line is ignored. \ this is a comment Words DEF name ... RETURN defines a user word. Words can be called directly by name or with CALL name. CALL is required when a word is defined after the call site. Control flow IF/FI, DO/JZ, DO/JNZ all *peek* TOS — the condition value is not consumed. Use DROP when you no longer need it. Conditional chains without DUP: EQ IF ... FI NOT IF ... FI DROP WORDS (Forth compatibility) Typing WORDS prints all currently defined words (builtins + any user-defined words registered via ks_register_native). Implementation details ---------------------- - The interpreter is a token-walker (no bytecode compilation step). DEF bodies are stored as token-array slices and re-walked on each call. This keeps the implementation small and straightforward. - Loops (DO/JNZ, DO/JZ) are implemented as recursive re-execution of the body token slice. - Memory (FETCH/STORE) is a flat 1 MiB byte array inside ks_vm_t. Addresses are byte offsets; values are 64-bit little-endian. - The two data stacks (DS1/DS2) and the SWITCH/THROW/CATCH primitives are fully implemented. CD0 tracks the active stack. - LEAVE inside a loop exits immediately; LEAVE outside a loop acts as RETURN (matches the spec). - BYE halts the VM and returns KS_ERR_HALTED to the caller. Known limitations ----------------- - No bytecode compiler yet (see the open question below). - RECV reads one line from stdin and parses it as a decimal integer. - MAX_TOKENS is 65 536 — source files larger than ~500 KB may hit this limit; increase MAX_TOKENS in vm.c if needed. - User-defined words are local to a single ks_exec_string call. Call ks_exec_string once with the full source, not line by line, if you need words defined in one call to be visible in the next. Open question: compiler in the same binary? ------------------------------------------- Short answer: no, not hard at all — and yes, it fits naturally in one binary. The current interpreter walks the token stream twice at most (once to collect DEF bodies, once to execute). A compiler would replace the execution pass with a code-generation pass that emits a compact bytecode (e.g. a 1-byte opcode + optional 8-byte immediate). What that looks like in practice: 1. A second pass function ks_compile(interp_t*, uint8_t *out, size_t *len) walks the same token array and emits opcodes. 2. A bytecode executor ks_run_bytecode(ks_vm_t*, uint8_t*, size_t) replaces the recursive token-walker with a tight dispatch loop. 3. `kinship file` → compile + run (already the implied semantics). `kinship -c file` → compile only, write .ksb file. `kinship < file` → interpret directly (no compile step needed). The token-walker and the compiler can share the same word table and the same ks_vm_t struct — they only differ in what they do with each token. The binary stays one file, the library gains one extra symbol (ks_compile), and the total added code is roughly the size of vm.c itself. The main challenge is resolving forward CALL addresses during compilation, which requires a two-pass approach or a fixup table — both are standard techniques.