ludic/selfhost/backend/emit_head.ludic
Orkuncakilkaya 9660587e10 compiler: free a string an expression made once it has been used
The left half of a + chain, a template's pieces and holes, a number's text and a side made only to be
compared are marked fresh and freed after the +, ==, != or print that reads them. lp_int_str and
lp_long_str move their digits to the start of the buffer, so the pointer they return is the one
malloc gave. Reseeded. examples/lang/string_temps.ludic: kept intermediates stay good, and 20,000
rounds grow the heap 0 bytes (2.9 MB before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:06:45 +03:00

355 lines
17 KiB
Text

# emit_head.ludic — string constants and the module header (libc declarations,
# the slice header type, struct layouts, globals, argv, format strings).
function hexdig(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n } # 0-9 A-F
# emit `@.strN = ... c"escaped\00"` and return its name; % and non-print -> \XX
function emit_str_const(s: pointer) -> pointer {
let name = `@.str{itoa(ll_str)}`
ll_str += 1
let n = len(s)
emith(name); emith(" = private unnamed_addr constant [")
emith(itoa(n + 1)); emith(" x i8] c\"")
emit_escaped(s)
emith("\\00\"\n")
return name
}
# the bytes of s as the body of an IR c"..." string: " \ and non-print -> \XX
function emit_escaped(s: pointer) -> void {
let n = len(s)
var i = 0
while i < n {
let c = s[i]
if c == '"' or c == CH_BACKSLASH or c < ' ' or c > '~' {
buf_putc(head, CH_BACKSLASH) # \XX hex escape
buf_putc(head, hexdig(c / 16))
buf_putc(head, hexdig(c % 16))
} else { buf_putc(head, c) }
i += 1
}
}
# an int literal's LLVM constant: its value, or a long literal's own digits
function int_lit_code(e: Node) -> pointer {
if e.s != null { return e.s }
return itoa(e.ival)
}
# the constant initializer for a global var: a literal, or 0/null
function global_init(d: Node) -> pointer {
if (d.a == null) or is_fp(d.ty) { return zero_of(llty(d.ty)) } # a float's value is set by L_init_globals
let e = d.a
if e.kind == E_INT { return int_lit_code(e) }
if e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) }
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return (("-") + int_lit_code(e.a)) }
if e.kind == E_MEMBER and e.a.kind == E_ID { # `Enum.Variant` is a compile-time int
let ord = enum_ordinal(e.a.s, e.s)
if ord >= 0 { return itoa(ord) }
}
if (llty(d.ty) == "ptr") { return "null" }
return "0"
}
# does a global's initializer need code at startup (anything global_init cannot fold)?
function global_needs_init_code(d: Node) -> bool {
if (d.a == null) { return false }
if is_fp(d.ty) { return true }
let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL or e.kind == E_NULL { return false }
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return false }
if e.kind == E_MEMBER and e.a.kind == E_ID { if enum_ordinal(e.a.s, e.s) >= 0 { return false } }
return true
}
# @L_init_globals(): evaluate every global initializer global_init could not fold
# (`var run: Progress = new Progress`, `var speed: int = BASE * 2`, a call), in
# declaration order, once at startup — after the runtime boots, before Start.
function emit_global_init_fn() -> void {
emit_init_fn("L_init_runtime", true)
emit_init_fn("L_init_globals", false)
}
# 0.S: the runtime's states are made first, before it boots (rt_init reads them); the rest after
function is_runtime_state_var(d: Node) -> bool { return d.uns == 1 and is_state_ty(d.ty) and is_runtime_file(d.file) }
function emit_init_fn(name: pointer, runtime: bool) -> void {
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_VAR and global_needs_init_code(d) and is_runtime_state_var(d) == runtime {
# an initializer is the global's own file's code: its errors, and what its module
# may see (L3), are that file's - not whichever statement was lowered last
g_err_file = d.file
g_err_line = d.line
# a port's bind is written in the app and checked there (frontend/ports.ludic)
if is_port_var(d) {
g_err_file = d.a.file
g_err_line = d.a.line
g_vis_off = true
}
let v = emit_expr(d.a)
g_vis_off = false
let cv = coerce_code(v, d.ty) # any conversion is its own line, before the store
emit(" store "); emit(llty(d.ty)); emit(" "); emit(cv); emit(", ptr @g_"); emit(d.s); emit("\n")
}
i += 1
}
emit(" ret void\n")
code = saved
emit(`define void @{name}() {{\nentry:\n`)
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
function emit_header() -> void {
emith("; Ludic (self-hosted) -> LLVM IR\n")
emith("declare i32 @printf(ptr, ...)\n")
emith("declare ptr @malloc(i64)\n")
emith("declare ptr @calloc(i64, i64)\n")
emith("declare ptr @realloc(ptr, i64)\n")
emith("declare void @free(ptr)\n")
emith("declare i64 @fread(ptr, i64, i64, ptr)\n")
emith("declare i64 @fwrite(ptr, i64, i64, ptr)\n")
emith("declare i32 @fclose(ptr)\n")
# on Windows these four are defined by emit_win_prelude, below
if not g_target_win {
emith("declare ptr @fopen(ptr, ptr)\n")
emith("declare i32 @fseek(ptr, i64, i32)\n")
emith("declare i64 @ftell(ptr)\n")
emith("declare i64 @time(ptr)\n")
}
emith("declare void @exit(i32)\n")
emith("declare i32 @system(ptr)\n")
emith("declare ptr @getenv(ptr)\n")
emith("declare ptr @memcpy(ptr, ptr, i64)\n")
emith("declare ptr @memset(ptr, i32, i64)\n")
emith("declare ptr @memmove(ptr, ptr, i64)\n")
emith("declare i64 @strlen(ptr)\n")
emith("declare i32 @strcmp(ptr, ptr)\n")
emith("declare i32 @strncmp(ptr, ptr, i64)\n")
emith("declare ptr @strstr(ptr, ptr)\n")
emith("declare i32 @atoi(ptr)\n")
emith("declare i32 @getchar()\n")
emith("declare i32 @putchar(i32)\n")
emith("declare void @win_open(i32, i32, i32, ptr)\n")
emith("declare i32 @win_poll()\n")
emith("declare void @win_present(ptr, i32, i32)\n")
emith("declare i32 @win_running()\n")
emith("declare void @win_close()\n")
emith("declare void @win_held(ptr)\n")
emith("declare i32 @win_key_char(i32)\n")
emith("declare i32 @win_text(ptr, i32)\n")
emith("declare void @win_mouse(ptr)\n")
emith("declare void @win_pad(ptr)\n")
emith("declare void @win_touch(ptr)\n")
emith("declare void @win_cursor_mode(i32)\n")
emith("declare ptr @snd_load(ptr)\n") # #22 audio backend (audio.ll)
emith("declare void @snd_play(ptr, i32, i32, i32)\n")
emith("declare void @snd_stop(ptr)\n")
emith("declare i32 @snd_playing(ptr)\n")
emith("declare void @snd_set_volume(ptr, i32)\n")
emith("declare void @snd_set_rate(ptr, i32)\n")
emith("declare void @snd_set_pan(ptr, i32)\n")
emith("declare ptr @hs_req_new(ptr, ptr)\n") # #6 HTTP transport (http.ll)
emith("declare void @hs_req_header(ptr, ptr, ptr)\n")
emith("declare void @hs_req_body(ptr, ptr, i32)\n")
emith("declare void @hs_send(i32, ptr)\n")
emith("declare i32 @hs_done(i32)\n")
emith("declare i32 @hs_status(i32)\n")
emith("declare ptr @hs_body(i32)\n")
emith("declare i32 @hs_blen(i32)\n")
emith("declare ptr @hs_header(i32, ptr)\n")
emith("declare void @hs_free(i32)\n")
if g_target_win { emit_win_prelude() } # the UCRT/Win32 definitions of the POSIX names above
else {
emith("@__stderrp = external global ptr\n")
emith("@__stdoutp = external global ptr\n")
}
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
emith("@.fmt_long = private unnamed_addr constant [6 x i8] c\"%lld\\0A\\00\"\n")
emith("@.fmt_line = private unnamed_addr constant [4 x i8] c\"%s\\0A\\00\"\n")
emith("@L_argc = internal global i32 0\n")
emith("@L_argv = internal global ptr null\n")
emith("@L_clock = internal global i32 0\n") # Clock.* — the game-controlled simulated clock
emith("@.gametitle = private unnamed_addr constant [")
emith(itoa(len(g_game_name) + 1)); emith(" x i8] c\""); emit_escaped(g_game_name); emith("\\00\"\n")
emith("%LSlice = type { ptr, i32, i32 }\n")
# property layouts — a %Cmp_ record of named fields, emitted here so `new`
# works whether or not the program runs the ECS. The per-entity @S_/@H_ arrays
# are separate (emit_ecs_storage), emitted only for a program that runs the ECS.
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_COMP {
var j = 0 # the same record twice would silently use the first
while j < i {
if prog[j].kind == N_COMP and (prog[j].s == d.s) {
if (j < g_prog_user_end) and (i >= g_prog_user_end) { perr(`property {d.s} is also a property of the engine runtime; choose another name`) }
perr(`property {d.s} is declared twice`)
}
j += 1
}
emith(layout_ty(d.s)); emith(" = type { ")
if len(d.kids) == 0 { emith("i32") }
var f = 0
while f < len(d.kids) {
if f > 0 { emith(", ") }
emith(llty(d.kids[f].ty))
f += 1
}
emith(" }\n")
}
i += 1
}
# globals (vars) — aggregates/pointers default to null, scalars to 0
i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_VAR {
var j = 0 # the same name twice would be one LLVM global
while j < i {
if prog[j].kind == N_VAR and (prog[j].s == d.s) {
if (j < g_prog_user_end) and (i >= g_prog_user_end) { perr(`variable {d.s} is also a variable of the engine runtime; choose another name`) }
perr(`variable {d.s} is declared twice`)
}
j += 1
}
emith("@g_"); emith(d.s); emith(" = internal global ")
emith(llty(d.ty)); emith(" ")
emith(global_init(d))
emith("\n")
}
i += 1
}
}
# One `declare <ret> @<sym>(<argtys>)` per `extern fn`, so the linker resolves the
# call to the bound symbol. Emitted after the header; a program with no `extern fn`
# emits nothing here, so un-networked builds stay byte-identical.
function emit_extern_decls() -> void {
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_EXTERN {
emith("declare "); emith(llty(d.ty)); emith(" @"); emith(d.a.s); emith("(")
var f = 0
while f < len(d.kids) {
if f > 0 { emith(", ") }
emith(llty(d.kids[f].ty))
f += 1
}
emith(")\n")
}
i += 1
}
}
# The string runtime, emitted (once) into any program that uses `+`/`==`/`!=`
# on strings. Hand-written IR over NUL-terminated byte buffers: str_eq walks both
# until a mismatch or a shared terminator; str_concat measures both, mallocs
# len+len+1, copies each half, and NUL-terminates. @malloc is always declared.
function emit_str_prelude() -> void {
emith("define i32 @lp_str_eq(ptr %a, ptr %b) {\n")
emith("entry:\n br label %loop\n")
emith("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n")
emith(" %pa = getelementptr inbounds i8, ptr %a, i32 %i\n")
emith(" %pb = getelementptr inbounds i8, ptr %b, i32 %i\n")
emith(" %ca = load i8, ptr %pa\n %cb = load i8, ptr %pb\n")
emith(" %df = icmp ne i8 %ca, %cb\n br i1 %df, label %ret0, label %chk\n")
emith("chk:\n %zt = icmp eq i8 %ca, 0\n br i1 %zt, label %ret1, label %cont\n")
emith("cont:\n %i1 = add i32 %i, 1\n br label %loop\n")
emith("ret1:\n ret i32 1\nret0:\n ret i32 0\n}\n")
emith("define ptr @lp_str_concat(ptr %a, ptr %b) {\n")
emith("entry:\n br label %al\n")
emith("al:\n %ia = phi i32 [ 0, %entry ], [ %ia1, %alb ]\n")
emith(" %pa = getelementptr inbounds i8, ptr %a, i32 %ia\n %cca = load i8, ptr %pa\n")
emith(" %za = icmp eq i8 %cca, 0\n br i1 %za, label %bl0, label %alb\n")
emith("alb:\n %ia1 = add i32 %ia, 1\n br label %al\n")
emith("bl0:\n br label %bl\n")
emith("bl:\n %ib = phi i32 [ 0, %bl0 ], [ %ib1, %blb ]\n")
emith(" %pb = getelementptr inbounds i8, ptr %b, i32 %ib\n %ccb = load i8, ptr %pb\n")
emith(" %zb = icmp eq i8 %ccb, 0\n br i1 %zb, label %alloc, label %blb\n")
emith("blb:\n %ib1 = add i32 %ib, 1\n br label %bl\n")
emith("alloc:\n %sum = add i32 %ia, %ib\n %sz = add i32 %sum, 1\n")
emith(" %sz64 = sext i32 %sz to i64\n %out = call ptr @malloc(i64 %sz64)\n br label %c1\n")
emith("c1:\n %i = phi i32 [ 0, %alloc ], [ %i1, %c1b ]\n")
emith(" %d1 = icmp slt i32 %i, %ia\n br i1 %d1, label %c1b, label %c2i\n")
emith("c1b:\n %s1 = getelementptr inbounds i8, ptr %a, i32 %i\n %v1 = load i8, ptr %s1\n")
emith(" %o1 = getelementptr inbounds i8, ptr %out, i32 %i\n store i8 %v1, ptr %o1\n")
emith(" %i1 = add i32 %i, 1\n br label %c1\n")
emith("c2i:\n br label %c2\n")
emith("c2:\n %j = phi i32 [ 0, %c2i ], [ %j1, %c2b ]\n")
emith(" %d2 = icmp slt i32 %j, %ib\n br i1 %d2, label %c2b, label %fin\n")
emith("c2b:\n %s2 = getelementptr inbounds i8, ptr %b, i32 %j\n %v2 = load i8, ptr %s2\n")
emith(" %oj = add i32 %ia, %j\n %o2 = getelementptr inbounds i8, ptr %out, i32 %oj\n store i8 %v2, ptr %o2\n")
emith(" %j1 = add i32 %j, 1\n br label %c2\n")
emith("fin:\n %pe = getelementptr inbounds i8, ptr %out, i32 %sum\n store i8 0, ptr %pe\n ret ptr %out\n}\n")
}
# int -> decimal string, emitted (once) into any program that uses string(int)
# (string interpolation of a number). Writes digits from the end of a 24-byte
# buffer, prepends '-' for negatives, and moves them to the buffer's start.
function emit_int_str() -> void {
emith("define ptr @lp_int_str(i32 %n0) {\n")
emith("entry:\n %buf = call ptr @malloc(i64 24)\n")
emith(" %isneg = icmp slt i32 %n0, 0\n %neg = sub i32 0, %n0\n")
emith(" %n = select i1 %isneg, i32 %neg, i32 %n0\n")
emith(" %term = getelementptr inbounds i8, ptr %buf, i32 23\n store i8 0, ptr %term\n")
emith(" %iszero = icmp eq i32 %n0, 0\n br i1 %iszero, label %zc, label %dl\n")
emith("zc:\n store i8 48, ptr %buf\n %z1 = getelementptr inbounds i8, ptr %buf, i32 1\n store i8 0, ptr %z1\n ret ptr %buf\n")
emith("dl:\n br label %dloop\n")
emith("dloop:\n %pos = phi i32 [ 22, %dl ], [ %pos2, %dbody ]\n %cur = phi i32 [ %n, %dl ], [ %cur2, %dbody ]\n")
emith(" %done = icmp eq i32 %cur, 0\n br i1 %done, label %sign, label %dbody\n")
emith("dbody:\n %d = urem i32 %cur, 10\n %ch = add i32 %d, 48\n %ch8 = trunc i32 %ch to i8\n")
emith(" %pp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 %ch8, ptr %pp\n")
emith(" %cur2 = udiv i32 %cur, 10\n %pos2 = sub i32 %pos, 1\n br label %dloop\n")
emith("sign:\n br i1 %isneg, label %addneg, label %fin\n")
emith("addneg:\n %sp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 45, ptr %sp\n %posn = sub i32 %pos, 1\n br label %fin\n")
emith("fin:\n %fpos = phi i32 [ %pos, %sign ], [ %posn, %addneg ]\n")
emith(" %rpos = add i32 %fpos, 1\n %res = getelementptr inbounds i8, ptr %buf, i32 %rpos\n")
# the digits to the buffer's start, so what is returned is what was malloc'd and can be freed
emith(" %cnt = sub i32 24, %rpos\n %cnt64 = sext i32 %cnt to i64\n call ptr @memmove(ptr %buf, ptr %res, i64 %cnt64)\n ret ptr %buf\n}\n")
}
# the i64 twin of fn_int_str: a signed 64-bit integer -> decimal text. Emitted
# once per program that stringifies a `long` (g_uses_longstr). A 64-bit value is
# at most 20 digits plus sign and NUL, so the 24-byte scratch buffer still fits.
function emit_long_str() -> void {
emith("define ptr @lp_long_str(i64 %n0) {\n")
emith("entry:\n %buf = call ptr @malloc(i64 24)\n")
emith(" %isneg = icmp slt i64 %n0, 0\n %neg = sub i64 0, %n0\n")
emith(" %n = select i1 %isneg, i64 %neg, i64 %n0\n")
emith(" %term = getelementptr inbounds i8, ptr %buf, i32 23\n store i8 0, ptr %term\n")
emith(" %iszero = icmp eq i64 %n0, 0\n br i1 %iszero, label %zc, label %dl\n")
emith("zc:\n store i8 48, ptr %buf\n %z1 = getelementptr inbounds i8, ptr %buf, i32 1\n store i8 0, ptr %z1\n ret ptr %buf\n")
emith("dl:\n br label %dloop\n")
emith("dloop:\n %pos = phi i32 [ 22, %dl ], [ %pos2, %dbody ]\n %cur = phi i64 [ %n, %dl ], [ %cur2, %dbody ]\n")
emith(" %done = icmp eq i64 %cur, 0\n br i1 %done, label %sign, label %dbody\n")
emith("dbody:\n %d = urem i64 %cur, 10\n %d32 = trunc i64 %d to i32\n %ch = add i32 %d32, 48\n %ch8 = trunc i32 %ch to i8\n")
emith(" %pp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 %ch8, ptr %pp\n")
emith(" %cur2 = udiv i64 %cur, 10\n %pos2 = sub i32 %pos, 1\n br label %dloop\n")
emith("sign:\n br i1 %isneg, label %addneg, label %fin\n")
emith("addneg:\n %sp = getelementptr inbounds i8, ptr %buf, i32 %pos\n store i8 45, ptr %sp\n %posn = sub i32 %pos, 1\n br label %fin\n")
emith("fin:\n %fpos = phi i32 [ %pos, %sign ], [ %posn, %addneg ]\n")
emith(" %rpos = add i32 %fpos, 1\n %res = getelementptr inbounds i8, ptr %buf, i32 %rpos\n")
# the digits to the buffer's start, so what is returned is what was malloc'd and can be freed
emith(" %cnt = sub i32 24, %rpos\n %cnt64 = sext i32 %cnt to i64\n call ptr @memmove(ptr %buf, ptr %res, i64 %cnt64)\n ret ptr %buf\n}\n")
}
# s[a..b] -> a fresh NUL-terminated copy of the bytes [a, b), emitted (once) into
# any program that slices a string. Mallocs (b-a)+1, copies, terminates.
function emit_str_slice() -> void {
emith("define ptr @lp_str_slice(ptr %s, i32 %start, i32 %end) {\n")
emith("entry:\n %len = sub i32 %end, %start\n %sz = add i32 %len, 1\n")
emith(" %sz64 = sext i32 %sz to i64\n %out = call ptr @malloc(i64 %sz64)\n br label %loop\n")
emith("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %body ]\n")
emith(" %d = icmp slt i32 %i, %len\n br i1 %d, label %body, label %fin\n")
emith("body:\n %si = add i32 %start, %i\n %sp = getelementptr inbounds i8, ptr %s, i32 %si\n %c = load i8, ptr %sp\n")
emith(" %op = getelementptr inbounds i8, ptr %out, i32 %i\n store i8 %c, ptr %op\n %i1 = add i32 %i, 1\n br label %loop\n")
emith("fin:\n %tp = getelementptr inbounds i8, ptr %out, i32 %len\n store i8 0, ptr %tp\n ret ptr %out\n}\n")
}