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`
|
**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
|
compare them **by content** (not by pointer). `"go" + dir == "goleft"` works as
|
||||||
written. (Under the hood these call a small emitted string runtime; a `==`/`!=`
|
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
|
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
|
`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
|
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 +.
|
# strings.ludic — strings are values: compare with ==/!=, join with +, or (best)
|
||||||
# Running it prints: 1 2 3 4 5
|
# interpolate with `backticks {expr}`. Running it prints: 1 2 3 4 5 6 7
|
||||||
program Strings {
|
program Strings {
|
||||||
entry {
|
entry {
|
||||||
let greeting = "hello"
|
let greeting = "hello"
|
||||||
|
|
@ -17,5 +17,10 @@ program Strings {
|
||||||
if line == "ababab" { print_int(4) }
|
if line == "ababab" { print_int(4) }
|
||||||
|
|
||||||
if (greeting + who) != greeting { print_int(5) }
|
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 loc_mut: []int # 1 = mutable (var / param / loop-var), 0 = immutable (let)
|
||||||
var nloc: int = 0
|
var nloc: int = 0
|
||||||
var g_uses_str: bool = false # a `str + str` / `str == str` was emitted -> emit the prelude
|
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)
|
# loop targets for break/continue (innermost last)
|
||||||
var brk_lbl: []ptr
|
var brk_lbl: []ptr
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ fn emit_program() -> void {
|
||||||
head = buf_new()
|
head = buf_new()
|
||||||
code = buf_new()
|
code = buf_new()
|
||||||
g_uses_str = false
|
g_uses_str = false
|
||||||
|
g_uses_intstr = false
|
||||||
loc_name = new []ptr; loc_reg = new []ptr; loc_ty = new []ptr; loc_mut = new []int
|
loc_name = new []ptr; loc_reg = new []ptr; loc_ty = new []ptr; loc_mut = new []int
|
||||||
brk_lbl = new []ptr; cnt_lbl = new []ptr
|
brk_lbl = new []ptr; cnt_lbl = new []ptr
|
||||||
self_stk = 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 }
|
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_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
|
# 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 == "quit") { emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
|
||||||
if (name == "len") { return emit_len(e) }
|
if (name == "len") { return emit_len(e) }
|
||||||
if (name == "push") { return emit_push(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 == "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 (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) }
|
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(" %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")
|
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_NL: int = 4
|
||||||
const TK_EOF: int = 5
|
const TK_EOF: int = 5
|
||||||
const TK_FLOAT: int = 6
|
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 }
|
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)
|
tok_push(TK_STR, out, 0, line)
|
||||||
continue
|
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
|
if c == 39 { # 'c' char literal -> int
|
||||||
i = i + 1
|
i = i + 1
|
||||||
var v = 0
|
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(")")
|
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 {
|
fn p_primary() -> Node {
|
||||||
let t = toks[pi]
|
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_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_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 }
|
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
|
else bad "toggle: $(tail -1 /tmp/tog.out)"; fi
|
||||||
# String operators: ==/!= compare by content, + concatenates (str_* prelude).
|
# 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 \
|
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
|
&& [ "$(/tmp/ludic_str </dev/null | tr '\n' ' ')" = "1 2 3 4 5 6 7 " ]; then
|
||||||
ok "strings.ludic (str ==/!=/+ operators)"
|
ok "strings.ludic (str ==/!=/+ and `interpolation`)"
|
||||||
else bad "strings: $(tail -1 /tmp/str.out)"; fi
|
else bad "strings: $(tail -1 /tmp/str.out)"; fi
|
||||||
# --- Toolchain-agent CLI smoke tests append below this line ---
|
# --- Toolchain-agent CLI smoke tests append below this line ---
|
||||||
echo "== self-hosted front-end binaries (ludicc / ludic) =="
|
echo "== self-hosted front-end binaries (ludicc / ludic) =="
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,17 @@
|
||||||
{
|
{
|
||||||
"name": "string.quoted.single.ludic",
|
"name": "string.quoted.single.ludic",
|
||||||
"match": "'(\\\\.|[^'\\\\])'"
|
"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",
|
"name": "string.quoted.single.ludic",
|
||||||
"match": "'(\\\\.|[^'\\\\])'"
|
"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;
|
if (src[i] == '"') i++; else bad = 1;
|
||||||
ltok_push(L, LT_STR, s, i, line, bad); continue;
|
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 == '\''){
|
if (c == '\''){
|
||||||
int s = i; i++; int bad = 0;
|
int s = i; i++; int bad = 0;
|
||||||
if (src[i] == '\\' && src[i+1]) i += 2; else if (src[i] && src[i] != '\n') i++;
|
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