Lynxer documentation
Lynxer is a small, standalone C++ toolchain for the Lynxer language. It runs .lynx programs without a Python runtime and ships its standard library as native shared libraries. It provides standalone Linux and Windows executables, 40 natively backed modules, an AST optimizer, a frozen native-module ABI, a source debugger, a language server and performance benchmarks. See limitations for planned runtime and standard-library improvements.
Contents
Getting started
| Document | What it covers |
|---|---|
| install | Requirements, build commands, artifacts, environment variables |
| CLI | Every flag, exit code, and error behaviour of the lynxer executable |
| language-server | stdio LSP setup, diagnostics, completion, hover and navigation |
| benchmarks | Interpreter, optimizer and compiled startup measurements |
The language
| Document | What it covers |
|---|---|
| language | Program structure, variables, operators, control flow, functions, scoping, interpolation |
| functions | The global, func, local, and named lambda forms, inferred generic parameters, and caller-supplied codeblocks |
| macros | File-scoped macros, pub exports, token arguments and codeblock parameters |
| operators | The operator set and precedence |
| conditionals | if/elif/else and switch/case/default |
| loops | while, for (C style and range), doWhile, iterate, forever |
| try-catch | The try/catch statement, raise, and exceptionInfo() |
| types | Every type name, ranges, and conversion rules |
| lists | list and tuple values and their builtins |
| tuples | Immutable ordered sequences |
| codeblocks | Inline codeblock arguments |
| structs | Data-only records |
| classes | Fields, local methods, constructors, and extends inheritance |
| enums | Tagged unions and switch patterns |
| vargroups | Records with defaulted fields |
| modules | import, search order, module member access |
| importAs | Aliasing an imported module |
Internals and reference
| Document | What it covers |
|---|---|
| builtins | Every function the interpreter implements itself |
| syscalls | The named Linux syscall wrappers: OS/architecture gate, namespaces, full list |
| async | Local async sub-functions, await, and the async* builtins |
| native-memory | Typed raw memory, blocks/arrays/views, native struct layouts |
| filesystem | The handle-based filesystem* API |
| networking | The managed networking* socket API |
| process | The managed process* subprocess API |
| native-module-abi | Writing a native .so module: entry point, signatures, data conventions |
| native-modules | Using native modules, the nativeModule*/ffi* handle APIs |
| embedding | Exporting Lynxer functions to a C ABI other languages can call |
| stdlib-contracts | The frozen contract every wrapper and backend must satisfy |
| extending | Adding a stdlib module end to end |
| stdlib/ | One page per standard-library module |
| legacy-surface | The original feature surface and its replacements |
| removed-features | Bytecode, Python bridging and other removed features |
| limitations | Deliberate constraints and what is not implemented |
| windows | Windows build status, embedding, and the features excluded from it |
Build and run
make buildLynxer # interpreter + every native stdlib module
lynxer/lynxer program.lynx
lynxer/lynxer --list-stdlibs
The root Makefile builds both implementations; make alone builds everything. make buildLynxer builds the interpreter and the native modules, and make buildLynxerArm64 cross-builds the ARM64 interpreter. make testLynxer runs the whole suite:
lynxer/scripts/check_module_contracts.py— wrapper/backend contract check.- Every
lynxer/examples/*.expectedfixture, diffed on stdout+stderr, including the optimizer and low-level native-memory/syscall fixtures. - Interpreted-versus-
--compileparity for a set of fixtures. - The
--format/--format-onelineformatter fixture (output, idempotence, and that the formatted file still runs) and the--validate-executeableself-check. - The bundled-executable,
--include, and bytecode-removal checks.
Both Lynxer CI workflows (.github/workflows/build-lynxer-linux-amd64.yml and build-lynxer-linux-arm64.yml) run make testLynxer LYNXER_SKIP_DISPLAY=1, which drops the fixtures that need a display or an audio device (a CI runner has neither, and the graphics backend crashes without a display).
A Rust toolchain is required: every Rust-backed module and the native-call engine are built by cargo, and a missing toolchain or a failed build fails make instead of dropping a backend. python3 is required for the two check scripts in the test suite.
Standard library modules
There are 43 bundled modules. A module is exposed to Lynxer by stdlib/<name>.lynx and backed by a stdlib/<name>.so. The backends marked Rust come from a crate under rust/; pure modules are written in Lynxer only and need no shared library.
| Module | Backend | Implementation |
|---|---|---|
| cli | native | POSIX process/env/terminal APIs |
| colorlib | pure | ANSI escape sequences |
| compress | Rust | flate2/zstd/brotli/lz4_flex/zip/tar (rust/compress) |
| crypto | Rust | sha2/sha1/md-5/sha3/blake3/hmac/subtle/getrandom/ed25519-dalek (rust/crypto) |
| csv | native | hand-written CSV/TSV reader and writer |
| debug | native + pure | <chrono>, getrusage, assertions in Lynxer |
| encoding | Rust | base64, hex, data-encoding, bs58, ascii85, percent-encoding, quoted_printable (rust/encoding) |
| fileIO | native | <fstream>, <filesystem> |
| game | Rust | macroquad (rust/game) |
| graphics | Rust | macroquad (rust/graphics) |
| image | Rust | image (rust/image) |
| ini | Rust | rust-ini (rust/ini) |
| js | native | the node binary |
| json | Rust | serde_json (rust/json) |
| lua | Rust | mlua with vendored Lua 5.4 (rust/lua) |
| math | native | <cmath> plus statistics and vector helpers |
| multiprocessing | native + pure | std::thread and shell subprocesses |
| network | Rust | ureq + tungstenite over rustls |
| os | native | <filesystem>, POSIX |
| path | native | <filesystem>, POSIX stat |
| random | native | seeded linear congruential generator in C++ |
| raylib | Rust | raylib 6.0 via raylib-sys (rust/raylib) |
| re | Rust | fancy-regex (rust/re) |
| regex | Rust | fancy-regex with a named-pattern cache (rust/regex) |
| server | Rust | axum + tokio |
| shell | native | popen, std::system |
| sound | Rust | rodio + cpal + symphonia |
| sqldb | Rust | rusqlite (bundled SQLite) |
| sys | native | C++ runtime and POSIX |
| text | Rust + Lynxer | Unicode operations and string helpers |
| time | native | <chrono>, <ctime> |
| toml | Rust | toml (rust/toml) |
| tui | Rust | ratatui + crossterm |
| typing | pure | Lynxer type builtins |
| uuid | Rust | uuid (rust/uuid) |
| watch | Rust | inotify (rust/watch) |
| xml | Rust | quick-xml (rust/xml) |
| yaml | Rust | serde_yml (rust/yaml) |
The Rust workspace has twenty-six member crates: the twenty-three module cdylib backends listed above (LYNXER_RUST_MODULE_NAMES in the Makefile), the two crates they share (abi, and regex_engine, the fancy-regex engine behind re/regex), and ffi, the required native-call engine — a staticlib linked into the interpreter through lynxer/ffi_abi.h, not a stdlib module. It is built on the libffi crate, so cargo and libffi headers are required to build the interpreter; see native-module-abi. There is no CMake staging step and no third_party/ directory: TLS is rustls (no system OpenSSL) and the HTTP/WebSocket stack is pure Rust.
venv and the OS-native GUI/turtle modules are intentionally excluded — see limitations. The graphics module covers the immediate-mode drawing/window/UI space with macroquad.
Compiling a program
--compile produces one self-contained executable that embeds the program, every transitively imported .lynx source and .so library, plus anything added with --include:
lynxer/lynxer --compile app.lynx extras/helpers.lynx \
--include vendor/libcustom.so --include assets/message.txt -o app
The first .lynx file is the program; further .lynx/.so inputs are embedded and importable by name. A non-.lynx/.so include is embedded as data and read with bundledFile(name) / bundledFiles(). See CLI.
Adding a stdlib module
- Write the backend. A C++ module is
stdlib/<name>.cppexportinglynxer_module_init_v1and oneextern "C"function per entry; it is picked up automatically by thestdlib/*.cppwildcard. A Rust module is a crate underrust/exporting the same entry point plus one#[no_mangle] extern "C"op per entry (thelynxer_abicrate provides the packing, panic guards, and registration helper); add it to the workspace and toLYNXER_RUST_MODULE_NAMESin the root Makefile. See native-module-abi.
An op generated by a Rust export_*! macro must use a packed signature — cdecl:int64(...), cdecl:float64(...), or cdecl:cstring(...) — never a fixed shape such as cdecl:int64(int64). The export_*! macros generate a four-scalar packed prototype, so a fixed-shape declaration calls the symbol through the wrong C prototype and segfaults on the first call, with no build-time warning. A hand-written Rust op can use cdecl:v2:value(value) for typed aggregates; see native-module-abi.
- Write
stdlib/<name>.lynx. Itssetup()imports the library withimportAs("<name>.so", "native<Name>")and forwards each function asglobal f(...) { return global.nativeName.f(...); }. Start the file with a line containing exactly////, then the description, then another////;lynxer --list-stdlibsprints that block.
- Add
examples/stdlib_<name>.lynxplus a siblingstdlib_<name>.expected. The suite runs everyexamples/stdlib_*.lynxand diffs it against its.expectedoutput.
- Run
make testLynxer. It runslynxer/scripts/check_module_contracts.py, which compares the wrapper against the backend: everyglobal.native<Alias>.<op>(...)call must name a registered op, and for a Rust backend everyargs.<kind>(i)read must be in range for the arguments the wrapper passes. This catches contract mismatches a fixture cannot — a fixture records what the code does, so it will happily assert a broken module whose behaviour it reproduces.
See extending and stdlib-contracts for the full checklist and the contract.
Environment variables
| Variable | Effect |
|---|---|
LYNXER_OPT_REPORT=1 | Print the AST optimizer's transformation counts to stderr after the run |
LYNXER_GAME_HEADLESS=1 | Run the game module without opening a window |
LYNXER_GRAPHICS_HEADLESS=1 | Run the graphics module without opening a window |
LYNXER_SKIP_DISPLAY=1 | make testLynxer skips the display/audio fixtures (used by CI) |
File-wide functions
Use func for a helper that belongs to the current source file:
global setup(){}
func double(int value) -> int {
return value * 2;
}
global main(){
println(double(21)); // 42
}
func declarations must be top-level and appear between global setup() and global main(). They are called by bare name in their defining file. When a file is imported as a module, callers use global.moduleName.functionName(...). func names are file-scoped, so two different imported files may define the same helper name. global func name(...) is not valid syntax; see language.