fix(tvix/eval): thunk string interpolation
If we have multiple string parts, we need to thunk assembling the string. If we have a single literal, it is strict (like all literals), but a single interpolation part may compile to a thunk, depending on how the expression inside is compiled – we can avoid forcing to early here compared to the previous behavior. Note that this CL retains the bug that `"${x}"` is erroneously translated to `x`, implying e.g. `"${12}" == 12`. The use of `parts.len()` is unproblematic, since normalized_parts() builds a `Vec` instead of returning an iterator. Change-Id: I3aecbfefef65cc627b1b8a65be27cbaeada3582b Reviewed-on: https://cl.tvl.fyi/c/depot/+/6580 Autosubmit: sterni <sternenseemann@systemli.org> Reviewed-by: tazjin <tazjin@tvl.su> Tested-by: BuildkiteCI
This commit is contained in:
parent
b570da18d6
commit
bcd7e520f0
3 changed files with 40 additions and 18 deletions
|
@ -258,34 +258,48 @@ impl Compiler<'_, '_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_str(&mut self, slot: LocalIdx, node: ast::Str) {
|
fn compile_str(&mut self, slot: LocalIdx, node: ast::Str) {
|
||||||
// TODO: thunk string construction if it is not a literal
|
let parts = node.normalized_parts();
|
||||||
let mut count = 0;
|
let count = parts.len();
|
||||||
|
|
||||||
// The string parts are produced in literal order, however
|
if count != 1 {
|
||||||
// they need to be reversed on the stack in order to
|
self.thunk(slot, &node, |c, n, s| {
|
||||||
// efficiently create the real string in case of
|
// The string parts are produced in literal order, however
|
||||||
// interpolation.
|
// they need to be reversed on the stack in order to
|
||||||
for part in node.normalized_parts().into_iter().rev() {
|
// efficiently create the real string in case of
|
||||||
count += 1;
|
// interpolation.
|
||||||
|
for part in parts.into_iter().rev() {
|
||||||
|
match part {
|
||||||
|
// Interpolated expressions are compiled as normal and
|
||||||
|
// dealt with by the VM before being assembled into
|
||||||
|
// the final string. We need to force them here,
|
||||||
|
// so OpInterpolate definitely has a string to consume.
|
||||||
|
// TODO(sterni): coerce to string
|
||||||
|
ast::InterpolPart::Interpolation(ipol) => {
|
||||||
|
c.compile(s, ipol.expr().unwrap());
|
||||||
|
c.emit_force(&ipol);
|
||||||
|
}
|
||||||
|
|
||||||
match part {
|
ast::InterpolPart::Literal(lit) => {
|
||||||
// Interpolated expressions are compiled as normal and
|
c.emit_constant(Value::String(lit.into()), n);
|
||||||
// dealt with by the VM before being assembled into
|
}
|
||||||
// the final string.
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.push_op(OpCode::OpInterpolate(Count(count)), n);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
match &parts[0] {
|
||||||
|
// Since we only have a single part, it is okay if this yields a thunk
|
||||||
|
// TODO(sterni): coerce to string
|
||||||
ast::InterpolPart::Interpolation(node) => {
|
ast::InterpolPart::Interpolation(node) => {
|
||||||
self.compile(slot, node.expr().unwrap());
|
self.compile(slot, node.expr().unwrap());
|
||||||
self.emit_force(&node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ast::InterpolPart::Literal(lit) => {
|
ast::InterpolPart::Literal(lit) => {
|
||||||
self.emit_constant(Value::String(lit.into()), &node);
|
self.emit_constant(Value::String(lit.as_str().into()), &node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if count != 1 {
|
|
||||||
self.push_op(OpCode::OpInterpolate(Count(count)), &node);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_unary_op(&mut self, slot: LocalIdx, op: ast::UnaryOp) {
|
fn compile_unary_op(&mut self, slot: LocalIdx, op: ast::UnaryOp) {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"strict literal"
|
|
@ -0,0 +1,7 @@
|
||||||
|
let
|
||||||
|
final = { text = "strict literal"; inherit x y; };
|
||||||
|
x = "lazy ${throw "interpolation"}";
|
||||||
|
y = "${throw "also lazy!"}";
|
||||||
|
in
|
||||||
|
|
||||||
|
final.text
|
Loading…
Reference in a new issue