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

The μDewy Subset Programming Language

udewy (μdewy, "micro-dewy") is a strict subset of the Dewy programming language, designed for bootstrapping. It serves as an intermediate step in a trusted computing base, providing a language simple enough to implement in assembly while being expressive enough to write a real compiler.

Key principle: Any well-formed udewy program should compile and behave identically under both the udewy compiler and the full Dewy compiler.

This document serves as the definitive specification for the udewy language. There is no implementation-defined behavior; all semantics are fully specified here.

Quick Start

# Run a udewy program (default x86_64 target)
python -m udewy.p0 udewy/tests/test_hello.udewy

# Compile only (don't run)
python -m udewy.p0 -c udewy/tests/test_hello.udewy

# Target a different backend
# For wasm32, this opens the generated HTML in your browser
python -m udewy.p0 --target wasm32 udewy/tests/test_hello.udewy
python -m udewy.p0 --target riscv udewy/tests/test_hello.udewy
python -m udewy.p0 --target arm udewy/tests/test_hello.udewy

The compiler writes artifacts under __dewycache__/, mirroring the source path relative to the current directory. udewy path/to/main.udewy produces __dewycache__/path/to/main. A path already under __dewycache__/ is not nested again. Sources outside the current directory go under __dewycache__/__external__/<12-hex>/….

NOTE: long-term goals is for the default compile target to match the host machine/OS

Supported Targets

TargetOutputRequirements
x86_64 (default)Linux ELF executableGNU as, ld
wasm32Single HTML with embedded WASMwat2wasm (wabt)
riscvRISC-V 64-bit executableriscv64-linux-gnu toolchain, qemu-riscv64
armAArch64 executableaarch64-linux-gnu toolchain, qemu-aarch64

Hello World

# SYS_WRITE and STDOUT are builtin constants provided by the x86_64 backend
let main = ():>int => {
    let msg:int = "Hello from udewy!\n"
    let len:int = __load__(msg - 8)
    __syscall3__(SYS_WRITE STDOUT msg len)
    return 0
}

Status & Scope

This repo contains a reference implementation of udewy, including a lot of supplimentary features for more pleasant everyday use (e.g. multiple backends, graphics support, float bit helpers, etc). The core trusted computing rung for udewy will likely be a more simplified implementation targeting a single backend (likely risc-v), and skipping most of the other nice-to-have features present in this implementation.

Part 1: Core Language Specification

1.1 Lexical Structure

Character Set

udewy source code is ASCII. Bytes outside the ASCII range may only appear inside string literals, path literals, and comments; the compiler never interprets them. Inside string literals they pass through to the program verbatim (see String Literals).

Valid ASCII characters:

  • Letters: A-Z, a-z
  • Digits: 0-9
  • Symbols: ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ { | } ~
  • Whitespace: space (0x20), tab (0x09), carriage return (0x0D), newline (0x0A)

Whitespace

Whitespace characters (space, tab, carriage return, newline) are ignored except as token separators. There are no significant indentation or newline rules.

Comments

Only line comments are supported. A # character begins a comment that extends to the end of the line:

# This is a comment
let x:int = 42  # inline comment

Block comments are not supported.

Two comment forms carry metadata for debuggers. A line of the form # @loc path:line:column names the source position of the statements that follow it (until the next such line); a compiler that emits udewy from another language uses it to point a debugger at the original source. Without markers, a statement's position is its own line in the udewy file. A line of the form # @var name shown formatter type right before a declaration (or before a function, for its parameters) describes that variable to the debugger: the name it is shown under (- keeps the declared name), a function in the program a debugger may call to render the value as text (- for none; it takes the variable's word and returns the address of a [length][bytes] block), and its type as spelled, to the end of the line; # @var name - hides the declaration (a compiler's temporary). Without a marker a variable is shown under its own name with its udewy annotation. Both are debug information only — a backend that ignores them produces the same program — and reach the backend through the mark_location, note_local, begin_scope, and end_scope hooks of the Backend protocol; the x86_64 backend emits DWARF line and variable information from them.

Identifiers

Identifiers consist of letters, digits, and underscores, and must begin with a letter or underscore:

identifier ::= [a-zA-Z_][a-zA-Z0-9_]*

Identifiers are case-sensitive. The following are reserved keywords and cannot be used as identifiers:

let  const  if  else  loop  break  continue  return
import  extern  transmute  and  or  xor  not  true  false  void

NOTE: import is a preprocessing-only directive. It is recognized before any actual code; any import that reaches tokenization is an error.

Number Literals

udewy supports three number formats. All produce 64-bit integer values. Underscore separators (_) are allowed anywhere within the digit sequence for readability.

Decimal integers:

42
1_000_000
0

Hexadecimal integers (prefix 0x, digits case-insensitive):

0xff
0x1a2b_3c4d
0xDEAD_BEEF

Binary integers (prefix 0b):

0b1010
0b1111_0000

All number literals are unsigned and must fit within 64 bits.

Boolean Literals

Boolean values are represented as specific 64-bit integer patterns:

LiteralValueBit Pattern
true-1 (signed) / 18446744073709551615 (unsigned)0xFFFF_FFFF_FFFF_FFFF (all bits set)
false00x0000_0000_0000_0000 (no bits set)

This representation allows bitwise operators (and, or, xor, not) to function correctly as both bitwise and logical operators.

String Literals

Strings are enclosed in double quotes. Strings may span multiple lines:

let msg:int = "Hello, World!"
let multi:int = "This string
spans multiple lines"

Escape sequences: A backslash followed by certain characters produces special values:

EscapeValueDescription
\n10Newline (line feed)
\t9Horizontal tab
\r13Carriage return
\00Null byte
\xHH / \XHH0xHHHex byte escape (x and the two hex digits are case-insensitive)
\<newline>(none)Line continuation - the newline is skipped
\<other><other>Any other character produces that character literally

The last rule means \" produces a double-quote character and \\ produces a backslash. Hex byte escapes insert a single raw byte; for example, "\x41" is the byte 0x41 ('A'), and "\xce\xbc" is the UTF-8 encoding of μ.

Line continuation: A backslash immediately before a newline causes that newline to be skipped:

let long:int = "\
This is a very \
long string that appears \
on one line"

Non-ASCII content: Aside from escape sequences, the bytes between the quotes are copied into the literal verbatim. The compiler performs no encoding validation, conversion, or normalization, so a literal contains exactly the bytes of the source file. Since source files are conventionally UTF-8, "μZero" produces the UTF-8 encoding of (0xCE 0xBC 0x5A 0x65 0x72 0x6F) -- 2 initial bytes for μ followed by ascii Zero. A source file saved in another encoding would pass its raw bytes through just the same in that encoding rather than UTF-8. The length prefix counts bytes, not characters or codepoints; all text-encoding interpretation is left to the program. A backslash followed by a non-ASCII character passes that character's bytes through unchanged.

NOTE: for an explicit byte-stable version with a fixed encoding regardless of the underlying file encoding, use byte escapes for any non-ASCII content, e.g. "\xce\xbcZero".

Memory layout: String literals are stored in static memory with an 8-byte length prefix. The variable holds a pointer to the first character (after the length). See Memory Layout.

Based String Literals

Based strings use 0b"..." or 0x"..." to write exact bytes directly:

let bits:int = 0b"1010_0001 11"       # a1 c0
let header:int = 0x"de ad
    # comments may separate digits
    be ef"

Based strings are ordinary expressions, not directives or a special declaration form. Only base 2 and base 16 are supported, and the prefixes are exactly lowercase 0b and 0x. Binary bodies accept 0 and 1; hexadecimal bodies accept 0-9, a-f, and A-F.

Whitespace, underscores, and # line comments are separators and contribute no bits. After removing those separators, digits are packed in source order using MSB-first wire order: the first digit supplies the highest bit or nibble of the first byte, then packing continues toward the low bits before moving to the next byte. If the final byte is incomplete, its remaining low bits are automatically zero-filled on the right. Thus 0b"1" is the byte 0x80, 0b"1111_0000 1" is 0xF0 0x80, and 0x"a b c" is 0xAB 0xC0.

Because a # comment continues through the newline, a closing quote on the same line is part of the comment; place the closing quote on a later line.

No target-endian conversion is performed. The bytes in static storage are exactly this wire-order sequence on every backend. A based string carries no element type, dimensions, or shape metadata; consumers are responsible for knowing the data's format.

Ordinary strings and based strings share the same static-storage layout: an 8-byte byte-length immediately before the data, with the expression evaluating to a pointer to the first data byte. __load__(ptr - 8) is therefore the byte length for both forms.

Path Literals

Path Literals use the p"..." syntax. They are only recognized by the import preprocessor and are not part of the regular token stream or parser grammar:

import p"utils.udewy"
import p"../lib/helpers.udewy"

Path literals support the same escape sequences as regular strings.

The void Keyword

void is a special keyword representing the absence of a meaningful value. It is primarily used in:

return void              # return from a void function
let f = ():>void => {}   # declare a void-returning function

1.2 Type System

Runtime Representation

udewy treats everything as 64-bit integers at runtime. Pointers, booleans, characters—all are integers under the hood. There is no runtime type checking.

