Merge branch 'lang/str-temps' into lang/uifree
This commit is contained in:
commit
d9c574f107
10 changed files with 56019 additions and 48735 deletions
9
changes/string-temporaries.md
Normal file
9
changes/string-temporaries.md
Normal 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`).
|
||||
46
examples/lang/string_temps.ludic
Normal file
46
examples/lang/string_temps.ludic
Normal 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}`) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
52330
selfhost/ludicc.seed.ll
52330
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue