Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling, and docs. Phase 1 of the syntax-redesign cohesion pass has landed: edge-system fix, signature-query, when-alias, and the documentation truth-pass. Suite green (14/14), C-free bootstrap fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
369 lines
12 KiB
LLVM
369 lines
12 KiB
LLVM
; ============================================================================
|
|
; runtime/web/wasm.ll — the web platform layer, written in LLVM IR.
|
|
;
|
|
; This is to the browser what runtime/native/cocoa.ll is to macOS: the one
|
|
; per-platform file, hand-written in the same IR ludicc emits, assembled by the
|
|
; same toolchain and handed to the same linker. No C is compiled here either.
|
|
;
|
|
; It exists because wasm32-unknown-unknown has no libc. The Ludic runtime
|
|
; (runtime/native/*.ludic) stands on a floor of ~15 platform calls — malloc,
|
|
; memcpy, fopen, getchar — and on a native target those are the OS's. On the
|
|
; web there is no OS underneath, so this file *is* the floor:
|
|
;
|
|
; * the allocator is implemented here, over wasm linear memory;
|
|
; * memcpy/memset/strlen are implemented here;
|
|
; * everything genuinely external — files, stdin, stdout, the clock, and the
|
|
; five win_* window functions — is a wasm import, resolved by
|
|
; runtime/web/platform.js.
|
|
;
|
|
; Nothing above this file changes for the web. core.ludic, image.ludic,
|
|
; truetype.ludic and ui.ludic compile to wasm unmodified.
|
|
; ============================================================================
|
|
|
|
target triple = "wasm32-unknown-unknown"
|
|
|
|
declare i32 @llvm.wasm.memory.size.i32(i32)
|
|
declare i32 @llvm.wasm.memory.grow.i32(i32, i32)
|
|
declare void @llvm.memcpy.p0.p0.i32(ptr, ptr, i32, i1)
|
|
declare void @llvm.memmove.p0.p0.i32(ptr, ptr, i32, i1)
|
|
declare void @llvm.memset.p0.i32(ptr, i8, i32, i1)
|
|
|
|
; wasm-ld places this symbol immediately after the static data segments; its
|
|
; *address* is where our heap may begin.
|
|
@__heap_base = external global i8
|
|
|
|
; ---------------------------------------------------------------------------
|
|
; The allocator.
|
|
;
|
|
; A first-fit free list over linear memory, growing the wasm memory with
|
|
; memory.grow when it runs out. Every block carries an 8-byte header
|
|
; [i32 payload_size][i32 next_free] and the pointer handed out is header+8, so
|
|
; free() is O(1) and malloc() reuses blocks of a size it has seen before —
|
|
; which is the shape of the load the PNG decoder actually makes (a dozen tiles
|
|
; of identical dimensions, each decode allocating and releasing the same three
|
|
; buffers). Blocks are split when the fit is loose; adjacent free blocks are
|
|
; not coalesced, which costs some fragmentation and buys a lot of simplicity.
|
|
; ---------------------------------------------------------------------------
|
|
|
|
@ludic_brk = internal global i32 0 ; bump pointer; 0 until first use
|
|
@ludic_free = internal global i32 0 ; head of the free list, 0 = empty
|
|
|
|
define ptr @malloc(i32 %n0) {
|
|
entry:
|
|
%small = icmp slt i32 %n0, 1
|
|
%n1 = select i1 %small, i32 1, i32 %n0
|
|
%pad = add i32 %n1, 7
|
|
%n = and i32 %pad, -8 ; 8-byte aligned payloads
|
|
%head = load i32, ptr @ludic_free
|
|
br label %scan
|
|
|
|
scan: ; walk the free list, first fit
|
|
%cur = phi i32 [ %head, %entry ], [ %nxt, %scan.next ]
|
|
%prev = phi i32 [ 0, %entry ], [ %cur, %scan.next ]
|
|
%end = icmp eq i32 %cur, 0
|
|
br i1 %end, label %bump, label %check
|
|
|
|
check:
|
|
%cp = inttoptr i32 %cur to ptr
|
|
%sz = load i32, ptr %cp
|
|
%nxtp = getelementptr i8, ptr %cp, i32 4
|
|
%nxt = load i32, ptr %nxtp
|
|
%fits = icmp sge i32 %sz, %n
|
|
br i1 %fits, label %take, label %scan.next
|
|
|
|
scan.next:
|
|
br label %scan
|
|
|
|
take: ; unlink %cur from the free list
|
|
%first = icmp eq i32 %prev, 0
|
|
br i1 %first, label %unlink.head, label %unlink.mid
|
|
unlink.head:
|
|
store i32 %nxt, ptr @ludic_free
|
|
br label %split
|
|
unlink.mid:
|
|
%pp = inttoptr i32 %prev to ptr
|
|
%pnp = getelementptr i8, ptr %pp, i32 4
|
|
store i32 %nxt, ptr %pnp
|
|
br label %split
|
|
|
|
split: ; give the tail back if it is worth it
|
|
%floor = add i32 %n, 40
|
|
%loose = icmp sge i32 %sz, %floor
|
|
br i1 %loose, label %do.split, label %take.done
|
|
do.split:
|
|
store i32 %n, ptr %cp
|
|
%tail = sub i32 %sz, %n
|
|
%tailsz = sub i32 %tail, 8
|
|
%tailat0 = add i32 %cur, 8
|
|
%tailat = add i32 %tailat0, %n
|
|
%tp = inttoptr i32 %tailat to ptr
|
|
store i32 %tailsz, ptr %tp
|
|
%tnp = getelementptr i8, ptr %tp, i32 4
|
|
%fh = load i32, ptr @ludic_free
|
|
store i32 %fh, ptr %tnp
|
|
store i32 %tailat, ptr @ludic_free
|
|
br label %take.done
|
|
take.done:
|
|
%reuse = getelementptr i8, ptr %cp, i32 8
|
|
ret ptr %reuse
|
|
|
|
bump: ; nothing reusable: take fresh memory
|
|
%b0 = load i32, ptr @ludic_brk
|
|
%new = icmp eq i32 %b0, 0
|
|
br i1 %new, label %brk.init, label %brk.have
|
|
brk.init:
|
|
%hb = ptrtoint ptr @__heap_base to i32
|
|
%hb7 = add i32 %hb, 7
|
|
%hba = and i32 %hb7, -8
|
|
br label %brk.have
|
|
brk.have:
|
|
%brk = phi i32 [ %b0, %bump ], [ %hba, %brk.init ]
|
|
%hdr = add i32 %brk, 8
|
|
%need = add i32 %hdr, %n
|
|
%pages = call i32 @llvm.wasm.memory.size.i32(i32 0)
|
|
%have = shl i32 %pages, 16
|
|
%short = icmp ugt i32 %need, %have
|
|
br i1 %short, label %grow, label %carve
|
|
grow:
|
|
%deficit = sub i32 %need, %have
|
|
%round = add i32 %deficit, 65535
|
|
%want = lshr i32 %round, 16
|
|
%got = call i32 @llvm.wasm.memory.grow.i32(i32 0, i32 %want)
|
|
%oom = icmp eq i32 %got, -1
|
|
br i1 %oom, label %fail, label %carve
|
|
fail:
|
|
ret ptr null
|
|
carve:
|
|
%hp = inttoptr i32 %brk to ptr
|
|
store i32 %n, ptr %hp
|
|
store i32 %need, ptr @ludic_brk
|
|
%up = inttoptr i32 %hdr to ptr
|
|
ret ptr %up
|
|
}
|
|
|
|
define void @free(ptr %p) {
|
|
entry:
|
|
%nil = icmp eq ptr %p, null
|
|
br i1 %nil, label %out, label %push
|
|
push:
|
|
%pi = ptrtoint ptr %p to i32
|
|
%hi = sub i32 %pi, 8
|
|
%hp = inttoptr i32 %hi to ptr
|
|
%np = getelementptr i8, ptr %hp, i32 4
|
|
%fh = load i32, ptr @ludic_free
|
|
store i32 %fh, ptr %np
|
|
store i32 %hi, ptr @ludic_free
|
|
br label %out
|
|
out:
|
|
ret void
|
|
}
|
|
|
|
; calloc and realloc are here because LLVM *invents* calls to them. It
|
|
; recognises the allocator by name, so an -O2 pass rewrites the
|
|
; mem_alloc()+mem_set(0) that core.ludic's rt_init does into a single calloc.
|
|
; The platform layer therefore has to be a complete enough libc for whatever the
|
|
; optimiser lowers to, not just for what ludicc emits.
|
|
define ptr @calloc(i32 %count, i32 %size) {
|
|
entry:
|
|
%n = mul i32 %count, %size
|
|
%p = call ptr @malloc(i32 %n)
|
|
%nil = icmp eq ptr %p, null
|
|
br i1 %nil, label %out, label %zero
|
|
zero:
|
|
call void @llvm.memset.p0.i32(ptr %p, i8 0, i32 %n, i1 false)
|
|
br label %out
|
|
out:
|
|
ret ptr %p
|
|
}
|
|
|
|
; the block header carries its own size, so realloc can grow in place
|
|
define ptr @realloc(ptr %p, i32 %n) {
|
|
entry:
|
|
%nil = icmp eq ptr %p, null
|
|
br i1 %nil, label %fresh, label %known
|
|
fresh:
|
|
%f = call ptr @malloc(i32 %n)
|
|
ret ptr %f
|
|
known:
|
|
%pi = ptrtoint ptr %p to i32
|
|
%hi = sub i32 %pi, 8
|
|
%hp = inttoptr i32 %hi to ptr
|
|
%old = load i32, ptr %hp
|
|
%big = icmp sge i32 %old, %n
|
|
br i1 %big, label %keep, label %move
|
|
keep:
|
|
ret ptr %p
|
|
move:
|
|
%new = call ptr @malloc(i32 %n)
|
|
%bad = icmp eq ptr %new, null
|
|
br i1 %bad, label %fail, label %shift
|
|
fail:
|
|
ret ptr null
|
|
shift:
|
|
call void @llvm.memcpy.p0.p0.i32(ptr %new, ptr %p, i32 %old, i1 false)
|
|
call void @free(ptr %p)
|
|
ret ptr %new
|
|
}
|
|
|
|
; ---------------------------------------------------------------------------
|
|
; Bulk memory and strings.
|
|
;
|
|
; size_t is 32-bit on wasm32, which is the signature ludicc emits for this
|
|
; target and the one LLVM canonicalises these names to. They use the wasm
|
|
; bulk-memory instructions (memory.copy / memory.fill) rather than a byte loop.
|
|
; ---------------------------------------------------------------------------
|
|
|
|
define ptr @memcpy(ptr %d, ptr %s, i32 %n) {
|
|
call void @llvm.memcpy.p0.p0.i32(ptr %d, ptr %s, i32 %n, i1 false)
|
|
ret ptr %d
|
|
}
|
|
|
|
define ptr @memset(ptr %d, i32 %c, i32 %n) {
|
|
%b = trunc i32 %c to i8
|
|
call void @llvm.memset.p0.i32(ptr %d, i8 %b, i32 %n, i1 false)
|
|
ret ptr %d
|
|
}
|
|
|
|
define ptr @memmove(ptr %d, ptr %s, i32 %n) {
|
|
call void @llvm.memmove.p0.p0.i32(ptr %d, ptr %s, i32 %n, i1 false)
|
|
ret ptr %d
|
|
}
|
|
|
|
define i32 @memcmp(ptr %a, ptr %b, i32 %n) {
|
|
entry:
|
|
br label %loop
|
|
loop:
|
|
%i = phi i32 [ 0, %entry ], [ %i1, %step ]
|
|
%done = icmp uge i32 %i, %n
|
|
br i1 %done, label %same, label %test
|
|
test:
|
|
%ap = getelementptr i8, ptr %a, i32 %i
|
|
%bp = getelementptr i8, ptr %b, i32 %i
|
|
%av = load i8, ptr %ap
|
|
%bv = load i8, ptr %bp
|
|
%eq = icmp eq i8 %av, %bv
|
|
br i1 %eq, label %step, label %differ
|
|
step:
|
|
%i1 = add i32 %i, 1
|
|
br label %loop
|
|
differ:
|
|
%az = zext i8 %av to i32
|
|
%bz = zext i8 %bv to i32
|
|
%r = sub i32 %az, %bz
|
|
ret i32 %r
|
|
same:
|
|
ret i32 0
|
|
}
|
|
|
|
; LLVM emits bcmp for comparisons whose result is only tested against zero.
|
|
define i32 @bcmp(ptr %a, ptr %b, i32 %n) {
|
|
%r = call i32 @memcmp(ptr %a, ptr %b, i32 %n)
|
|
ret i32 %r
|
|
}
|
|
|
|
define i32 @strlen(ptr %s) {
|
|
entry:
|
|
br label %loop
|
|
loop:
|
|
%i = phi i32 [ 0, %entry ], [ %i1, %step ]
|
|
%cp = getelementptr i8, ptr %s, i32 %i
|
|
%c = load i8, ptr %cp
|
|
%z = icmp eq i8 %c, 0
|
|
br i1 %z, label %out, label %step
|
|
step:
|
|
%i1 = add i32 %i, 1
|
|
br label %loop
|
|
out:
|
|
ret i32 %i
|
|
}
|
|
|
|
; ---------------------------------------------------------------------------
|
|
; Everything genuinely outside the module.
|
|
;
|
|
; These are wasm imports: platform.js supplies them, exactly as Cocoa supplies
|
|
; objc_msgSend to cocoa.ll. Files are served from a preloaded read-only image of
|
|
; the game's assets; the save file is backed by localStorage.
|
|
; ---------------------------------------------------------------------------
|
|
|
|
declare i32 @web_fopen(ptr, ptr) ; -> handle, 0 on failure
|
|
declare i32 @web_fread(i32, ptr, i32)
|
|
declare i32 @web_fwrite(i32, ptr, i32)
|
|
declare void @web_fclose(i32)
|
|
declare i32 @web_fseek(i32, i32, i32)
|
|
declare i32 @web_ftell(i32)
|
|
declare i32 @web_time()
|
|
declare void @web_exit(i32)
|
|
|
|
; A file handle is an opaque `ptr` to the Ludic runtime, and a small integer to
|
|
; JS. Handle 0 doubles as libc's NULL, so `file_open` failure checks work
|
|
; unchanged.
|
|
define ptr @fopen(ptr %path, ptr %mode) {
|
|
%h = call i32 @web_fopen(ptr %path, ptr %mode)
|
|
%p = inttoptr i32 %h to ptr
|
|
ret ptr %p
|
|
}
|
|
|
|
define i32 @fread(ptr %buf, i32 %size, i32 %count, ptr %f) {
|
|
%bytes = mul i32 %size, %count
|
|
%h = ptrtoint ptr %f to i32
|
|
%got = call i32 @web_fread(i32 %h, ptr %buf, i32 %bytes)
|
|
ret i32 %got
|
|
}
|
|
|
|
define i32 @fwrite(ptr %buf, i32 %size, i32 %count, ptr %f) {
|
|
%bytes = mul i32 %size, %count
|
|
%h = ptrtoint ptr %f to i32
|
|
%put = call i32 @web_fwrite(i32 %h, ptr %buf, i32 %bytes)
|
|
ret i32 %put
|
|
}
|
|
|
|
define i32 @fclose(ptr %f) {
|
|
%h = ptrtoint ptr %f to i32
|
|
call void @web_fclose(i32 %h)
|
|
ret i32 0
|
|
}
|
|
|
|
define i32 @fseek(ptr %f, i32 %off, i32 %whence) {
|
|
%h = ptrtoint ptr %f to i32
|
|
%r = call i32 @web_fseek(i32 %h, i32 %off, i32 %whence)
|
|
ret i32 %r
|
|
}
|
|
|
|
define i32 @ftell(ptr %f) {
|
|
%h = ptrtoint ptr %f to i32
|
|
%r = call i32 @web_ftell(i32 %h)
|
|
ret i32 %r
|
|
}
|
|
|
|
define i64 @time(ptr %slot) {
|
|
%s = call i32 @web_time()
|
|
%r = sext i32 %s to i64
|
|
ret i64 %r
|
|
}
|
|
|
|
define void @exit(i32 %code) noreturn {
|
|
call void @web_exit(i32 %code)
|
|
unreachable
|
|
}
|
|
|
|
; stdin/stdout. A windowed web build never reads stdin — rt_poll() goes through
|
|
; win_poll() — but the intrinsic still has to resolve, and the headless path
|
|
; (used by the test suite under Node) genuinely uses both.
|
|
declare i32 @web_getchar()
|
|
declare void @web_putchar(i32)
|
|
|
|
define i32 @getchar() {
|
|
%c = call i32 @web_getchar()
|
|
ret i32 %c
|
|
}
|
|
|
|
define i32 @putchar(i32 %c) {
|
|
call void @web_putchar(i32 %c)
|
|
ret i32 %c
|
|
}
|
|
|
|
; `print_str` is the one intrinsic ludicc lowers differently for the web: on a
|
|
; native target it goes through variadic printf("%s"), which wasm's strict
|
|
; signature checking makes awkward, so the web backend emits a direct call to
|
|
; @web_print_str instead. It is a pure import — platform.js defines it — and so
|
|
; needs no adapter here.
|