Type Annotations

Type annotations are syntactically required in variable declarations and function signatures but are not checked by the udewy compiler. They exist to:

  1. Maintain compatibility with full Dewy (which does check types)
  2. Document programmer intent
  3. Guide certain parsing decisions

Variable type annotation (:type):

let x:int = 42

Parameterized types (:type<param> or <param> alone):

let data:array<int> = buffer
let mixed<int|string> = value    # type param without colon

The content inside <> is not validated, and annotations such as array<T> are opaque to udewy. This allows complex type expressions that udewy couldn't otherwise parse:

let x<(int & Something<10>) | undefined> = 10

Function return type (:>type, :>type<param> or :> <param>):

let add = (a:int b:int):>int => { return a + b }
let get_value = ():>result<int> => { ... }
let flexible = ():> <A&B|C<int>> => { ... }  # complex return type

The transmute Keyword

transmute is a bit-preserving type cast that is a no-op in udewy:

let ptr:int = some_address transmute int
let arr:int = buffer transmute array<byte>

transmute preserves the underlying bits while changing the type annotation. It allows udewy code that manipulates raw integers to be valid in the strictly-typed full Dewy.

Syntax: <expr> transmute <type>

Where <type> can be an identifier, an identifier with type parameters, or just type parameters:

expr transmute int
expr transmute array<int>
expr transmute <T|U>

Ignored Type Declarations

udewy also accepts a narrow Dewy-compatibility declaration form:

Point:type = [x:int y:int]
let Point:type = [x:int y:int]
Line:type = [start:Point end:Point]

This form is recognized only when the annotation is the literal type. In udewy, it is treated like an ignored type-level declaration:

  • the entire right-hand side is consumed syntactically and ignored
  • no runtime code is generated
  • no local or global value binding is created

This allows shared udewy/Dewy source to include named type or struct declarations for the full Dewy compiler without affecting udewy execution.

1.3 Expressions

Operator Precedence

udewy parses expressions using the precedence table below. Binary operators are left-associative within a precedence level, and parentheses can be used to override the default grouping.

Operand evaluation remains left-to-right, even when precedence groups the expression differently. For example, a + b * c evaluates a, then b, then c, and groups as a + (b * c).

Precedence levels (highest to lowest):

LevelOperatorsDescription
7*, //, %Multiplicative
6+, -Additive
5<<, >>Shift
4=?, not=?, >?, <?, >=?, <=?Comparison
3andBitwise/logical AND
2xorBitwise/logical XOR
1orBitwise/logical OR

Examples:

# OK: multiplicative operators bind before additive operators
let product_sum:int = a + b * c

# OK: explicit grouping
let grouped:int = (a + b) * c

# OK: left-associative within the same precedence level
let chain:int = a + b + c

Unary Operators

OperatorDescriptionSemantics
-NegationTwo's complement negation: 0 - x
notBitwise/logical NOTInverts all 64 bits

Unary operators bind tightly to their operand:

let x:int = -(a + b)
let y:int = not flags

Arithmetic Operators

OperatorDescriptionSemantics
+Addition64-bit wrapping addition
-Subtraction64-bit wrapping subtraction
*Multiplication64-bit wrapping multiplication
//Integer divisionSigned 64-bit division, truncated toward zero
%ModuloSigned 64-bit remainder

Division and modulo use signed interpretation (RISC-V div / rem semantics on all targets):

Casea // ba % b
b = 0-1 (0xFFFF_FFFF_FFFF_FFFF)a
INT_MIN // -1INT_MIN0

All other cases use truncating signed division toward zero.

Shift Operators

OperatorDescriptionSemantics
<<Left shiftShift left, fill with zeros
>>Right shift (unsigned)Logical shift right, fill with zeros

The shift amount is masked to the low 6 bits (0-63).

Important: The >> operator performs an unsigned (logical) shift, filling vacated bits with zeros regardless of the sign bit. For arithmetic (signed) right shift that preserves the sign bit, use the __signed_shr__ intrinsic. See Semantic Differences

Comparison Operators

All comparison operators return true (0xFFFF_FFFF_FFFF_FFFF) or false (0x0000_0000_0000_0000).

OperatorDescriptionSemantics
=?EqualTrue if operands are bit-identical
not=?Not equalTrue if operands differ in any bit
>?Greater thanSigned comparison
<?Less thanSigned comparison
>=?Greater or equalSigned comparison
<=?Less or equalSigned comparison

Note: Relational comparisons (>?, <?, etc.) interpret operands as signed 64-bit integers.

Bitwise/Logical Operators

OperatorDescriptionSemantics
andBitwise AND64-bit bitwise AND
orBitwise OR64-bit bitwise OR
xorBitwise XOR64-bit bitwise XOR

Due to the boolean representation (true = all 1s, false = all 0s), these operators work correctly as both bitwise and logical operators.

Important: In ordinary expressions, and and or are bitwise and both operands are always evaluated. In if and loop conditions only, and and or use logical short-circuit evaluation (compatible with Dewy bool conditions): the right-hand side is skipped when the result is already determined.

Parentheses and Grouping

Parentheses override precedence and grouping:

let x:int = (a + b) * c

Function Calls

Named call:

result = add(1 2)

Expression call (calling a computed function pointer):

let fn_ptr:int = get_handler()
(@fn_ptr)(arg1 arg2)

Arguments are space-separated (no commas).

NOTE: Indirect calls require parentheses around the callee. Bare fn_ptr(arg1 arg2) is always a named call: it looks up a top-level function fn_ptr and ignores any local or global binding of that name. Use (@fn_ptr)(arg1 arg2) to call through a value. Consequently, binding a local or global with the same name as an existing top-level function is ill-formed — name(...) will still call the function, not the binding.

@name is decorative in udewy: it is the value of name, exactly as the bare name is. It exists so a udewy program reads the same under Dewy, where a bare function name is always a call and @name means the function itself. Write it wherever a function is used as a value — return @double, let handler:int = @on_start — and it is required when a name in parentheses is called: (fn_ptr)(args) is rejected, because under Dewy that would call fn_ptr() first and apply the arguments to its result.

1.4 Statements

Variable Declarations

Variables must be declared with let or const, require a type annotation, and require an initializer:

let x:int = 42
const BUFFER_SIZE:int = 1024
let data:array<int> = __alloca__(BUFFER_SIZE)

let declares a mutable binding. const declares an immutable binding and cannot be assigned after its initializer.

At local scope, the initializer must be a normal expression. extern initializers are not allowed inside function bodies.

Ignored Type Declarations

Anywhere a statement or top-level declaration may appear, udewy accepts type declarations of the form <IDENT>:type = <EXPR>, let <IDENT>:type = <EXPR>, or const <IDENT>:type = <EXPR>, e.g.

Point:type = [x:int y:int]
let Point:type = [x:int y:int]
const Line:type = [start:Point end:Point]

Semantics:

  • The type annotation must exactly be the literal type
  • The declaration is parsed and ignored.
  • The right-hand side is not evaluated.
  • The declared name may still appear inside type annotations or other ignored type declarations.
  • The declared name may not be used as a runtime value; doing so is a compile-time error.

Assignment

Simple assignment:

x = 42

The assignment target must be a let binding. Assigning to a const is a compile-time error.

Compound assignment operators combine a binary operation with assignment:

OperatorEquivalent
x += yx = x + y
x -= yx = x - y
x *= yx = x * y
x //= yx = x // y
x %= yx = x % y
x <<= yx = x << y
x >>= yx = x >> y
x and= yx = x and y
x or= yx = x or y
x xor= yx = x xor y

If / Else

if condition {
    # then branch
} else if other_condition {
    # else-if branch
} else {
    # else branch
}

Braces are always required. Conditions check if any bit is set (non-zero = true, zero = false).

Well-formedness note: Full Dewy requires conditions to be strictly bool typed. Using a non-bool value as a condition (e.g., if some_int { ... }) will fail Dewy type-checking even though it compiles in udewy.

Loop

udewy has a single loop construct that loops while a condition is true:

let i:int = 0
loop i <? 10 {
    # body
    i = i + 1
}

The condition is evaluated before each iteration. Braces are required.

Break and Continue

Within a loop:

  • break exits the innermost loop immediately
  • continue jumps to the next iteration (re-evaluates condition)
loop true {
    if done {
        break
    }
    if skip_this {
        continue
    }
    # ...
}

Return

All functions must explicitly return using return:

return value     # return a value
return void      # return from a void function

The return expression is mandatory. Use return void for functions that don't return a meaningful value.

Prelude Directives

Prelude directives are preprocessing-only forms recognized before tokenization. They may appear only in the leading prelude at the top of a file or inside active preprocessor target blocks in that prelude. They are removed before the normal udewy parser sees the source.

Include Bytes

$include_bytes(p"tables/gcb.bin") as gcb_table

$include_bytes(p"path") as name embeds a file's contents as read-only static data and binds name (a module-level int64) to its address — exactly as let name:int64 = 0x"…" of the same bytes would. It is a prelude directive: it may appear only in the leading prelude alongside import and $supported_targets, the argument must be a single path literal (nothing else is accepted), and a relative path is resolved against the directory of the file containing the directive. The file is read by the preprocessor, so its bytes never appear in the program text; a missing file is a compile-time error. Generated programs use it for large tables (the Dewy compiler's Unicode data).

