Merge Phase 7d: string interpolation
This commit is contained in:
commit
311042e5ab
13 changed files with 4748 additions and 3962 deletions
16
LANGUAGE.md
16
LANGUAGE.md
|
|
@ -584,7 +584,21 @@ are **`& | ^ << >> ~`** (`>>` is a logical/unsigned shift).
|
|||
**Strings are values.** `a + b` concatenates two strings, and `a == b` / `a != b`
|
||||
compare them **by content** (not by pointer). `"go" + dir == "goleft"` works as
|
||||
written. (Under the hood these call a small emitted string runtime; a `==`/`!=`
|
||||
against `null` is still a pointer test.) `expr with { field: … }`
|
||||
against `null` is still a pointer test.)
|
||||
|
||||
**Interpolation is the readable way to build them.** A backtick string
|
||||
`` `text {expr} text` `` embeds any expression in `{…}` — numbers, bools and
|
||||
`fixed` values become text automatically, strings pass through — and desugars to
|
||||
the `+` chain above:
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — illustrative interpolation
|
||||
let msg = `hello {name}, you have {count + 1} messages`
|
||||
# == "hello " + name + ", you have " + str(count + 1) + " messages"
|
||||
```
|
||||
|
||||
`str(x)` is the same conversion on its own. Write a literal brace as `{{` / `}}`.
|
||||
`expr with { field: … }`
|
||||
is not implemented; records appear only in `spawn`. Char literals (`'w'`) are
|
||||
`int` code points; colors are hex ints (`0xff8800`). `null` is the null-pointer
|
||||
literal; test any pointer/record/slice with `x == null` / `x != null` (an unset
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# strings.ludic — strings are values: compare with ==/!=, join with +.
|
||||
# Running it prints: 1 2 3 4 5
|
||||
# strings.ludic — strings are values: compare with ==/!=, join with +, or (best)
|
||||
# interpolate with `backticks {expr}`. Running it prints: 1 2 3 4 5 6 7
|
||||
program Strings {
|
||||
entry {
|
||||
let greeting = "hello"
|
||||
|
|
@ -17,5 +17,10 @@ program Strings {
|
|||
if line == "ababab" { print_int(4) }
|
||||
|
||||
if (greeting + who) != greeting { print_int(5) }
|
||||
|
||||
# interpolation: `{expr}` embeds any expression (numbers become text)
|
||||
let n = 42
|
||||
if `{greeting}, {who}!` == "hello, Ludic!" { print_int(6) }
|
||||
if `n={n}, next={n + 1}` == "n=42, next=43" { print_int(7) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ var loc_ty: []ptr
|
|||
var loc_mut: []int # 1 = mutable (var / param / loop-var), 0 = immutable (let)
|
||||
var nloc: int = 0
|
||||
var g_uses_str: bool = false # a `str + str` / `str == str` was emitted -> emit the prelude
|
||||
var g_uses_intstr: bool = false # `str(int)` was emitted -> emit the int->string prelude
|
||||
|
||||
# loop targets for break/continue (innermost last)
|
||||
var brk_lbl: []ptr
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ fn emit_program() -> void {
|
|||
head = buf_new()
|
||||
code = buf_new()
|
||||
g_uses_str = false
|
||||
g_uses_intstr = false
|
||||
loc_name = new []ptr; loc_reg = new []ptr; loc_ty = new []ptr; loc_mut = new []int
|
||||
brk_lbl = new []ptr; cnt_lbl = new []ptr
|
||||
self_stk = new []ptr
|
||||
|
|
@ -84,6 +85,7 @@ fn emit_program() -> void {
|
|||
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
|
||||
}
|
||||
if g_uses_str { emit_str_prelude() } # @fn_str_eq / @fn_str_concat, after all uses are seen
|
||||
if g_uses_intstr { emit_int_str() } # @fn_int_str, for str(int) in interpolation
|
||||
}
|
||||
|
||||
# Flush the emitted IR. With a null path it goes to stdout (the pipe the shell
|
||||
|
|
|
|||
|
|
@ -126,6 +126,12 @@ fn emit_call(e: Node) -> Val {
|
|||
if (name == "quit") { emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
|
||||
if (name == "len") { return emit_len(e) }
|
||||
if (name == "push") { return emit_push(e) }
|
||||
if (name == "str") { # str(x): int/bool/fixed -> text, a string passes through
|
||||
let a = emit_expr(e.kids[0])
|
||||
if (llty(a.ty) == "ptr") { return a }
|
||||
g_uses_intstr = true
|
||||
return val(emit_bind(("call ptr @fn_int_str(i32 " + (a.code + ")"))), "str")
|
||||
}
|
||||
if (name == "fx") { let a = emit_expr(e.kids[0]); return val(emit_bind(("shl i32 " + (a.code + ", 16"))), "fixed") }
|
||||
if (name == "flr") { let a = emit_expr(e.kids[0]); return val(emit_bind(("ashr i32 " + (a.code + ", 16"))), "int") }
|
||||
if is_intrinsic(name) { return emit_intrinsic(name, e) }
|
||||
|
|
|
|||
|
|
@ -151,3 +151,26 @@ fn emit_str_prelude() -> void {
|
|||
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 str(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.
|
||||
fn emit_int_str() -> void {
|
||||
emith("define ptr @fn_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 %zp = getelementptr inbounds i8, ptr %buf, i32 22\n store i8 48, ptr %zp\n ret ptr %zp\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 ret ptr %res\n}\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const TK_OP: int = 3
|
|||
const TK_NL: int = 4
|
||||
const TK_EOF: int = 5
|
||||
const TK_FLOAT: int = 6
|
||||
const TK_INTERP: int = 7 # `text {expr} text` — raw content, split by the parser
|
||||
|
||||
property Tok { kind: int = 0, text: ptr = null, ival: int = 0, line: int = 0 }
|
||||
|
||||
|
|
@ -69,6 +70,16 @@ fn lex(src: ptr) -> void {
|
|||
tok_push(TK_STR, out, 0, line)
|
||||
continue
|
||||
}
|
||||
if c == 96 { # `interpolated string` — captured raw
|
||||
i = i + 1
|
||||
let out = mem_alloc(n)
|
||||
var j = 0
|
||||
while i < n and peek8(src, i) != 96 { poke8(out, j, peek8(src, i)); j = j + 1; i = i + 1 }
|
||||
i = i + 1
|
||||
poke8(out, j, 0)
|
||||
tok_push(TK_INTERP, out, 0, line)
|
||||
continue
|
||||
}
|
||||
if c == 39 { # 'c' char literal -> int
|
||||
i = i + 1
|
||||
var v = 0
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -54,8 +54,64 @@ fn args_call(call: Node) -> void {
|
|||
eat_op(")")
|
||||
}
|
||||
|
||||
# ---- string interpolation --------------------------------------------------
|
||||
# `text {expr} text` desugars to a `+` chain of string literals and `str(expr)`
|
||||
# holes, so it reuses the string-concat operator and needs no new runtime.
|
||||
fn interp_lit(buf: ptr, len: int) -> Node { let n = node(E_STR); n.s = substr(buf, 0, len); return n }
|
||||
fn interp_add(acc: Node, part: Node) -> Node {
|
||||
if acc == null { return part }
|
||||
return mkbin("+", acc, part)
|
||||
}
|
||||
fn interp_str(e: Node) -> Node { # wrap a hole in str(...)
|
||||
let c = node(E_CALL); let id = node(E_ID); id.s = "str"; c.a = id; push(c.kids, e); return c
|
||||
}
|
||||
fn parse_hole(inner: ptr) -> Node { # re-lex+parse an embedded expression
|
||||
let saved_toks = toks; let saved_pi = pi
|
||||
lex(inner); pi = 0; skipnl()
|
||||
let e = expr()
|
||||
toks = saved_toks; pi = saved_pi
|
||||
return e
|
||||
}
|
||||
fn parse_interp(raw: ptr) -> Node {
|
||||
let n = slen(raw)
|
||||
var acc: Node = null
|
||||
let lit = mem_alloc(n + 1)
|
||||
var lj = 0
|
||||
var i = 0
|
||||
while i < n {
|
||||
let c = peek8(raw, i)
|
||||
if c == 123 { # '{'
|
||||
if peek8(raw, i + 1) == 123 { poke8(lit, lj, 123); lj = lj + 1; i = i + 2; continue } # {{ -> {
|
||||
if lj > 0 { acc = interp_add(acc, interp_lit(lit, lj)); lj = 0 }
|
||||
i = i + 1
|
||||
let hs = i
|
||||
var depth = 1
|
||||
while i < n and depth > 0 {
|
||||
let d = peek8(raw, i)
|
||||
if d == 123 { depth = depth + 1 }
|
||||
else { if d == 125 { depth = depth - 1; if depth == 0 { break } } }
|
||||
i = i + 1
|
||||
}
|
||||
acc = interp_add(acc, interp_str(parse_hole(substr(raw, hs, i - hs))))
|
||||
i = i + 1 # skip the closing '}'
|
||||
} else {
|
||||
if c == 125 and peek8(raw, i + 1) == 125 { poke8(lit, lj, 125); lj = lj + 1; i = i + 2; continue } # }} -> }
|
||||
if c == 92 { # backslash escape in literal text
|
||||
let e = peek8(raw, i + 1); var r = e
|
||||
if e == 110 { r = 10 }
|
||||
if e == 116 { r = 9 }
|
||||
poke8(lit, lj, r); lj = lj + 1; i = i + 2; continue
|
||||
}
|
||||
poke8(lit, lj, c); lj = lj + 1; i = i + 1
|
||||
}
|
||||
}
|
||||
if lj > 0 or (acc == null) { acc = interp_add(acc, interp_lit(lit, lj)) }
|
||||
return acc
|
||||
}
|
||||
|
||||
fn p_primary() -> Node {
|
||||
let t = toks[pi]
|
||||
if t.kind == TK_INTERP { pi = pi + 1; return parse_interp(t.text) }
|
||||
if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; pi = pi + 1; return n }
|
||||
if t.kind == TK_FLOAT { let n = node(E_FLOAT); n.ival = t.ival; pi = pi + 1; return n }
|
||||
if t.kind == TK_STR { let n = node(E_STR); n.s = t.text; pi = pi + 1; return n }
|
||||
|
|
|
|||
4
test.sh
4
test.sh
|
|
@ -68,8 +68,8 @@ if ./selfhost/game-build.sh build/ludicc examples/toggle.ludic "/tmp/ludic_tog"
|
|||
else bad "toggle: $(tail -1 /tmp/tog.out)"; fi
|
||||
# String operators: ==/!= compare by content, + concatenates (str_* prelude).
|
||||
if ./selfhost/game-build.sh build/ludicc examples/strings.ludic "/tmp/ludic_str" >/tmp/str.out 2>&1 \
|
||||
&& [ "$(/tmp/ludic_str </dev/null | tr '\n' ' ')" = "1 2 3 4 5 " ]; then
|
||||
ok "strings.ludic (str ==/!=/+ operators)"
|
||||
&& [ "$(/tmp/ludic_str </dev/null | tr '\n' ' ')" = "1 2 3 4 5 6 7 " ]; then
|
||||
ok "strings.ludic (str ==/!=/+ and `interpolation`)"
|
||||
else bad "strings: $(tail -1 /tmp/str.out)"; fi
|
||||
# --- Toolchain-agent CLI smoke tests append below this line ---
|
||||
echo "== self-hosted front-end binaries (ludicc / ludic) =="
|
||||
|
|
|
|||
|
|
@ -37,6 +37,17 @@
|
|||
{
|
||||
"name": "string.quoted.single.ludic",
|
||||
"match": "'(\\\\.|[^'\\\\])'"
|
||||
},
|
||||
{
|
||||
"name": "string.interpolated.ludic",
|
||||
"begin": "`",
|
||||
"end": "`",
|
||||
"beginCaptures": { "0": { "name": "punctuation.definition.string.begin.ludic" } },
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.string.end.ludic" } },
|
||||
"patterns": [
|
||||
{ "name": "meta.embedded.ludic", "begin": "\\{", "end": "\\}", "patterns": [{ "include": "$self" }] },
|
||||
{ "name": "constant.character.escape.ludic", "match": "\\\\." }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -37,6 +37,17 @@
|
|||
{
|
||||
"name": "string.quoted.single.ludic",
|
||||
"match": "'(\\\\.|[^'\\\\])'"
|
||||
},
|
||||
{
|
||||
"name": "string.interpolated.ludic",
|
||||
"begin": "`",
|
||||
"end": "`",
|
||||
"beginCaptures": { "0": { "name": "punctuation.definition.string.begin.ludic" } },
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.string.end.ludic" } },
|
||||
"patterns": [
|
||||
{ "name": "meta.embedded.ludic", "begin": "\\{", "end": "\\}", "patterns": [{ "include": "$self" }] },
|
||||
{ "name": "constant.character.escape.ludic", "match": "\\\\." }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -227,6 +227,12 @@ static void lud_lex(LLex* L, const char* src){
|
|||
if (src[i] == '"') i++; else bad = 1;
|
||||
ltok_push(L, LT_STR, s, i, line, bad); continue;
|
||||
}
|
||||
if (c == '`'){ /* `interpolated {expr}` string */
|
||||
int s = i; i++; int bad = 0;
|
||||
while (src[i] && src[i] != '`'){ if (src[i] == '\\' && src[i+1]) i += 2; else i++; }
|
||||
if (src[i] == '`') i++; else bad = 1;
|
||||
ltok_push(L, LT_STR, s, i, line, bad); continue;
|
||||
}
|
||||
if (c == '\''){
|
||||
int s = i; i++; int bad = 0;
|
||||
if (src[i] == '\\' && src[i+1]) i += 2; else if (src[i] && src[i] != '\n') i++;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue