about summary refs log tree commit diff
path: root/tvix/cli/src/assignment.rs
diff options
context:
space:
mode:
authorAspen Smith <root@gws.fyi>2024-07-05T03·46-0400
committerclbot <clbot@tvl.fyi>2024-07-05T16·40+0000
commitfc63594631590547c9a31001806095f2e079a20e (patch)
treebed08e994d56fb224a62be6ab371f9cd1fec17e6 /tvix/cli/src/assignment.rs
parentac3d717944412b17d7dcd18006c2f9f522b1b3f7 (diff)
feat(tvix/repl): Allow binding variables at the top-level r/8346
Allow binding variables at the REPL's toplevel in the same way the Nix
REPL does, using the syntax <ident> = <expr>. This fully, strictly
evaluates the value and sets it in the repl's "env", which gets passed
in at the toplevel when evaluating expressions.

The laziness behavior differs from Nix's, but I think this is good:

    ❯ nix repl
    Welcome to Nix version 2.3.18. Type :? for help.

    nix-repl> x = builtins.trace "x" 1

    nix-repl> x
    trace: x
    1

    nix-repl> x
    1

vs tvix:

    tvix-repl> x = builtins.trace "x" 1
    trace: "x" :: string
    tvix-repl> x
    => 1 :: int
    tvix-repl> x
    => 1 :: int

Bug: https://b.tvl.fyi/issues/371
Change-Id: Ieb2d626b7195fa87be638c9a4dae2eee45eb9ab1
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11954
Reviewed-by: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
Autosubmit: aspen <root@gws.fyi>
Diffstat (limited to 'tvix/cli/src/assignment.rs')
-rw-r--r--tvix/cli/src/assignment.rs74
1 files changed, 74 insertions, 0 deletions
diff --git a/tvix/cli/src/assignment.rs b/tvix/cli/src/assignment.rs
new file mode 100644
index 000000000000..6fd9725d2956
--- /dev/null
+++ b/tvix/cli/src/assignment.rs
@@ -0,0 +1,74 @@
+use rnix::{Root, SyntaxKind, SyntaxNode};
+use rowan::ast::AstNode;
+
+/// An assignment of an identifier to a value in the context of a REPL.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct Assignment<'a> {
+    pub(crate) ident: &'a str,
+    pub(crate) value: rnix::ast::Expr,
+}
+
+impl<'a> Assignment<'a> {
+    /// Try to parse an [`Assignment`] from the given input string.
+    ///
+    /// Returns [`None`] if the parsing fails for any reason, since the intent is for us to
+    /// fall-back to trying to parse the input as a regular expression or other REPL commands for
+    /// any reason, since the intent is for us to fall-back to trying to parse the input as a
+    /// regular expression or other REPL command.
+    pub fn parse(input: &'a str) -> Option<Self> {
+        let mut tt = rnix::tokenizer::Tokenizer::new(input);
+        macro_rules! next {
+            ($kind:ident) => {{
+                loop {
+                    let (kind, tok) = tt.next()?;
+                    if kind == SyntaxKind::TOKEN_WHITESPACE {
+                        continue;
+                    }
+                    if kind != SyntaxKind::$kind {
+                        return None;
+                    }
+                    break tok;
+                }
+            }};
+        }
+
+        let ident = next!(TOKEN_IDENT);
+        let _equal = next!(TOKEN_ASSIGN);
+        let (green, errs) = rnix::parser::parse(tt);
+        let value = Root::cast(SyntaxNode::new_root(green))?.expr()?;
+
+        if !errs.is_empty() {
+            return None;
+        }
+
+        Some(Self { ident, value })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn simple_assignments() {
+        for input in ["x = 4", "x     =       \t\t\n\t4", "x=4"] {
+            let res = Assignment::parse(input).unwrap();
+            assert_eq!(res.ident, "x");
+            assert_eq!(res.value.to_string(), "4");
+        }
+    }
+
+    #[test]
+    fn complex_exprs() {
+        let input = "x = { y = 4; z = let q = 7; in [ q (y // { z = 9; }) ]; }";
+        let res = Assignment::parse(input).unwrap();
+        assert_eq!(res.ident, "x");
+    }
+
+    #[test]
+    fn not_an_assignment() {
+        let input = "{ x = 4; }";
+        let res = Assignment::parse(input);
+        assert!(res.is_none(), "{input:?}");
+    }
+}