ludic/selfhost/emit_head.ludic
Orkuncakilkaya e1d7797e29 Phase 6f: let = immutable, var = mutable (binding-only)
Bindings now signal mutability the way Rust/Swift do, instead of `let` meaning
"local" and `var` meaning "module-level":

  - `let x = e`  -> immutable binding; a later `x = …` is a compile error
    (`cannot assign to immutable 'x' … use var`).
  - `var x = e`  -> mutable binding, at local OR module scope (position decides
    scope; the keyword decides mutability).
  - `const`      -> unchanged (compile-time).

Immutability is of the *binding*, not the object: `let n = new Node; n.kind = 1`
is fine (mutation through the reference); only rebinding `n` is rejected. The
check lives in emit_assign — a direct `name =` whose target is a `let` local
(loc_mut == 0) errors; field/index targets and `var`/param/loop bindings are
unaffected.

Delivered as three reseeds so the self-hosting compiler never had to compile
source its own rules would reject:
  A) add `var` as a local statement + per-local mutability tracking (loc_mut),
     no enforcement;
  B) migrate every reassigned `let` -> `var` across the compiler, runtime and
     examples (337 declarations), driven by a per-function, string/comment-aware
     scan (binding targets only, never `x.f =` / `x[i] =`);
  C) turn on the check. bootstrap-cfree (compiler vs its own source) and every
     golden build (which splices the runtime) then proved zero reassigned `let`
     was missed anywhere.

Also folded in: removed leftover debug instrumentation in block() (a `cur=` /
print_int(777…) trace on the separator-error path) and fixed parse.ludic's stale
header comment (no more `struct`). Reseeded (21711 lines); C-free fixpoint holds;
goldens identical; 17/17; vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 23:52:59 +03:00

110 lines
4 KiB
Text

# emit_head.ludic — string constants and the module header (libc declarations,
# the slice header type, struct layouts, globals, argv, format strings).
fn 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
fn emit_str_const(s: ptr) -> ptr {
let name = sconcat("@.str", itoa(ll_str))
ll_str = ll_str + 1
let n = slen(s)
emith(name); emith(" = private unnamed_addr constant [")
emith(itoa(n + 1)); emith(" x i8] c\"")
var i = 0
while i < n {
let c = peek8(s, i)
if c == 34 or c == 92 or c < 32 or c > 126 {
buf_putc(head, 92) # backslash
buf_putc(head, hexdig(c / 16))
buf_putc(head, hexdig(c % 16))
} else { buf_putc(head, c) }
i = i + 1
}
emith("\\00\"\n")
return name
}
# a fresh SSA register bound to a `getelementptr`, returned as its name
fn emit_gep_i8(base: ptr, idx: ptr) -> ptr {
let r = nreg()
emit(" "); emit(r); emit(" = getelementptr inbounds i8, ptr ")
emit(base); emit(", i32 "); emit(idx); emit("\n")
return r
}
# the constant initializer for a global var: a literal, or 0/null
fn global_init(d: Node) -> ptr {
if ptr_is_null(d.a) { if streq(llty(d.ty), "ptr") { return "null" }; return "0" }
let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) }
if e.kind == E_UN and streq(e.s, "-") and e.a.kind == E_INT { return sconcat("-", itoa(e.a.ival)) }
if streq(llty(d.ty), "ptr") { return "null" }
return "0"
}
fn 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 @realloc(ptr, i64)\n")
emith("declare void @free(ptr)\n")
emith("declare ptr @fopen(ptr, ptr)\n")
emith("declare i64 @fread(ptr, i64, i64, ptr)\n")
emith("declare i64 @fwrite(ptr, i64, i64, ptr)\n")
emith("declare i32 @fseek(ptr, i64, i32)\n")
emith("declare i64 @ftell(ptr)\n")
emith("declare i32 @fclose(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 i64 @strlen(ptr)\n")
emith("declare i32 @getchar()\n")
emith("declare i32 @putchar(i32)\n")
emith("declare i64 @time(ptr)\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("@__stderrp = external global ptr\n")
emith("@.fmt_str = private unnamed_addr constant [3 x i8] c\"%s\\00\"\n")
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
emith("@L_argc = internal global i32 0\n")
emith("@L_argv = internal global ptr null\n")
emith("@.gametitle = private unnamed_addr constant [")
emith(itoa(slen(g_game_name) + 1)); emith(" x i8] c\""); emith(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 {
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 = f + 1
}
emith(" }\n")
}
i = 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 {
emith("@g_"); emith(d.s); emith(" = internal global ")
emith(llty(d.ty)); emith(" ")
emith(global_init(d))
emith("\n")
}
i = i + 1
}
}