Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Backend protocol 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, or backend/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:

  1. Code generation - Emit target-specific instructions
  2. Calling convention - How functions pass arguments and return values
  3. Platform intrinsics - OS-specific operations (syscalls, host functions, etc.)
  4. Imported source provenance - Optional recognition of backend-owned library modules
  5. 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 yields void

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 extern bindings via udewy/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

TierExamplesRequired for self-hosting bootstrap?
Core intrinsics__load__, __store__, __alloca__, __signed_shr__Yes
Platform intrinsics__syscall3__, __host_log__, libc extern via C capabilitiesYes, but only the surface for the chosen target
Builtin constantsSYS_WRITE, STDOUT, browser host importsPer target
Optional extensionscanvas/WebGL, __call_extern_mixed_*__, SDL/ClayNo

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:

  1. Use only core intrinsics for direct operations
  2. Create a platform abstraction layer - a set of wrapper functions that call the appropriate platform intrinsics
  3. 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.