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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 12:06:45 +03:00
parent 2fe4018d28
commit 9660587e10
10 changed files with 56019 additions and 48735 deletions

View file

@ -695,9 +695,9 @@ function emit_call(e: Node) -> Val {
let a = emit_expr(e.kids[0])
if (llty(a.ty) == "ptr") { return a }
if is_fp(a.ty) { return emit_fp_str(a) }
if (llty(a.ty) == "i64") { g_uses_longstr = true; return val(emit_bind(`call ptr @lp_long_str(i64 {a.code})`), "string") }
if (llty(a.ty) == "i64") { g_uses_longstr = true; return fresh_val(emit_bind(`call ptr @lp_long_str(i64 {a.code})`), "string") }
g_uses_intstr = true
return val(emit_bind(`call ptr @lp_int_str(i32 {a.code})`), "string")
return fresh_val(emit_bind(`call ptr @lp_int_str(i32 {a.code})`), "string")
}
if (name == "print") { # print(x): a value + newline (string, long, or int)
var a = emit_expr(e.kids[0])
@ -705,6 +705,7 @@ function emit_call(e: Node) -> Val {
if (llty(a.ty) == "ptr") { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr " + `{a.code})\n`) }
else { if (llty(a.ty) == "i64") { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_long, i64 " + `{a.code})\n`) }
else { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_int, i32 " + `{a.code})\n`) } }
if a.fresh { emit(` call void @free(ptr {a.code})\n`) } # a template printed is held by nothing
return val("0", "void")
}
if (name == "bytes") { # bytes(n): allocate n bytes -> a byte buffer

View file

@ -5,8 +5,10 @@
# lit: the expression of a value made only of numeric literals with at least one
# decimal among them (`1.5`, `-0.25`, `1.0 / 3.0`). It is `fixed` on its own and
# is re-evaluated exactly in a float context — see emit_float.ludic.
property Val { code: pointer = null, ty: pointer = null, lit: Node = null }
function val(code: pointer, ty: pointer) -> Val { let v = new Val; v.code = code; v.ty = ty; v.lit = null; return v }
# `fresh`: text this expression just malloc'd (a concatenation, a number's text) that nothing else holds
property Val { code: pointer = null, ty: pointer = null, lit: Node = null, fresh: bool = false }
function fresh_val(code: pointer, ty: pointer) -> Val { let v = val(code, ty); v.fresh = true; return v }
function val(code: pointer, ty: pointer) -> Val { let v = new Val; v.code = code; v.ty = ty; v.lit = null; v.fresh = false; return v }
var head: Buf # module-level: types, globals, string constants
var code: Buf # function bodies

View file

@ -133,9 +133,17 @@ function to_long(v: Val) -> pointer {
function emit_str_op(op: pointer, a: Val, b: Val) -> Val {
g_uses_str = true
if (op == ("+")) {
return val(emit_bind(`call ptr @lp_str_concat(ptr {a.code}, ptr {b.code})`), "string")
let r = fresh_val(emit_bind(`call ptr @lp_str_concat(ptr {a.code}, ptr {b.code})`), "string")
# a half this expression made is copied now and held by nothing: `a + "/" + b` and every
# template's pieces once left one string behind per `+`
if a.fresh { emit(` call void @free(ptr {a.code})\n`) }
if b.fresh and b.code != a.code { emit(` call void @free(ptr {b.code})\n`) }
return r
}
let r = emit_bind(`call i32 @lp_str_eq(ptr {a.code}, ptr {b.code})`)
# a side made just to be compared is held by nothing once it has been: `` `{x}` == "..." ``
if a.fresh { emit(` call void @free(ptr {a.code})\n`) }
if b.fresh and b.code != a.code { emit(` call void @free(ptr {b.code})\n`) }
if (op == ("!=")) {
let c = emit_bind(`icmp eq i32 {r}, 0`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")

View file

@ -247,7 +247,7 @@ function emit_fp_str(v: Val) -> Val {
var d = v.code
var single = "0"
if (v.ty == "float") { d = emit_bind(`fpext float {v.code} to double`); single = "1" }
return val(emit_bind(`call ptr @lp_fp_str(double {d}, i32 {single})`), "string")
return fresh_val(emit_bind(`call ptr @lp_fp_str(double {d}, i32 {single})`), "string")
}
# @lp_fp_str(v, single): "%.*g" with the fewest digits that round-trip (through

View file

@ -127,6 +127,7 @@ function emit_header() -> void {
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")
@ -292,7 +293,7 @@ function emit_str_prelude() -> void {
# 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 returns a pointer into the buffer.
# 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")
@ -300,7 +301,7 @@ function emit_int_str() -> void {
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 %zp = getelementptr inbounds i8, ptr %buf, i32 22\n store i8 48, ptr %zp\n ret ptr %zp\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")
@ -310,7 +311,9 @@ function emit_int_str() -> void {
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 ret ptr %res\n}\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
@ -323,7 +326,7 @@ function emit_long_str() -> void {
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 %zp = getelementptr inbounds i8, ptr %buf, i32 22\n store i8 48, ptr %zp\n ret ptr %zp\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")
@ -333,7 +336,9 @@ function emit_long_str() -> void {
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 ret ptr %res\n}\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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff