4.3 Backend Architecture
udewy's compilation model is designed to be modular with respect to target platforms. A backend encapsulates all target-specific concerns:
- Architecture: CPU instruction set (x86_64, RISC-V, AArch64, WASM, etc.)
- Operating System: System call interface and conventions (Linux, Windows, macOS, bare metal, browser, etc.)
- Output Format: Executable format (ELF, PE, Mach-O, WASM, etc.)
Parser / Backend Boundary
The parser (p0.py) is responsible for parsing udewy source and translating the result into calls on the abstract Backend interface. It should not contain target-specific logic or special-case knowledge about concrete backends.
Concrete backends are responsible for responding to those abstract operations in whatever way is appropriate for their target. They may differ in calling conventions, instruction selection, intrinsic support, output format, and runtime environment, but those differences should be expressed through the Backend protocol rather than through extra coupling with the parser.
In general, the parser, tokenizer/import preprocessor, and concrete backends should only know about each other through what is described by Backend in backend/common.py and the generic import outputs in t0.py. Changes to these shared surfaces should therefore be relatively rare and made only when they provide a clear architectural benefit, such as substantially simplifying the parser/backend relationship, removing complexity, or enabling an important capability that cleanly belongs in the shared abstraction.
When editing udewy, avoid adding concrete backend names, target ABI rules, syscall tables, host-library conventions, file-layout assumptions, or intrinsic families directly to the parser or import preprocessor. Prefer one of these shapes instead:
- If every backend must implement the behavior, add a small generic method or hook to
Backend. - If only some targets support the behavior, keep the names, validation rules, lowering, constants, and diagnostics inside those backends.
- If source imports need to carry backend-specific configuration, pass generic import provenance through the
Backendprotocol and let the selected backend interpret its own library modules. - If a backend needs platform constants or syscall tables, keep them in backend-owned modules rather than in
p0.py,t0.py, orbackend/common.py.
udewy-native programs that do not rely on extern declarations use udewy's own entry point and do not require C runtime startup code. When extern declarations are used, the final link additionally depends on whatever artifacts are provided to satisfy those extern symbols.
Backend Responsibilities
Each backend implements the parser protocol and provides:
- Code generation - Emit target-specific instructions
- Calling convention - How functions pass arguments and return values
- Platform intrinsics - OS-specific operations (syscalls, host functions, etc.)
- Imported source provenance - Optional recognition of backend-owned library modules
- Memory model - Address space layout and constraints
Intrinsic Categories
Intrinsics and backend-provided names fall into tiers. The tiers matter for portability, for self-hosting, and for thinking about what a minimal trusted-base implementation would need to reproduce.
Minimal core (language-level)
These are the operations defined in Part 2: Core Intrinsics. Every backend in this repository implements them:
- Memory:
__load__/__store__and the sized variants,__alloca__,__static_alloca__,__static_words__ - Arithmetic helpers:
__signed_shr__,__unsigned_idiv__,__unsigned_mod__, unsigned comparisons - Debugging:
__breakpoint__()traps into a debugger attached to the process (int3,ebreak,brk #0; a no-op where the target has no debugger protocol, such as wasm32) and yieldsvoid
A program that uses only core intrinsics plus ordinary udewy syntax is backend-portable at the language level. It still needs a platform layer for I/O, allocation, and exit unless it is fully freestanding.
This tier is the practical floor for a self-hosting compiler, a hand-written bootstrap implementation, or a future minimal single-backend profile (for example targeting only RISC-V Linux). It deliberately excludes graphics, browser host functions, mixed FP extern calls, and third-party library shims.
Platform surface (per-backend)
Each backend adds the intrinsics and builtin constants needed for its environment:
- Native Linux backends (
x86_64,riscv,arm):__syscall0__through__syscall6__, plus syscall numbers, file descriptors, and mmap/open flags - WASM browser backend: host functions such as
__host_log__,__host_exit__, and (when used) canvas/DOM/WebGL/input intrinsics documented in Addendum D - C backend: no syscalls; hosted behavior comes from explicit
externbindings viaudewy/third_party/c/capability imports
Portable programs should call these through a platform abstraction layer (wrapper functions + target-specific imports), not by sprinkling backend-specific intrinsics through application logic.
Optional extensions (not required for the language or bootstrap)
These are real and supported in this reference implementation, but they are not part of the minimal udewy language contract:
- Mixed GP/FP extern calls (
__call_extern_mixed_N__, float bit-pattern helpers) on native Linux and C backends — for linking to C libraries such as SDL/OpenGL - Third-party integrations documented in Addendum E (SDL, Clay)
- WASM graphics and input (canvas, WebGL, pointer/keyboard) — for browser demos and interactive programs
A smaller trusted-base compiler could omit entire extension families as long as the programs it is asked to compile do not use them.
Summary table
| Tier | Examples | Required for self-hosting bootstrap? |
|---|---|---|
| Core intrinsics | __load__, __store__, __alloca__, __signed_shr__ | Yes |
| Platform intrinsics | __syscall3__, __host_log__, libc extern via C capabilities | Yes, but only the surface for the chosen target |
| Builtin constants | SYS_WRITE, STDOUT, browser host imports | Per target |
| Optional extensions | canvas/WebGL, __call_extern_mixed_*__, SDL/Clay | No |
For example:
- Linux backends provide
__syscall0__through__syscall6__intrinsics, plus builtin constants for syscall numbers (SYS_WRITE,SYS_EXIT, etc.) and common flags - The WASM browser backend provides
__host_log__,__host_time__, etc. for browser interaction - A hypothetical Windows backend would provide different intrinsics for Win32 API calls
Builtin Constants
Linux backends automatically provide constants for:
- Syscall numbers:
SYS_READ,SYS_WRITE,SYS_OPEN,SYS_CLOSE,SYS_EXIT, etc. - File descriptors:
STDIN,STDOUT,STDERR - Open flags:
O_RDONLY,O_WRONLY,O_RDWR,O_CREAT,O_TRUNC,O_APPEND - Memory mapping flags:
PROT_READ,PROT_WRITE,PROT_EXEC,MAP_SHARED,MAP_PRIVATE,MAP_ANONYMOUS
These constants are available without explicit declaration:
# No need to declare SYS_WRITE - it's provided by the x86_64 backend
let msg = "Hello\n"
let len = __load__(msg - 8)
__syscall3__(SYS_WRITE STDOUT msg len)
Note: Syscall numbers differ between architectures. x86_64 uses the traditional Linux syscall numbers, while RISC-V and AArch64 use the newer unified syscall table (e.g., SYS_OPENAT instead of SYS_OPEN).
Writing Portable Code
To write udewy programs that work across multiple backends:
- Use only core intrinsics for direct operations
- Create a platform abstraction layer - a set of wrapper functions that call the appropriate platform intrinsics
- Import the correct platform module for your target
Example structure:
program.udewy # main program using platform API
├── platform_api.udewy # abstract interface (print, alloc, exit, etc.)
├── platform_linux_x86_64.udewy # Linux x86_64 implementation
├── platform_linux_riscv.udewy # Linux RISC-V implementation
├── platform_wasm.udewy # Browser WASM implementation
└── platform_windows.udewy # (future) Windows implementation
For the hosted c backend, "portable" usually means keeping direct operations within core udewy semantics and treating libc or OS APIs as explicit extern boundaries. The backend itself does not provide syscall intrinsics or builtin constants; if a program binds to calloc, printf, syscall, or a platform SDK symbol, that portability boundary is owned by the program.