Import Directives

Import directives bring definitions from other udewy files into scope:

import p"utils.udewy"
import p"lib/helpers.udewy"
import p"../third_party/libfoo.a"

Semantics:

  • Paths are relative to the importing file's directory
  • Imports are recognized only in the leading prelude at the top of a file
  • Imports ending in .udewy are treated as udewy source and processed recursively
  • Imported paths with any other suffix are treated as direct external link artifacts, not source
  • Native artifacts are expected to be fully prepared for the final backend link step; native targets hand them directly to the system linker
  • Each imported source file or artifact path is only included once
  • Imported udewy source is prepended to the source being compiled
  • After preprocessing, import directives are removed from the source before tokenization and parsing
  • import remains a reserved word; if it reaches tokenization, the tokenizer reports an error

The import preprocessor records the resolved paths of imported udewy source files as generic provenance. It does not interpret those paths beyond source loading and duplicate suppression; concrete backends may use that provenance to recognize their own library modules.

Target Support Metadata

Any source file may declare the targets it supports:

$supported_targets = ["c" "wasm32" "x86_64"]

If the selected compile target is not present, preprocessing stops with a diagnostic for that file. Target names are the same names accepted by the compiler's --target flag.

Target-Conditional Prelude Blocks

Target-conditional blocks choose which prelude directives are active:

if $target =? "wasm32" {
    import p"./backend_wasm.udewy"
}

if $target not=? "c" {
    import p"./native_only.udewy"
}

Only the exact $target =? "..." and $target not=? "..." conditions are supported. The block body may contain only prelude directives. Inactive blocks are skipped without resolving their imports or emitting their diagnostics.

Diagnostic Directives

Source files may emit preprocessor diagnostics:

$warning("wasm32 has limited support")
$error("this target needs a custom backend module")

Both forms accept exactly one string literal. $warning prints a source-context warning and continues. $error stops preprocessing with the provided message.

1.5 Functions

Declaration

Functions are declared using lambda syntax assigned to a variable:

let add = (a:int b:int):>int => {
    return a + b
}

let greet = ():>void => {
    # do something
    return void
}
  • All parameters must have type annotations (e.g., a:int)
  • Return type annotation is required using :> syntax (e.g., :>int)
  • Parameters are space-separated (no commas)
  • All functions must explicitly return
  • Functions may only be declared at top level (no nested functions)

External Declarations

udewy supports top-level external declarations for functions and globals provided by linked artifacts:

let SDL_Init = (flags:int):>bool => extern
let SDL_Quit = ():>void => extern
let errno:int = extern

Extern declarations use the same syntax as ordinary top-level declarations, but replace the function body or initializer with the extern keyword.

Rules:

  • Extern declarations are only valid at top level
  • Local extern declarations are a compile-time error
  • Extern functions participate in normal forward-reference resolution by name
  • Extern globals and functions are resolved by the native linker, not by udewy source imports
  • Backends that do not support native external linking may reject extern

Forward References

Functions can be called before they are defined. Unknown identifiers during parsing are treated as forward function references and resolved at the end of compilation. If a forward reference remains undefined, compilation fails. Only functions may be forward-referenced; globals, constants, and ignored type declarations must be declared before use.

Calling Convention

Functions follow platform-specific calling conventions (see backend addendums). Arguments are passed in registers and/or on the stack depending on the backend.

No Closures

udewy does not support closures. Functions cannot capture variables from enclosing scopes. All functions operate only on their parameters, global variables, and locally declared variables.

1.6 Memory Layout

Overview

udewy uses a simple, uniform memory layout. All values are 64-bit integers. Complex data structures are built using pointers and manual offset calculations.

Strings and Based Strings

Ordinary strings and based strings share the same layout in static memory:

┌──────────────────────┬────────────────┐
│ Byte length (8 bytes)│ Data (N bytes) │
└──────────────────────┴────────────────┘
  • Length prefix: 8 bytes containing the byte count
  • Data: exactly the ordinary string's decoded source bytes or the based string's MSB-first wire-order bytes

The expression holds a pointer to the start of the data (after the length). Access the byte length at ptr - 8:

let packet:int = 0x"01 02 ff"
let len:int = __load__(packet - 8)       # 3
let first:int = __load_u8__(packet)      # 1
let second:int = __load_u8__(packet + 1) # 2

Static vs Dynamic Data

Ordinary and based string literals are stored in static memory (the data section). Their storage may be shared by multiple uses and persists across calls, so literals should not be used as mutable working buffers.

let process = ():>void => {
    let local_buf:int = __alloca__(256)        # fresh for this call
    let shared_buf:int = __static_alloca__(64) # one zero-initialized static buffer
    # write through __store_u8__, __store_u64__, etc.
    return void
}

Use __alloca__(size) for a fresh function-local mutable buffer, __static_alloca__(size) for a shared zero-initialized mutable buffer, or an allocator appropriate to the target for other lifetimes.

Simulating Structs

udewy doesn't have built-in structs. Use offset constants:

const PERSON_NAME:int = 0
const PERSON_AGE:int = 8
const PERSON_HEIGHT:int = 16
const PERSON_SIZE:int = 24

let person:int = alloc(PERSON_SIZE)

__store__(name_ptr person + PERSON_NAME)
__store__(25 person + PERSON_AGE)
__store__(180 person + PERSON_HEIGHT)

let age:int = __load__(person + PERSON_AGE)

1.7 Scoping

Block Scope

Variables are block-scoped. A new scope is created for:

  • Function bodies
  • If/else branches
  • Loop bodies

Variables declared in an inner scope shadow variables with the same name in outer scopes.

Name Resolution

