{"id":48268188,"url":"https://github.com/no382001/forth-vm","last_synced_at":"2026-04-20T10:01:12.553Z","repository":{"id":332099943,"uuid":"1132145456","full_name":"no382001/forth-vm","owner":"no382001","description":"Forth VM and statically-typed s-expression compiler, written in C++ and Scryer Prolog","archived":false,"fork":false,"pushed_at":"2026-04-07T20:45:23.000Z","size":97,"stargazers_count":16,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-07T22:29:35.424Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Prolog","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/no382001.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-11T12:19:26.000Z","updated_at":"2026-04-07T20:45:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/no382001/forth-vm","commit_stats":null,"previous_names":["no382001/forth-vm"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/no382001/forth-vm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/no382001%2Fforth-vm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/no382001%2Fforth-vm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/no382001%2Fforth-vm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/no382001%2Fforth-vm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/no382001","download_url":"https://codeload.github.com/no382001/forth-vm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/no382001%2Fforth-vm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32042293,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-20T00:18:06.643Z","status":"online","status_checked_at":"2026-04-20T02:00:06.527Z","response_time":94,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-04T22:00:29.174Z","updated_at":"2026-04-20T10:01:12.545Z","avatar_url":"https://github.com/no382001.png","language":"Prolog","funding_links":[],"categories":["Prolog"],"sub_categories":[],"readme":"# forth-vm\n\n\u003e **draft** — work in progress, details may change\n\n16-bit stack VM with a statically-typed s-expression compiler called **sets** *((**s**)-(**e**)xpression (**t**)yped (**s**)ystems language)*. VM in C++20, compiler in Scryer Prolog.\n\n## contents\n\n- [build](#build)\n- [usage](#usage)\n- [test](#test)\n- [language](#language)\n  - [syntax](#syntax-ebnf)\n  - [types](#types)\n  - [effect annotations](#effect-annotations)\n  - [built-in primitives](#built-in-primitives)\n  - [directives](#directives)\n  - [expressions](#expressions)\n  - [example](#example)\n- [compiler pipeline](#compiler-pipeline)\n- [VM](#vm)\n  - [memory model](#memory-model)\n  - [instruction set](#instruction-set)\n\n## build\n\n```sh\nmake\n```\n\nRequires: C++20 compiler, [Scryer Prolog](https://github.com/mthom/scryer-prolog)\n\n## usage\n\n```sh\n./run programs/echo.sets\n```\n\n## test\n\n```sh\nmake test\n```\n\nRequires: [bats](https://github.com/bats-core/bats-core)\n\n\u003e the inline `?-` tests in the compiler `.pl` files won't run unless you use the [quads branch of Scryer](https://github.com/no382001/scryer-prolog/tree/quads)\n\n## language\n\n### syntax (EBNF)\n\n```ebnf\nprogram    = form* ;\nform       = list | bracket | asm | string | number | symbol ;\nlist       = '(' form* ')' ;\nbracket    = '[' form* ']' ;\nasm        = '{' symbol* '}' ;\nstring     = '\"' char* '\"' ;\nnumber     = '-'? digit+ ;\nsymbol     = (any char except whitespace, parens, brackets, braces, '\"', ';')+ ;\n\ntop-level  = def | extern | const | directive ;\ndef        = '(' 'def' symbol params ':' type effect? expr+ ')' ;\nextern     = '(' 'extern' symbol param-types ':' type ')' ;\nconst      = '(' 'const' symbol type expr ')' ;\ndirective  = '(' '$include' string ')'\n           | '(' '$section' number ')'\n           | '(' '$alloc' symbol number ')' ;\n\nparams     = '(' param* ')' ;\nparam      = symbol | '(' symbol ':' type ')' ;\nparam-types = '(' type* ')' ;\neffect     = '[' 'det' ']' | '[' 'semidet' ']' | '[' 'nondet' ']' ;\n\ntype       = 'int' | 'byte' | 'bool' | 'void' | '(' 'ptr' type ')' ;\n\nexpr       = number | string | symbol\n           | '(' 'if' expr expr expr ')'\n           | '(' 'let' '(' binding* ')' expr+ ')'\n           | '(' 'do' expr+ ')'\n           | '(' 'while' expr expr+ ')'\n           | '(' 'deref' expr ')'\n           | '(' 'deref8' expr ')'\n           | '(' 'store' expr expr ')'\n           | '(' 'store8' expr expr ')'\n           | '(' 'addr' symbol ')'\n           | '(' 'execute' expr ')'\n           | '(' binop expr expr ')'\n           | '(' symbol expr* ')'\n           | '{' symbol* '}' ;\n\nbinding    = '(' symbol expr ')' ;\nbinop      = '+' | '-' | '=' | '\u003c' | '\u003e' | '!=' | '\u003c=' | '\u003e=' | 'and' | 'or' | 'xor' ;\n```\n\nComments start with `;` and run to end of line.\n\n### types\n\n| type | description |\n|------|-------------|\n| `int` | signed integer (`cell_t` wide) |\n| `byte` | 8-bit value (always) |\n| `bool` | boolean (0 or 1) |\n| `void` | no value |\n| `(ptr T)` | pointer to T |\n\n\u003e Sizes depend on the VM configuration. To change them, edit `src/vm.h`:\n\u003e - `cell_t` — the native cell type; determines the width of `int` and `(ptr T)` (default: `int16_t`)\n\u003e - `MEMORY_SIZE` — total address space in bytes (default: `0xFFFF`)\n\u003e - `STACK_SIZE` — max depth of each stack in cells (default: `256`)\n\n### effect annotations\n\nFunctions can optionally declare their computational effect:\n\n| annotation | meaning |\n|------------|---------|\n| `[det]` | pure, no side effects and thus eligible for constant folding |\n| `[semidet]` | reads or writes memory |\n| `[nondet]` | performs I/O or otherwise escapes |\n\nThe compiler infers effects and errors if a declaration is stricter than inferred. Unannotated `det` functions produce a warning.\n\n### built-in primitives\n\n| primitive | signature | description |\n|-----------|-----------|-------------|\n| `emit` | `(int) : void` | write character to stdout |\n| `key` | `() : int` | read character from stdin |\n| `bye` | `() : void` | halt the VM |\n\nInline VM opcodes can be emitted directly with `{...}`, bypassing the type system.\n\n### directives\n\nDirectives are compile-time only — they emit no code.\n\n| directive | description |\n|-----------|-------------|\n| `($include \"file\")` | splices `file` into the current program at this point, resolved relative to the including file's directory |\n| `($section addr)` | sets the static allocation cursor to `addr` |\n| `($alloc name size)` | binds `name` to the current cursor as an `int` constant, then advances the cursor by `size` bytes |\n\n### expressions\n\n**`(let ((x expr) ...) body...)`** — binds names to values for the duration of `body`. Each binding is stored at a statically-assigned memory address allocated per-function starting at `0x4000`. No allocation occurs at runtime; the addresses are fixed at compile time.\n\n**`(addr name)`** — pushes the address of a named function as an integer, without calling it. Used to pass functions as values.\n\n**`(execute expr)`** — evaluates `expr` to get a function address, then calls it as an indirect call. Combined with `addr`, this is the only form of indirect dispatch.\n\n**String literals** — a string `\"hello\"` compiles to a `branch` over its bytes, which are embedded inline in the code segment as null-terminated bytes, followed by a `lit` of the string's start address. Strings are read-only and live in the code region.\n\n### example\n\n```lisp\n($section 1022)\n($alloc STRI  2)\n($alloc CHAR  2)\n($alloc IDX   2)\n($alloc BUF 256)\n\n($include \"core.sets\") ; definitions for `true` and `false`\n\n(def streq ((a : int) (b : int)) : bool\n  (store STRI 0)\n  (while (if (= (deref8 (+ a (deref STRI))) (deref8 (+ b (deref STRI))))\n             (!= (deref8 (+ a (deref STRI))) 0)\n             false)\n    (store STRI (+ (deref STRI) 1)))\n  (= (deref8 (+ a (deref STRI))) (deref8 (+ b (deref STRI)))))\n\n(def main () : void\n  (while true\n    (store IDX 0)\n    (while (do (store CHAR (key))\n               (!= (deref CHAR) 10))\n      (store8 (+ BUF (deref IDX)) (deref CHAR))\n      (store IDX (+ (deref IDX) 1)))\n    (store8 (+ BUF (deref IDX)) 0)\n    (if (streq BUF \"bye\")\n      (bye)\n      (do (store IDX 0)\n          (while (!= (deref8 (+ BUF (deref IDX))) 0)\n            (emit (deref8 (+ BUF (deref IDX))))\n            (store IDX (+ (deref IDX) 1)))\n          (emit 10)))))\n```\n\nMemory layout of the above program at runtime (default config):\n\n```\n; $section 1022 sets the cursor; each $alloc advances it\n\n addr  0        ~N    1022  1024  1026  1028            1283\n       ┌────────┬─────┬─────┬─────┬─────┬───────────────┐\n       │ code   │     │STRI │CHAR │ IDX │      BUF      │ ...\n       └────────┴─────┴─────┴─────┴─────┴───────────────┘\n                       \u003c-2-\u003e \u003c-2-\u003e \u003c-2-\u003e \u003c---256 bytes---\u003e\n\n; \"bye\" string literal is inlined in the code region (branch over + bytes + lit addr)\n\n; let/param slots are statically assigned per-function starting at 0x4000\n\n addr  16384  16386   16404\n       ┌──────┬───────┬────────── ...\n       │  a   │   b   │  main slots\n       │      streq   │\n       └──────┴───────┴────────── ...\n\n; stacks and registers occupy the top of address space\n; (with MEMORY_SIZE=65535, STACK_SIZE=256, CELL_SIZE=2)\n; DS_START = 64511, RS_START = 65023\n\n addr  64505   64511          65023          65535\n       ┌───────┬──────────────┬──────────────┐\n       │ regs  │   rstack     │   dstack     │\n       │SP RP  │   512 B      │   512 B      │\n       └───────┴──────────────┴──────────────┘\n```\n\n## compiler pipeline\n\nThe compiler is a multi-pass Prolog program (`compiler/`):\n\n| stage | file | description |\n|-------|------|-------------|\n| parse | `parser.pl` | characters -\u003e s-expression forms (DCG) |\n| ast | `ast.pl` | forms -\u003e typed AST nodes |\n| typecheck | `typecheck.pl` | monomorphic type synthesis + checking |\n| effects | `effects.pl` | fixed-point effect inference (det/semidet/nondet) |\n| dead code | `deadcode.pl` | warn on unreachable definitions |\n| const fold | `constfold.pl` | fold `det` calls with constant arguments |\n| codegen | `codegen.pl` | AST -\u003e VM token sequence |\n| emit | `emit.pl` | tokens -\u003e binary bytecode |\n\nThe assembler binding (`gen/gen.pl`) is auto-generated from the C++ dispatch table, keeping the Prolog compiler in sync with the VM instruction set.\n\n## VM\n\n### memory model\n\nFlat byte-addressable memory. Programs are loaded at address 0 and grow upward. The two stacks (data and return) are fixed-size regions at the top of the address space, laid out downward from `MEMORY_SIZE`:\n\n```\n0                                                           MEMORY_SIZE\n┌─────────────────────────────────┬───────┬──────────┬──────────┐\n│ program + heap (grows -\u003e)       │ regs  │  rstack  │  dstack  │\n└─────────────────────────────────┴───────┴──────────┴──────────┘\n                                  DS_START-n DS_START  RS_START\n```\n\nEverything is memory-mapped, there are no loose parts. The instruction pointer (`IP`), data stack pointer (`SP`), and return stack pointer (`RP`) are stored as cells in memory just below the stack region (`DS_START - 1..3`). The stacks themselves are contiguous cell arrays in memory. Nothing lives outside the flat address space.\n\nThis means that you can just dump memory in runtime, and when you load it back in you have restored a snapshot of the execution.\n\n`$section` / `$alloc` directives place named variables in the heap region; the compiler does not manage the heap at runtime.\n\nWord size, memory size, and stack depth are set in `src/vm.h` (see [types](#types)):\n\n| constant | default | effect |\n|----------|---------|--------|\n| `cell_t` | `int16_t` | native word width |\n| `MEMORY_SIZE` | `0xFFFF` | total address space in bytes |\n| `STACK_SIZE` | `256` | max depth of each stack in cells |\n\n### instruction set\n\nMinimal threaded-style bytecode:\n\n| group | opcodes |\n|-------|---------|\n| literals | `nop` `lit` |\n| memory | `@` `!` `c@` `c!` |\n| stack | `dup` `drop` `swap` `over` |\n| return stack | `\u003er` `r\u003e` `r@` |\n| alu | `+` `-` `and` `or` `xor` `=` `\u003c` |\n| control | `branch` `0branch` `call` `ret` `execute` |\n| system | `trap` |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fno382001%2Fforth-vm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fno382001%2Fforth-vm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fno382001%2Fforth-vm/lists"}