feat(tvix/eval): implement upvalue resolution in with scopes

These need to be handled specially by the runtime if the compiler
determines that a given local must be resolved via `with`.

Note that this implementation has a bug: It currently allows `with`
inside of nested lambdas to shadow statically known identifiers. This
will be cleaned up in the next commit.

Change-Id: If196b99cbd1a0f2dbb4a40a0e88cdb09a009c6b9
Reviewed-on: https://cl.tvl.fyi/c/depot/+/6299
Tested-by: BuildkiteCI
Reviewed-by: sterni <sternenseemann@systemli.org>
This commit is contained in:
Vincent Ambo 2022-08-27 03:31:28 +03:00 committed by tazjin
parent 10b0879c00
commit 33cde1422e
7 changed files with 47 additions and 2 deletions

View file

@ -16,6 +16,7 @@
use path_clean::PathClean;
use rnix::ast::{self, AstToken, HasEntry};
use rowan::ast::AstNode;
use smol_str::SmolStr;
use std::collections::{hash_map, HashMap};
use std::path::{Path, PathBuf};
use std::rc::Rc;
@ -68,6 +69,10 @@ enum Upvalue {
/// This upvalue captures an enclosing upvalue.
Upvalue(UpvalueIdx),
/// This upvalue captures a dynamically resolved value (i.e.
/// `with`).
Dynamic(SmolStr),
}
/// Represents a scope known during compilation, which can be resolved
@ -875,6 +880,10 @@ impl Compiler {
match upvalue {
Upvalue::Stack(idx) => self.chunk().push_op(OpCode::DataLocalIdx(idx)),
Upvalue::Upvalue(idx) => self.chunk().push_op(OpCode::DataUpvalueIdx(idx)),
Upvalue::Dynamic(s) => {
let idx = self.chunk().push_constant(Value::String(s.into()));
self.chunk().push_op(OpCode::DataDynamicIdx(idx))
}
};
}
}
@ -1008,11 +1017,18 @@ impl Compiler {
return None;
}
// Determine whether the upvalue is a local in the enclosing context.
if let Some(idx) = self.contexts[ctx_idx - 1].scope.resolve_local(name) {
return Some(self.add_upvalue(ctx_idx, Upvalue::Stack(idx)));
}
// If the upvalue comes from an enclosing context, we need to
// Determine whether the upvalue is a dynamic variable in the
// enclosing context.
if !self.contexts[ctx_idx - 1].scope.with_stack.is_empty() {
return Some(self.add_upvalue(ctx_idx, Upvalue::Dynamic(SmolStr::new(name))));
}
// If the upvalue comes from even further up, we need to
// recurse to make sure that the upvalues are created at each
// level.
if let Some(idx) = self.resolve_upvalue(ctx_idx - 1, name) {

View file

@ -115,4 +115,5 @@ pub enum OpCode {
// according to the count.
DataLocalIdx(StackIdx),
DataUpvalueIdx(UpvalueIdx),
DataDynamicIdx(ConstantIdx),
}

View file

@ -0,0 +1 @@
150

View file

@ -0,0 +1,5 @@
# Upvalues from `with` require special runtime handling. Do they work?
let
f = with { a = 15; }; n: n * a;
in
f 10

View file

@ -80,6 +80,17 @@ impl Value {
}
}
pub fn as_str(&self) -> EvalResult<&str> {
match self {
Value::String(s) => Ok(s.as_str()),
other => Err(ErrorKind::TypeError {
expected: "string",
actual: other.type_of(),
}
.into()),
}
}
pub fn to_string(self) -> EvalResult<NixString> {
match self {
Value::String(s) => Ok(s),

View file

@ -46,6 +46,12 @@ impl From<String> for NixString {
}
}
impl From<SmolStr> for NixString {
fn from(s: SmolStr) -> Self {
NixString(StringRepr::Smol(s))
}
}
impl Hash for NixString {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.as_str().hash(state)

View file

@ -379,6 +379,11 @@ impl VM {
.push(self.frame().closure.upvalues[upv_idx].clone());
}
OpCode::DataDynamicIdx(ident_idx) => {
let ident = self.chunk().constant(ident_idx).as_str()?;
closure.upvalues.push(self.resolve_with(ident)?);
}
_ => panic!("compiler error: missing closure operand"),
}
}
@ -388,7 +393,7 @@ impl VM {
// Data-carrying operands should never be executed,
// that is a critical error in the VM.
OpCode::DataLocalIdx(_) | OpCode::DataUpvalueIdx(_) => {
OpCode::DataLocalIdx(_) | OpCode::DataUpvalueIdx(_) | OpCode::DataDynamicIdx(_) => {
panic!("VM bug: attempted to execute data-carrying operand")
}
}