Merge branch 'lang/str-temps' into lang/uifree

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 12:19:16 +03:00
commit d9c574f107
10 changed files with 56019 additions and 48735 deletions

View file

@ -0,0 +1,9 @@
bump: patch
type: fix
**A string made in the middle of an expression is freed once it has been used.** `a + "/" + b` made
`a + "/"`, copied it and kept it for ever; a template made one such piece per hole, plus each
number's text; and a side of `==` built only to be compared stayed too. The compiler now frees each
of those as soon as the next `+`, the comparison or `print` has read it, and `string(n)` hands back
the start of what it allocated so its text can be freed at all. A string held anywhere - a
variable, an argument, a function's result - is left alone. A loop of 20,000 template builds grew
2.9 MB and now grows 0 (`examples/lang/string_temps.ludic`).

View file

@ -0,0 +1,46 @@
# string_temps.ludic - a string made inside an expression and held by nothing (`a + "/"` in
# `a + "/" + b`, a template's pieces, a number's text, a side only compared) is freed as soon as it
# has been used, and one that IS held - in a variable, handed to a function, a function's result -
# is not. Prints 1 2 3 4 5 6 7 and `grew 0` (the heap is read on the Mac; 0 elsewhere).
program StringTemps {
function keep(s: string) -> string { return s }
function tag(i: int) -> string { return `t{i}` }
# every shape of throwaway text, compared against itself built another way: nothing survives
function round(i: int, a: string, b: string) -> int {
var bad = 0
if a + "/" + b + "/" + string(i) != `{a}/{b}/{i}` { bad += 1 }
if `{i}-{i * 2}-{long(i) * 1000000000}` != string(i) + "-" + string(i * 2) + "-" + string(long(i) * 1000000000) { bad += 1 }
return bad
}
entry {
let a = "dir"
let b = "file"
# held intermediates stay good after the `+` that read them
let mid = a + "/"
let full = mid + b
if mid == "dir/" and full == "dir/file" { print(1) }
let n = string(-42)
let t = n + "!" + n
if n == "-42" and t == "-42!-42" { print(2) }
let k = keep(a + "+" + b)
let k2 = k + "=" + k
if k == "dir+file" and k2 == "dir+file=dir+file" { print(3) }
var acc = ""
for i in 0 .. 4 { acc += string(i) + "," }
if acc == "0,1,2,3," { print(4) }
if string(0) == "0" and string(2147483647) == "2147483647" and `{long(9000000000)}` == "9000000000" { print(5) }
let nested = `a{`b{1 + 1}c`}d`
# a function's result is its caller's to keep (keep() hands back its own argument), so only
# the text built round it goes
if nested == "ab2cd" and "[" + tag(3) + "]" == "[t3]" { print(6) }
var bad = 0
for i in 0 .. 200 { bad += round(i, a, b) }
if bad == 0 { print(7) }
let before = Vk.heap_bytes()
for i in 0 .. 20000 { bad += round(i, a, b) }
let grew = Vk.heap_bytes() - before
if bad == 0 { print(`grew {grew}`) } else { print(`bad {bad}`) }
}
}

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

View file

@ -1079,6 +1079,7 @@ function cmd_dev_test() -> int {
reject_case("rejected/private_generic", "grow is private to module kit", "an instance of a private generic is private to its module")
feat_case("lang/long_literals", "", "4294967295 3988292384 -4294967296 1099511627776 -3750763034362895579 -1 1", "long_literals.ludic (a decimal literal past 2^31 - 1, and a hex literal of more than eight digits, is a long)")
reject_case("rejected/long_into_int", "4294967295 does not fit one; it is a long", "a long literal given to an int is refused, not wrapped")
feat_case("lang/string_temps", "", "1 2 3 4 5 6 7 grew 0", "string_temps.ludic (a concatenation's pieces, a template's holes and a compared side are freed once used; a kept one is not)")
feat_case("lang/nested_templates", "", "outer [inner 3 {x}] \"`}\" end", "nested_templates.ludic (a template literal inside another's hole)")
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")
reject_case("rejected/alias_arity", "this call to Trail.length leaves out to, which has no default", "an alias's arguments are checked against its target")