When a bare identifier is referenced (not followed by (), it is resolved by searching:

  1. Current block scope
  2. Enclosing block scopes (innermost to outermost)
  3. Function parameters
  4. Global scope (top-level constants, globals, and functions)

Ignored type declarations participate only in type annotations and other ignored type declarations. If a name resolves to an ignored type declaration and is used as a runtime value, compilation fails.

If not found in any runtime scope, it is treated as a forward reference to a function.

Named calls (name(...)) skip this search and always resolve name as a top-level function (or intrinsic). A local or global binding does not intercept them. Binding a name that already names a top-level function is therefore ill-formed.

Global Scope

Top-level declarations are in global scope and visible throughout the file (including before their declaration point due to forward reference handling).

Part 2: Core Intrinsics

Intrinsics are built-in operations that compile to target-specific instructions. They are called like functions but are handled specially by the compiler.

2.1 Memory Operations

These intrinsics provide direct memory access:

IntrinsicDescription
__load__(addr)Shorthand for __load_u64__(addr)
__load_u8__(addr)Load unsigned 8-bit value from addr, zero-extend to 64-bit
__load_u16__(addr)Load unsigned 16-bit value from addr, zero-extend to 64-bit
__load_u32__(addr)Load unsigned 32-bit value from addr, zero-extend to 64-bit
__load_u64__(addr)Load unsigned 64-bit value from addr
__load_i8__(addr)Load signed 8-bit value from addr, sign-extend to 64-bit
__load_i16__(addr)Load signed 16-bit value from addr, sign-extend to 64-bit
__load_i32__(addr)Load signed 32-bit value from addr, sign-extend to 64-bit
__load_i64__(addr)Load signed 64-bit value from addr
__store__(val addr)Shorthand for __store_u64__(val addr)
__store_u8__(val addr)Store low 8 bits of val to addr, return 0
__store_u16__(val addr)Store low 16 bits of val to addr, return 0
__store_u32__(val addr)Store low 32 bits of val to addr, return 0
__store_u64__(val addr)Store 64-bit val to addr, return 0
__store_i8__(val addr)Store low 8 bits of val to addr, return 0
__store_i16__(val addr)Store low 16 bits of val to addr, return 0
__store_i32__(val addr)Store low 32 bits of val to addr, return 0
__store_i64__(val addr)Store 64-bit val to addr, return 0
__alloca__(size)Allocate size bytes of temporary storage and return an 8-byte-aligned address
__static_alloca__(size)Allocate size bytes of writable static storage and return its address
__static_words__(word...)Intern one or more compile-time stable 64-bit words and return their 8-byte-aligned static address

Signed and unsigned 64-bit loads/stores are identical at runtime; both spellings exist to make programmer intent explicit. __load__/__store__ are convenience shorthands for the unsigned 64-bit forms. For stores, signedness affects only intent and documentation; the stored bit pattern is the low N bits of val.

__alloca__(size) reserves temporary storage whose lifetime lasts until the current function returns. The returned address is aligned to at least 8 bytes, and native backends may round the reserved size up further to preserve ABI stack alignment. Native backends typically implement this with function-local stack storage; the wasm backend uses a function-scoped stack region in linear memory.

__static_alloca__(size) reserves writable storage in the program's static data area. The storage is zero-initialized, has a single shared instance for the entire program, and is not tied to any function call frame. Its size argument must be a compile-time stable integer value, which may be provided directly as a number literal or indirectly through a const binding or backend-provided builtin constant.

__static_words__(word...) creates writable static storage initialized with one or more compile-time stable words. Accepted values are integer literals and stable aliases, non-extern function references, ordinary or based string pointers, and pointers returned by __static_alloca__ or another __static_words__ call. The result points directly to the first word: there is no length prefix, element type, bounds information, or array behavior.

const handlers:int = __static_words__(on_start on_update on_stop)
let handler:int = __load__(handlers + state * 8)
(@handler)(context)

Integer entries use the target's native 64-bit byte order. References use the backend's normal runtime representation: addresses on native and C targets, linear-memory addresses for WASM data, and WASM function-table indices for functions. This makes __static_words__ suitable for vtables, lookup tables, and ABI descriptors interpreted by the consumer. Use based strings instead when the bytes must be identical across targets.

2.2 Arithmetic Operations

IntrinsicDescription
__signed_shr__(val bits)Arithmetic (signed) right shift; fills vacated bits with the sign bit
__unsigned_idiv__(lhs rhs)Unsigned 64-bit division (RISC-V divu / remu semantics)
__unsigned_mod__(lhs rhs)Unsigned 64-bit remainder
Case__unsigned_idiv__(a, b)__unsigned_mod__(a, b)
b = 0UINT64_MAX (0xFFFF_FFFF_FFFF_FFFF)a

Divide-by-zero quotients use the same bit pattern as signed -1; only the interpretation differs. | __unsigned_lt__(lhs rhs) | Unsigned less-than comparison | | __unsigned_gt__(lhs rhs) | Unsigned greater-than comparison | | __unsigned_lte__(lhs rhs) | Unsigned less-than-or-equal comparison | | __unsigned_gte__(lhs rhs) | Unsigned greater-than-or-equal comparison |

Use __signed_shr__ when you need sign-preserving right shift. The >> operator always performs unsigned (logical) shift. Likewise, //, %, and relational operators remain signed by default; use the unsigned intrinsics when you need unsigned interpretation.

Part 3: Well-Formedness and Divergence

This section documents all cases where udewy behavior can diverge from full Dewy. Writing well-formed udewy today requires programmer diligence in these areas.

When the full dewy compiler is available, well-formedness will be machine-verifiable.

3.1 Type Mismatches (udewy compiles, Dewy rejects)

These patterns compile in udewy but would be rejected by full Dewy's type checker:

PatternIssue
if some_int { ... }Condition must be bool in Dewy
let x:int = true + 5Arithmetic on booleans
let p:int = arr + 8Pointer arithmetic without casts
some_fn(arg1 arg2) transmute intTransmute on wrong type

3.2 Semantic Differences (both compile, different behavior)

These patterns compile in both udewy and Dewy but may behave differently:

Patternudewy BehaviorDewy Behavior
x >> n (when x is signed)Unsigned shift (zeros fill)Signed shift (sign bit fills)*
a and b / a or bBoth sides always evaluated*Short-circuit evaluation
str1 =? str2Compares pointersCompares content

NOTE: Dewy selects signed or unsigned shift based on left operand type.

NOTE: and / or can short-circuit in udewy when they are the condition in an if or loop expression (which matches regular dewy behavior).

In general, using any of these differences is considered not well-formed.

3.3 Programmer Diligence Required Today

Until automatic well-formedness verification is implemented in the dewy compiler, manual care should be taken to write well-formed udewy:

  1. Use __signed_shr__ when arithmetic shift is needed for signed values
  2. Short-circuit only in conditions - side effects in and/or operands still always occur in ordinary expressions; only if/loop conditions short-circuit
  3. Implement content comparison functions when byte-string content equality is needed
  4. Use unsigned intrinsics explicitly when raw unsigned interpretation matters
  5. Test with increasing compiler strictness as the Dewy compiler matures. The full dewy compiler will be able to flag ALL cases of ill-formed udewy.

Part 4: Compilation Model

4.1 Overview

The udewy compiler is a single-pass compiler with:

  1. Import preprocessing (recursive file inclusion)
  2. Tokenization
  3. Parsing with direct code emission
  4. Backend-specific assembly/output generation

4.2 Prelude Processing

Before tokenization, leading prelude directives are processed:

  1. Parse the leading prelude at the beginning of the file
  2. Check any $supported_targets declaration against the selected compile target
  3. Evaluate narrow if $target ... blocks and skip inactive block bodies
  4. Emit any active $warning or $error diagnostics
  5. For each active import, recursively process the imported file
  6. Prepend imported content to the main source
  7. Remove prelude directives from the source being compiled
  8. Track imported files to prevent duplicate inclusion

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.

4.4 Backend Selection

The hosted and native command-line compilers include source/variable debug metadata by default. --no-debug-info skips that metadata without changing program evaluation, compile diagnostics, or __breakpoint__() instructions. Dewy's ordinary compilation uses this option for generated µDewy; dewy debug keeps the metadata and source mappings.

The target backend is selected at compile time via the --target flag:

python -m udewy.p0 --target <backend> program.udewy

Available backends are documented in the addendums. New backends can be added by implementing the backend protocol defined in backend/common.py.

4.5 Forward References

Unknown identifiers during parsing are assumed to be forward references to functions unless they resolve to an ignored type declaration. At the end of compilation, all remaining forward references must be resolved or an error is reported.

Part 5: Formal Grammar

source_file     ::= prelude_directive* program

prelude_directive ::= import_directive
                    | supported_targets
                    | target_if
                    | meta_diagnostic

import_directive ::= 'import' path_string
path_string     ::= 'p' STRING

supported_targets ::= '$supported_targets' '=' '[' STRING* ']'

target_if       ::= 'if' target_condition '{' prelude_directive* '}'
target_condition ::= '$target' ('=?' | 'not=?') STRING

meta_diagnostic ::= ('$warning' | '$error') '(' STRING ')'

program         ::= top_level_stmt*

top_level_stmt  ::= fn_decl
                  | const_decl
                  | ignored_type_decl

fn_decl         ::= ('let' | 'const') IDENT '=' '(' param_list ')' fn_type_annot '=>' (block | 'extern')

const_decl      ::= ('let' | 'const') IDENT type_annot '=' (const_expr | 'extern')

param_list      ::= (IDENT type_annot)*

fn_type_annot   ::= ':>' IDENT type_param?
                  | ':>' type_param
type_annot      ::= ':' IDENT type_param?
                  | type_param
type_param      ::= '<' type_content '>'

block           ::= '{' statement* '}'

statement       ::= var_decl
                  | ignored_type_decl
                  | assign_stmt
                  | if_stmt
                  | loop_stmt
                  | 'break'
                  | 'continue'
                  | return_stmt
                  | expr

var_decl        ::= ('let' | 'const') IDENT type_annot '=' expr

ignored_type_decl ::= IDENT ':type' '=' ignored_expr
                    | ('let' | 'const') IDENT ':type' '=' ignored_expr

assign_stmt     ::= IDENT '=' expr
                  | IDENT compound_op expr

compound_op     ::= '+=' | '-=' | '*=' | '//=' | '%=' 
                  | '<<=' | '>>=' | 'and=' | 'or=' | 'xor='

if_stmt         ::= 'if' expr block else_clause?
else_clause     ::= 'else' 'if' expr block else_clause?
                  | 'else' block

loop_stmt       ::= 'loop' expr block

return_stmt     ::= 'return' expr

expr            ::= prefix_expr (binop prefix_expr)* cast_annot?

prefix_expr     ::= '-' prefix_expr
                  | 'not' prefix_expr
                  | atom

atom            ::= NUMBER
                  | STRING
                  | BASED_STRING
                  | 'true'
                  | 'false'
                  | 'void'
                  | IDENT
                  | IDENT '(' arg_list ')'
                  | '(' expr ')' ('(' arg_list ')')?

arg_list        ::= expr*

cast_annot      ::= 'transmute' (IDENT type_param? | type_param)

ignored_expr    ::= a Dewy-compatible type expression or struct/type literal
                  # udewy consumes this syntax but does not evaluate it

binop           ::= '+' | '-' | '*' | '//' | '%'
                  | '<<' | '>>'
                  | '=?' | 'not=?' | '>?' | '<?' | '>=?' | '<=?'
                  | 'and' | 'or' | 'xor'

const_expr      ::= NUMBER | STRING | BASED_STRING | IDENT

# Lexical elements
IDENT           ::= [a-zA-Z_][a-zA-Z0-9_]*
NUMBER          ::= decimal | hex | binary
decimal         ::= [0-9][0-9_]*
hex             ::= '0x' [0-9a-fA-F_]+
binary          ::= '0b' [01_]+
STRING          ::= '"' string_char* '"'
string_char     ::= <any char except '"' or '\'>
                  | '\' <any char>
BASED_STRING    ::= ('0b' binary_string_body | '0x' hex_string_body)
binary_string_body ::= '"' ([01_] | whitespace | line_comment)* '"'
hex_string_body ::= '"' ([0-9a-fA-F_] | whitespace | line_comment)* '"'
line_comment    ::= '#' <characters through end of line>

NOTE: prelude_directive forms are consumed during preprocessing and do not appear in the token stream seen by the parser. The word import remains reserved, so any surviving import is rejected during tokenization.

Part 6: Intentional Limitations

udewy deliberately omits features to keep the compiler simple and auditable:

  • No indexing syntax (arr[i]): Would require type information to know element size
  • No runtime array literal syntax: square brackets remain available in prelude metadata, ignored type declarations, and bracketed type annotations
  • No value casts (as): Would require type-aware conversion
  • No string interpolation: Strings are simple byte sequences
  • No closures or nested functions: Functions only at top level
  • No function overloading: Each function name has exactly one definition
  • Expression and/or are bitwise: Both sides are always evaluated; only if/loop conditions short-circuit
  • No floating-point: Everything is 64-bit integers (with the caveat that float-bit helpers exist for extern ABI interop)
  • No garbage collection: Manual memory management via syscalls

Part 7: Examples

Fibonacci

let fib = (n:int):>int => {
    if n <? 2 {
        return n
    } else {
        return fib(n - 1) + fib(n - 2)
    }
}

let main = ():>int => {
    return fib(10)  # returns 55
}

Memory Allocation with mmap

# SYS_MMAP, PROT_*, MAP_* constants are provided by the x86_64 backend

let alloc = (size:int):>int => {
    return __syscall6__(SYS_MMAP 0 size (PROT_READ or PROT_WRITE) (MAP_PRIVATE or MAP_ANONYMOUS) (0 - 1) 0)
}

let main = ():>int => {
    let buffer:int = alloc(4096)
    __store__(42 buffer)
    return __load__(buffer)  # returns 42
}

Backend Addendums

The following addendums document the currently implemented backends. Each backend targets a specific combination of:

  • Architecture - The CPU instruction set
  • Operating System - The system call interface and runtime environment

New backends can be added for other architecture/OS combinations by implementing the backend protocol. For example, future backends might include:

  • x86_64 Windows (PE executables, Win32 API)
  • x86_64 macOS (Mach-O executables, Darwin syscalls)
  • AArch64 macOS (Apple Silicon)
  • Bare metal / embedded targets
  • Other browser runtimes (Node.js, Deno)

Each addendum specifies the platform intrinsics and conventions for that backend. Programs targeting multiple platforms should use a platform abstraction layer as described in Section 4.3.

Addendum A: x86_64 Linux Backend

A.1 Target Description

  • Architecture: x86_64 (AMD64)
  • Operating System: Linux
  • Output Format: ELF executable via GNU assembler
  • Calling Convention: System V AMD64 ABI

A.2 Calling Convention

PurposeRegisters
Arguments (1-6)rdi, rsi, rdx, rcx, r8, r9
Return valuerax
Caller-savedrax, rcx, rdx, rsi, rdi, r8-r11
Callee-savedrbx, rbp, r12-r15

Additional arguments beyond 6 are passed on the stack.

A.2.1 Codegen Notes

The x86_64 backend still follows udewy's logical value-stack model, but it does not map every save_value() directly to a machine push.

  • The current visible expression result stays in rax.
  • A small prefix of the logical saved-value stack is cached in callee-saved registers before falling back to spill slots on the real stack.
  • When a call has more than 6 arguments, the extra arguments are written into an outbound stack-argument area and the first 6 are placed in rdi, rsi, rdx, rcx, r8, and r9.
  • Call lowering also keeps the machine stack aligned to the ABI-required 16-byte boundary.

A.2.2 __alloca__ Alignment

  • __alloca__ returns an address aligned to 8 bytes.
  • Successive small allocations therefore advance in 8-byte units on this backend.
  • The backend still pads the machine stack as needed at call boundaries to satisfy the System V ABI.

A.2.3 Mixed GP / FP Extern Intrinsics

The x86_64 backend supports mixed integer/pointer and floating-point extern calls through the intrinsic family:

__call_extern_mixed_1__(fn type0 value0)
__call_extern_mixed_2__(fn type0 value0 type1 value1)
...
__call_extern_mixed_8__(fn type0 value0 ... type7 value7)

Rules:

  • fn is an extern function reference
  • each typeN must be a compile-time integer literal
  • 0 means pass valueN through the normal integer/pointer calling convention
  • 1 means treat the low 32 bits of valueN as raw f32 bits and pass them in the next XMM argument register
  • 2 means treat all 64 bits of valueN as raw f64 bits and pass them in the next XMM argument register

This backend also provides:

__i64_to_f32_bits__(value)
__i64_to_f64_bits__(value)
__f32_bits_to_i64__(value)
__f64_bits_to_i64__(value)

These convert a signed integer value into IEEE-754 f32 / f64 bit patterns, returned as ordinary udewy integers. __f32_bits_to_i64__ and __f64_bits_to_i64__ perform the reverse direction for values containing f32 / f64 bit patterns.

A.3 Syscall Intrinsics

__syscall0__(num)
__syscall1__(num arg1)
__syscall2__(num arg1 arg2)
__syscall3__(num arg1 arg2 arg3)
__syscall4__(num arg1 arg2 arg3 arg4)
__syscall5__(num arg1 arg2 arg3 arg4 arg5)
__syscall6__(num arg1 arg2 arg3 arg4 arg5 arg6)

Syscall convention:

  • Syscall number in rax
  • Arguments in rdi, rsi, rdx, r10, r8, r9
  • Return value in rax

A.4 Builtin Constants

The x86_64 backend provides the following constants automatically (no declaration needed):

Syscall Numbers:

ConstantValueDescription
SYS_READ0Read from file descriptor
SYS_WRITE1Write to file descriptor
SYS_OPEN2Open file
SYS_CLOSE3Close file descriptor
SYS_STAT4Get file status
SYS_FSTAT5Get file status by fd
SYS_LSEEK8Reposition file offset
SYS_MMAP9Map memory
SYS_MUNMAP11Unmap memory
SYS_BRK12Change data segment size
SYS_IOCTL16Device control
SYS_PIPE22Create pipe
SYS_DUP32Duplicate fd
SYS_DUP233Duplicate fd to specific number
SYS_GETPID39Get process ID
SYS_FORK57Create child process
SYS_EXECVE59Execute program
SYS_EXIT60Exit process
SYS_WAIT461Wait for process
SYS_KILL62Send signal
SYS_GETCWD79Get current directory
SYS_CHDIR80Change directory
SYS_MKDIR83Create directory
SYS_RMDIR84Remove directory
SYS_CREAT85Create file
SYS_UNLINK87Delete file
SYS_GETUID102Get user ID
SYS_GETGID104Get group ID
SYS_GETEUID107Get effective user ID
SYS_GETEGID108Get effective group ID
SYS_CLOCK_GETTIME228Get time
SYS_EXIT_GROUP231Exit all threads

File Descriptors:

ConstantValue
STDIN0
STDOUT1
STDERR2

Open Flags:

ConstantValue
O_RDONLY0
O_WRONLY1
O_RDWR2
O_CREAT64
O_TRUNC512
O_APPEND1024

Memory Mapping:

ConstantValue
PROT_NONE0
PROT_READ1
PROT_WRITE2
PROT_EXEC4
MAP_SHARED1
MAP_PRIVATE2
MAP_FIXED16
MAP_ANONYMOUS32

Addendum B: RISC-V 64 Linux Backend

B.1 Target Description

  • Architecture: RISC-V 64-bit (RV64)
  • Operating System: Linux
  • Output Format: ELF executable
  • Calling Convention: RISC-V LP64 ABI

B.2 Calling Convention

PurposeRegisters
Arguments (1-8)a0-a7
Return valuea0
Callee-saveds0-s11, ra
Stack pointersp (16-byte aligned)

Additional arguments beyond 8 are passed on the stack.

B.2.1 Codegen Notes

The RISC-V backend uses the same basic strategy as x86_64: it preserves udewy's logical value-stack behavior, but keeps the shallow part of that stack in registers.

  • The current visible expression result stays in a0.
  • A small prefix of saved values is cached in callee-saved registers before deeper values spill to the real stack.
  • Calls place the first 8 arguments in a0-a7 and marshal any remaining arguments into an outbound stack area.
  • Call lowering maintains the required 16-byte stack alignment at the actual call instruction.

B.2.2 __alloca__ Alignment

  • __alloca__ returns an address aligned to 16 bytes.
  • Successive small allocations therefore advance in 16-byte units on this backend.
  • This stronger alignment matches the backend's stack-alignment requirements.

B.2.3 Mixed GP / FP Extern Intrinsics

The RISC-V backend supports the same mixed integer/pointer and floating-point extern call intrinsic family as the other native Linux backends:

__call_extern_mixed_1__(fn type0 value0)
__call_extern_mixed_2__(fn type0 value0 type1 value1)
...
__call_extern_mixed_8__(fn type0 value0 ... type7 value7)

Rules:

  • fn is an extern function reference
  • each typeN must be a compile-time integer literal
  • 0 means pass valueN through the normal integer/pointer calling convention in a0-a7
  • 1 means treat the low 32 bits of valueN as raw f32 bits and pass them in the next floating-point argument register (fa0-fa7)
  • 2 means treat all 64 bits of valueN as raw f64 bits and pass them in the next floating-point argument register (fa0-fa7)

This backend also provides:

__i64_to_f32_bits__(value)
__i64_to_f64_bits__(value)
__f32_bits_to_i64__(value)
__f64_bits_to_i64__(value)

Integer-only udewy programs keep the backend's ordinary minimal RISC-V target assumptions. Using these FP conversion or mixed GP / FP extern intrinsics makes the generated artifact require a hard-float-capable RISC-V ABI/toolchain (LP64D-compatible). This is a whole-artifact requirement, not a per-call ABI switch.

B.3 Syscall Intrinsics

Same syntax as x86_64:

__syscall0__(num)
__syscall1__(num arg1)
# ... etc.

Syscall convention:

  • Syscall number in a7
  • Arguments in a0-a5
  • Return value in a0

B.4 Builtin Constants

The RISC-V backend provides constants automatically. RISC-V Linux uses the unified "new-style" syscall table.

Syscall Numbers:

ConstantValueDescription
SYS_GETCWD17Get current directory
SYS_DUP23Duplicate fd
SYS_DUP324Duplicate fd with flags
SYS_IOCTL29Device control
SYS_MKDIRAT34Create directory (relative)
SYS_UNLINKAT35Delete file (relative)
SYS_FTRUNCATE46Truncate file
SYS_FACCESSAT48Check file access
SYS_CHDIR49Change directory
SYS_OPENAT56Open file (relative)
SYS_CLOSE57Close fd
SYS_PIPE259Create pipe
SYS_LSEEK62Seek in file
SYS_READ63Read from fd
SYS_WRITE64Write to fd
SYS_FSTAT80Get file status
SYS_EXIT93Exit process
SYS_EXIT_GROUP94Exit all threads
SYS_KILL129Send signal
SYS_GETPID172Get process ID
SYS_GETUID174Get user ID
SYS_GETEUID175Get effective user ID
SYS_GETGID176Get group ID
SYS_GETEGID177Get effective group ID
SYS_BRK214Change data segment size
SYS_MUNMAP215Unmap memory
SYS_CLONE220Create process
SYS_EXECVE221Execute program
SYS_MMAP222Map memory
SYS_WAIT4260Wait for process

Note: RISC-V uses *at syscalls (e.g., SYS_OPENAT instead of SYS_OPEN). Use AT_FDCWD (-100) as the directory fd for current directory.

File descriptor, open flag, and mmap constants are the same as x86_64 (see Addendum A).

Addendum C: AArch64 Linux Backend

C.1 Target Description

  • Architecture: AArch64 (ARM 64-bit)
  • Operating System: Linux
  • Output Format: ELF executable
  • Calling Convention: AAPCS64

C.2 Calling Convention

PurposeRegisters
Arguments (1-8)x0-x7
Return valuex0
Callee-savedx19-x28, sp, fp
Link registerlr (x30)

Additional arguments beyond 8 are passed on the stack.

C.2.1 Codegen Notes

The AArch64 backend also keeps the parser-visible stack model, but uses registers for the shallow saved-value stack instead of immediately spilling everything.

  • The current visible expression result stays in x0.
  • A small prefix of saved values is cached in callee-saved registers, with deeper values spilling to stack slots.
  • Calls place the first 8 arguments in x0-x7 and place overflow arguments in the outbound call stack area.
  • Because AArch64 already requires 16-byte stack alignment and the backend spills in 16-byte slots, this path stays naturally aligned.

C.2.2 __alloca__ Alignment

  • __alloca__ returns an address aligned to 16 bytes.
  • Successive small allocations therefore advance in 16-byte units on this backend.
  • This stronger alignment matches the backend's stack-alignment requirements.

C.2.3 Mixed GP / FP Extern Intrinsics

The AArch64 backend supports the same mixed extern intrinsic family as x86_64:

__call_extern_mixed_1__(fn type0 value0)
__call_extern_mixed_2__(fn type0 value0 type1 value1)
...
__call_extern_mixed_8__(fn type0 value0 ... type7 value7)

Rules:

  • 0 passes the value through the general-purpose argument registers x0-x7
  • 1 treats the low 32 bits as raw f32 bits and passes them in the next floating-point argument register
  • 2 treats all 64 bits as raw f64 bits and passes them in the next floating-point argument register

This backend also provides:

__i64_to_f32_bits__(value)
__i64_to_f64_bits__(value)
__f32_bits_to_i64__(value)
__f64_bits_to_i64__(value)

These convert signed integers to f32 / f64 bit patterns while keeping udewy's runtime representation as integers. __f32_bits_to_i64__ and __f64_bits_to_i64__ convert float bit patterns back to signed integers.

C.3 Syscall Intrinsics

Same syntax as x86_64. Invoked via svc #0.

Syscall convention:

  • Syscall number in x8
  • Arguments in x0-x5
  • Return value in x0

C.4 Builtin Constants

AArch64 Linux uses the same unified syscall table as RISC-V Linux. All builtin constants (syscall numbers, file descriptors, flags) are identical to RISC-V (see Addendum B).

Addendum D: WASM32 Browser Backend

D.1 Target Description

  • Architecture: WebAssembly 32-bit
  • Environment: Web browser
  • Output Format: WAT (WebAssembly Text) converted to WASM, embedded in HTML
  • Memory Model: Linear memory with imported JavaScript memory object

D.2 Value Representation

  • All udewy values are i64 in WASM
  • Memory addresses are i64 but truncated to i32 at every memory operation
  • Ordinary strings and based strings use the same byte-length-prefixed static layout as native backends

D.2.1 __alloca__ Alignment

  • __alloca__ returns an address aligned to 8 bytes.
  • Successive small allocations therefore advance in 8-byte units on this backend.
  • Because wasm uses a linear-memory bump pointer rather than a native ABI stack, it does not need the stronger 16-byte rule used by some native backends.

D.3 Host Function Intrinsics

Instead of syscalls, the WASM backend provides browser-focused host functions:

IntrinsicArgsDescription
__host_log__(ptr len)2Output text to browser console
__host_exit__(code)1Signal program exit
__host_time__()0Current timestamp in milliseconds
__host_random__()0Random 64-bit integer

These are subject to change

D.4 DOM Intrinsics

IntrinsicArgsDescription
__dom_set_text__(ptr len)2Set output element text content
__dom_append__(ptr len)2Append text to output element
__dom_clear__()0Clear output element
__dom_append_int__(value)1Append integer as text
__log_int__(value)1Log integer to console

D.5 Canvas Graphics Intrinsics

The WASM backend provides intrinsics for canvas-based graphics with animation support:

IntrinsicArgsDescription
__canvas_init__(width height)2Initialize canvas and return RGBA pixel buffer pointer
__canvas_width__()0Get current canvas width
__canvas_height__()0Get current canvas height
__canvas_present__()0Copy pixel buffer to canvas (display frame)
__canvas_set_aspect_lock__(enabled)1Enable or disable aspect-ratio locking using a udewy bool
__frame_count__()0Get current animation frame number
__frame_time__()0Get milliseconds since canvas initialization
__window_width__()0Get browser window inner width
__window_height__()0Get browser window inner height

Usage:

  1. Call __canvas_init__(width height) to create a canvas and get a pointer to the pixel buffer
  2. Optionally call __canvas_set_aspect_lock__(true) to keep the displayed canvas centered at its current aspect ratio as the browser window resizes
  3. Call __canvas_set_aspect_lock__(false) later if you want to return to unrestricted fullscreen scaling
  4. Write RGBA pixels (4 bytes per pixel) to the buffer: [R, G, B, A, R, G, B, A, ...]
  5. Call __canvas_present__() to display the frame
  6. The runtime automatically calls main() each animation frame when canvas mode is active

__canvas_set_aspect_lock__(enabled) expects a udewy boolean value, normally passed as the true or false literals. Internally, any non-zero value enables the lock and 0 disables it.

When aspect lock is enabled, the runtime uses the canvas's current backing dimensions, typically the width and height passed to __canvas_init__(), as the aspect ratio to preserve.

Example:

let buffer:int = 0
let width:int = 320
let height:int = 240

let set_pixel = (x:int y:int r:int g:int b:int):>int => {
    let offset:int = ((y * width) + x) * 4
    let addr:int = buffer + offset
    __store_u8__(r addr)
    __store_u8__(g addr + 1)
    __store_u8__(b addr + 2)
    __store_u8__(255 addr + 3)
    return 0
}

let main = ():>int => {
    buffer = __canvas_init__(width height)
    let t:int = __frame_time__()
    
    # Draw something based on time...
    
    __canvas_present__()
    return 0
}

D.6 Pointer Input Intrinsics

The WASM backend exposes basic pointer state for browser-interactive programs:

IntrinsicArgsDescription
__pointer_x__()0Get the current pointer x coordinate in canvas pixels
__pointer_y__()0Get the current pointer y coordinate in canvas pixels
__pointer_down__()0Get whether the primary pointer button is currently down

When a canvas or WebGL surface is active, coordinates are reported relative to that surface and scaled to its backing pixel resolution.

D.7 Keyboard Input Intrinsics

The WASM backend also exposes keyboard state using browser KeyboardEvent.code strings such as ArrowLeft, ArrowRight, KeyW, and Space:

IntrinsicArgsDescription
__key_down__(code_ptr code_len)2Get whether a key is currently held down
__key_pressed__(code_ptr code_len)2Get whether a key transitioned from up to down since the last animation frame
__key_released__(code_ptr code_len)2Get whether a key transitioned from down to up since the last animation frame

These intrinsics are intended for animated WASM programs running under canvas or WebGL, where main() is called once per frame.

D.8 WebGL Shader Intrinsics

The WASM backend also provides a minimal WebGL path for fullscreen fragment shader demos driven by udewy strings and integer uniforms:

IntrinsicArgsDescription
__webgl_init__(shader_ptr shader_len width height)4Compile a fragment shader string and initialize a fullscreen WebGL canvas
__webgl_uniform1i__(name_ptr name_len value)3Set an int uniform on the active shader program
__webgl_uniform2i__(name_ptr name_len x y)4Set an ivec2 uniform on the active shader program
__webgl_uniform1iv__(name_ptr name_len values_ptr count)4Set an int[count] uniform array from udewy memory
__webgl_uniform2iv__(name_ptr name_len values_ptr count)4Set an ivec2[count] uniform array from udewy memory
__webgl_render__()0Draw the active fullscreen shader

Usage:

  1. Store your fragment shader source as a normal udewy string
  2. Use __load__(shader - 8) to recover its byte length
  3. Call __webgl_init__(shader shader_len width height) once
  4. Update uniforms each frame with __webgl_uniform1i__, __webgl_uniform2i__, __webgl_uniform1iv__, or __webgl_uniform2iv__
  5. Call __webgl_render__() to draw

The runtime provides a built-in passthrough vertex shader with an a_position attribute, so user programs only need to supply fragment shader code.

D.9 Build Options

# Default: single HTML file with embedded base64 WASM
python -m udewy.p0 -c --target wasm32 program.udewy

# Run the embedded HTML directly in your browser
python -m udewy.p0 --target wasm32 program.udewy

# Serve over HTTP instead of opening file:// directly
python -m udewy.p0 --target wasm32 --serve-wasm program.udewy

# Split mode: separate .wasm file (served automatically when run)
python -m udewy.p0 -c --target wasm32 --split-wasm program.udewy
python -m udewy.p0 --target wasm32 --split-wasm program.udewy

When served with --serve-wasm or --split-wasm, the local server exits automatically after the browser tab closes.

Addendum E: External Libraries

This addendum documents the external native libraries currently supported by the repository's checked-in helper code and setup scripts.

E.1 SDL

The SDL integration lives under udewy/third_party/sdl/.

Current backend support:

  • Supported today: x86_64 Linux
  • Not currently supported by this SDL setup: riscv, arm, wasm32

The current SDL bundle is built locally by udewy/third_party/sdl/setup_sdl.py for the host Linux machine and stages host-native artifacts into udewy/third_party/sdl/artifacts/. Because those artifacts are native link inputs for the current machine, they are only wired up for the x86_64 Linux backend in the current repository workflow.

E.1.1 Using SDL

Get started like this:

# Build the local SDL bundle and generate the default udewy icon module
python udewy/third_party/sdl/setup_sdl.py

# Generate a custom icon module next to your source image
# This writes my_icon.udewy and exports the default symbol MY_ICON
python udewy/third_party/sdl/generate_udewy_icon.py my_icon.png

Import the SDL wrapper from your udewy program:

import p"../third_party/sdl/sdl.udewy"

The wrapper provides the low-level SDL extern declarations together with a few convenience helpers written in udewy, including SDL_SetWindowIconFromUdewyData(window icon_data) and SDL_SetDefaultWindowIcon(window).

E.1.2 Generated Icon Modules

The SDL helper can load packed icon data from a generated .udewy module without adding any new language semantics.

By default, generate_udewy_icon.py writes the generated module using the input filename with a .udewy extension and exports a symbol matching that filename stem in uppercase. For example:

python udewy/third_party/sdl/generate_udewy_icon.py my_icon.png
# writes my_icon.udewy
# exports MY_ICON

Use --symbol if you want to override the exported symbol name, or pass an explicit output path if you want the generated .udewy file somewhere else.

The generated module exports one int-annotated pointer whose value is a based string. The consumer knows the record shape; udewy does not attach runtime shape or element metadata. Its byte layout is:

  • bytes 0-7: icon magic word
  • bytes 8-15: width word
  • bytes 16-23: height word
  • bytes 24+: raw RGBA pixels in row-major order, four bytes per pixel

The generator inserts whitespace and line breaks for readability; those separators do not contribute bytes to the based string.

Use a generated icon module from SDL code like this:

import p"../third_party/sdl/sdl.udewy"
import p"./my_icon.udewy"

let main = ():>int => {
    let title:int = "icon demo\0"
    let window:int = SDL_CreateWindow(title 640 480 SDL_WINDOW_RESIZABLE)
    SDL_SetWindowIconFromUdewyData(window MY_ICON)
    # ...
}

sdl.udewy also imports a generated default icon module and exposes it as SDL_DEFAULT_WINDOW_ICON_DATA. Programs that want the bundled logo can call SDL_SetWindowIconFromUdewyData(window SDL_DEFAULT_WINDOW_ICON_DATA) or SDL_SetDefaultWindowIcon(window).

On Linux desktops, successful SDL_SetWindowIcon calls do not guarantee that the dock or launcher will show the new icon. This repository's SDL setup is Wayland-first, and compositor support still determines whether runtime icon changes appear in task switchers or docks.

For the python -m udewy path/to/program.udewy workflow on GNOME/Wayland, udewy also prepares a desktop-entry fallback:

  • the desktop file is written directly to ~/.local/share/applications/<app_id>.desktop
  • the basename before .desktop must match the SDL app ID
  • the Icon= entry points at an absolute PNG path
  • udewy sets SDL_APP_ID before launching the compiled binary so GNOME can match the running window to that desktop entry

E.2 Clay

The Clay integration lives under udewy/third_party/clay/.

Clay is a single-header C layout library. The udewy integration keeps clay.h behind a small C ABI adapter (udewy_clay.c) because Clay's public API uses C structs, macros, callbacks, and floating-point values that do not map cleanly to udewy's raw integer/pointer extern boundary.

Current backend support:

  • Supported today: x86_64, c, wasm32
  • Desktop drawing is provided by the existing SDL wrapper in the demo
  • Browser drawing currently uses udewy's canvas host functions

Build the local Clay artifacts like this:

python udewy/third_party/clay/setup_clay.py

The setup script downloads clay.h when needed, builds a native shim object, and writes target-specific link bundles under udewy/third_party/clay/artifacts/.

For wasm32, the setup script also compiles the Clay shim to a side .wasm module. That requires a clang WebAssembly linker such as wasm-ld; the browser demo uses that side module for Clay layout.

Import Clay from a udewy program:

import p"../third_party/clay/clay.udewy"

The wrapper exposes a small scalar shim surface: initialize Clay, set pointer state for hover/click hit testing, begin/end a layout, open/close simple colored boxes and text elements, and iterate rectangle/text render commands. Clay measures text through a callback and emits text render commands, but it does not rasterize fonts itself. The demo uses a tiny built-in bitmap font renderer so it stays dependency-free across SDL and browser canvas targets.

# Desktop SDL demo
python -m udewy --target x86_64 udewy/tests/clay/demo_clay.udewy

# Hosted C backend
python -m udewy --target c udewy/tests/clay/demo_clay.udewy

# Browser demo; serving is recommended when wasm side artifacts are involved
python -m udewy --target wasm32 --serve-wasm udewy/tests/clay/demo_clay.udewy

The browser route is an app-style canvas renderer, not an HTML/DOM renderer. It is useful for games, tools, and custom UI surfaces. A future Clay-to-DOM renderer would be the better direction for web pages that should preserve native browser behavior such as accessibility, text selection, forms, and find-in-page.

Addendum F: C Backend

F.1 Target Description

  • Architecture: C99 implementation with 64-bit uintptr_t, object pointers, function pointers, and uint64_t
  • Environment: Any system with a C compiler that can build the generated program
  • Output Format: C source compiled to the host platform's native executable format
  • Memory Model: Native target process memory, using udewy's byte-length-prefixed ordinary-string and based-string layout

This backend is intended as a portable code-generation target, not as a promise that every udewy program is portable. Programs remain portable only to the extent that their extern bindings, imported C capabilities, and imported native artifacts are portable.

F.2 Value Representation

  • All runtime values are emitted as udewy_word, a generated C typedef for uint64_t
  • Signed operations explicitly cast to int64_t where udewy semantics require signed interpretation
  • udewy booleans still use true = 0xFFFF_FFFF_FFFF_FFFF and false = 0
  • Ordinary strings and based strings keep the normal udewy layout: one 8-byte byte-length word immediately before the data pointer

The generated C helper layer uses direct unsigned char * access for u8 loads/stores and fixed-size memcpy helpers for wider loads/stores. The latter permit unaligned access without C aliasing assumptions and use the target's native byte order automatically. This matches the native-backend model: raw memory is target memory, not a fixed little-endian serialization format. Constant-size copies let C compilers recover ordinary word loads/stores without first optimizing byte-packing expressions.

F.2.1 Function Body Lowering

The C backend lowers udewy's parser-driven value-stack events into ordinary C locals and statements. Generated function bodies use readable names such as arg0, local0, and t0 rather than a runtime _ud_v / _ud_saved stack. Values are still materialized into temporaries at sequencing boundaries, especially function-call arguments, so udewy's left-to-right evaluation order does not depend on C's argument evaluation order.

Internal udewy functions use readable mangled C names such as udewy_fn_print_bytes_2, while real extern symbols preserve their linked C names. Generated helper and wrapper symbols keep the udewy_ prefix.

Because generated function-local names are intentionally concise, imported C headers that define macros named t0, local0, arg0, and so on could conflict during preprocessing. Avoid importing headers with macros that use those exact names, or add a backend-specific wrapper if such a header must be used.

F.2.2 Static Address Initializers

Address-bearing __static_words__ entries and stable globals are emitted as C99 pointer or function-pointer initializers in an 8-byte udewy_slot union. This lets the C linker initialize function, string, and static-storage references directly; generated programs do not need a startup function to patch address values into otherwise static data. Integer-only globals remain ordinary udewy_word objects.

This lowering relies on the target ABI representing object and function pointers in one 64-bit word, with pointer object bytes matching the corresponding uintptr_t value. Generated C contains compile-time size checks. A standalone probe checks the remaining representation and indirect-call assumptions for a particular compiler, target ABI, and optimization mode:

cc -std=c99 -O2 udewy/backend/c_abi_probe.c -o /tmp/udewy-c-abi-probe
/tmp/udewy-c-abi-probe

Passing the probe certifies that compiler/target/options combination, not every compiler for the same architecture. Cross-compiled probes must be executed on the target hardware or under an emulator.

F.2.3 __alloca__ Alignment

  • __alloca__ rounds requested sizes up to 8 bytes
  • dynamic __alloca__ lowers to the host compiler's alloca facility (or builtin equivalent)
  • if the selected C compiler does not expose an alloca facility, compilation fails by default

__static_alloca__ remains shared writable static storage and does not depend on alloca.

F.3 Platform Surface

With no C capability imports, the C backend implements only the core intrinsics. It does not provide:

  • Linux __syscall0__ through __syscall6__
  • browser/DOM/canvas intrinsics
  • backend-provided builtin constants such as SYS_WRITE or STDOUT

Use importable C capability modules for common hosted C APIs:

import p"../third_party/c/stdlib.udewy"

let main = ():>int => {
    let buf:int = calloc(4 8)
    return buf =? 0
}

The checked-in C capability modules live under udewy/third_party/c/. The C backend recognizes imports of these ordinary udewy source modules and maps them to its private capability names:

  • hosted.udewy: marks that the program targets ordinary hosted C
  • stdio.udewy: imports hosted.udewy, declares minimal stdio externs, and lets the C backend include <stdio.h>
  • stdlib.udewy: imports hosted.udewy, declares minimal stdlib externs, and lets the C backend include <stdlib.h>
  • math.udewy: imports hosted.udewy, lets the C backend include <math.h>, and links the host math library

This keeps the portability boundary explicit in source. Importing no C capability modules leaves the program in the minimal/freestanding-oriented profile.

F.4 Extern Calls

Extern calls on the C backend follow udewy's usual 64-bit integer/pointer model:

  • arguments are passed as raw 64-bit values
  • return values are observed as raw 64-bit values
  • pointers are passed by their bit pattern

This is a good match for ordinary integer/pointer C APIs such as allocators, memory functions, or platform handles. It is not intended to automatically model richer C type information, variadic formatting conventions, or mixed integer/floating-point ABI details.

Indirect function calls also pass through the same integer/pointer representation. As with the native assembly backends, this assumes a conventional hosted platform ABI where function pointers can be used in this manner.

For known externs from udewy/third_party/c/, the C backend can emit small typed C wrappers. These avoid conflicting libc declarations and keep user code on the normal udewy integer/pointer calling convention.

F.4.1 Mixed Integer/Floating-Point Extern Calls

The C backend supports the same low-level __call_extern_mixed_N__ intrinsic family used by the native backends. The static tag arguments are:

  • 0: pass the value as udewy_word
  • 1: reinterpret the value's low 32 bits as float
  • 2: reinterpret the value as double

This is the correct path for C APIs that take floating-point arguments, such as OpenGL functions. The SDL/OpenGL wrapper in udewy/third_party/sdl/sdl.udewy uses this pattern so that the same wrapper can target native Linux backends and the C backend.

The C backend also supports __i64_to_f32_bits__, __i64_to_f64_bits__, __f32_bits_to_i64__, and __f64_bits_to_i64__, which convert signed integer values into IEEE-754 bit patterns for use with mixed extern calls and convert returned/stored float bit patterns back to integers.

F.5 Generated Helper Emission

The C backend emits runtime helpers on demand. For example, a program that does not use __alloca__ will not emit the alloca prelude, and a program that only uses __load_u8__ will not emit the target-endian wide-load helpers. This keeps the default generated C close to the minimal profile.

Misc

Pronunciation

The name can be pronounced several ways depending on how you read the Greek letter μ (mu):

ReadingPronunciationIPA
μ as "micro"MY-kroh dew-ee/ˌmaɪkroʊ ˈduːi/
μ as "mu"MYOO dew-ee/mjuː ˈduːi/
μ as "u"YOO dew-ee/juː ˈduːi/

All are equally correct.

Files

  • p0.py - Parser
  • t0.py - Tokenizer
  • backend/ - Target-specific code generators
    • x86_64.py - x86_64 Linux
    • riscv.py - RISC-V 64 Linux
    • arm.py - AArch64 Linux
    • c.py - C backend
    • wasm.py - WebAssembly browser
    • common.py - Backend protocol definition
  • tests/ - Test programs
  • bootstrap/ - Self-hosted bootstrap compiler (in udewy itself)
    • main.udewy - CLI entry point
    • t0.udewy, t1.udewy, p0.udewy - Preprocessor, tokenizer, parser
    • runtime/ - Host I/O, subprocess, fs, diagnostics, paths, strbuf
    • backend/ - One file per backend (x86_64, riscv, arm, c, wasm)

Bootstrap Compiler

The bootstrap/ directory contains a self-hosted compiler written in udewy itself. It is feature-equivalent to the Python implementation and supports all five backends (x86_64, riscv, arm, c, wasm32). The x86_64 and C backends are fully self-hosting: the bootstrap can compile itself targeting either.

Backend-agnostic core

Most of the bootstrap is not tied to a single backend. The preprocessor (t0.udewy, t1.udewy), tokenizer, parser (p0.udewy), and the shared backend protocol (backend/common.udewy) are ordinary udewy code that emits through the abstract Backend interface. Each concrete backend file (backend/x86_64.udewy, backend/wasm.udewy, etc.) implements that interface for one target.

The only backend-specific coupling in day-to-day compiler logic is whatever the selected backend must provide at codegen time (syscalls vs browser host functions vs C extern lowering). The compiler algorithm is the same across targets.

Host I/O behind capabilities

Operating-system interactions (reading and writing files, spawning subprocesses, printing diagnostics, exiting) go through stdlib/stdlib.udewy, which imports a host I/O capability module for the active compile target:

  • stdlib/capabilities/host_io_x86_64.udewy
  • stdlib/capabilities/host_io_riscv.udewy
  • stdlib/capabilities/host_io_arm.udewy
  • stdlib/capabilities/host_io_wasm32.udewy
  • stdlib/capabilities/host_io_c.udewy

The bootstrap runtime/ helpers (fs.udewy, process.udewy, diag.udewy, paths.udewy) sit on top of that shared host_* API. Adding a new backend means implementing host I/O for that target, not rewriting the parser.

Why the bootstrap includes WASM

The wasm32 backend is part of the bootstrap so udewy programs including the compiler itself can be hosted and demonstrated directly in the browser.

  • main.udewy is the normal CLI entry point (native or hosted C).
  • web_compiler.udewy is a wasm32-only library entry point: a JavaScript host feeds source bytes in, the bootstrap runs t1 + p0 + the wasm backend, and WAT bytes come back out for assembly in the browser. The playground under udewy/tests/web/ is the UI that drives it.

Longer term, browser hosting might move up to full Dewy instead of living in the trusted-base rung; at that point we would ship Dewy-compiled compiler artifacts for the web rather than depending on udewy's wasm backend forever. For now, having wasm in the bootstrap is a practical way to demo the language and the self-hosted compiler on the web.

Usage

# Compile the bootstrap to a native binary (via the Python compiler)
python -m udewy --target x86_64 -c udewy/bootstrap/main.udewy
# Now use the bootstrap to compile programs
./__dewycache__/udewy/bootstrap/main --target wasm32 -c udewy/tests/test_hello.udewy
# Self-host: bootstrap compiles itself
./__dewycache__/udewy/bootstrap/main --target x86_64 -c udewy/bootstrap/main.udewy