patx/relay-lang

improved middlewares and sessions

Commit 8cc2eb3 · patx · 2026-02-11T00:51:41-05:00

Changeset
8cc2eb3933f93c78cc173ce48dfea147b3e784a3
Parents
afd82cab1f69db300cdad4d64248422c90bd3e6e

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/src/main.rs b/src/main.rs
index 8f9fb75..5562608 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -77,11 +77,11 @@ enum Stmt {
         module: String,
     },
     Assign {
-        name: String,
+        target: Expr,
         expr: Expr,
     },
     AugAssign {
-        name: String,
+        target: Expr,
         op: AugOp,
         expr: Expr,
     },
@@ -728,33 +728,40 @@ impl Parser {
             return self.parse_destructure_assign();
         }
 
-        // assignment
-        if let Some(name) = self.peek_ident_string() {
-            if self.peek_n_is(1, &Tok::Assign)
-                || self.peek_n_is(1, &Tok::AugAdd)
-                || self.peek_n_is(1, &Tok::AugSub)
-            {
-                self.bump(); // ident
-                let op = self.bump().kind.clone();
-                let expr = self.parse_expr()?;
-                return Ok(match op {
-                    Tok::Assign => Stmt::Assign { name, expr },
-                    Tok::AugAdd => Stmt::AugAssign {
-                        name,
-                        op: AugOp::Add,
-                        expr,
-                    },
-                    Tok::AugSub => Stmt::AugAssign {
-                        name,
-                        op: AugOp::Sub,
-                        expr,
-                    },
-                    _ => unreachable!(),
-                });
+        let target = self.parse_expr()?;
+        if self.peek_is(&Tok::Assign) || self.peek_is(&Tok::AugAdd) || self.peek_is(&Tok::AugSub)
+        {
+            if !Self::is_assignment_target(&target) {
+                return Err(self.err_here("Invalid assignment target"));
             }
+            let op = self.bump().kind.clone();
+            let expr = self.parse_expr()?;
+            return Ok(match op {
+                Tok::Assign => Stmt::Assign { target, expr },
+                Tok::AugAdd => Stmt::AugAssign {
+                    target,
+                    op: AugOp::Add,
+                    expr,
+                },
+                Tok::AugSub => Stmt::AugAssign {
+                    target,
+                    op: AugOp::Sub,
+                    expr,
+                },
+                _ => unreachable!(),
+            });
         }
 
-        Ok(Stmt::Expr(self.parse_expr()?))
+        Ok(Stmt::Expr(target))
+    }
+
+    fn is_assignment_target(expr: &Expr) -> bool {
+        match expr {
+            Expr::Ident(_) => true,
+            Expr::Member { object, .. } => Self::is_assignment_target(object),
+            Expr::Index { target, .. } => Self::is_assignment_target(target),
+            _ => false,
+        }
     }
 
     fn parse_decorator(&mut self) -> RResult<Decorator> {
@@ -1696,30 +1703,22 @@ impl Evaluator {
                 self.import_module(module).await?;
                 Ok(Flow::None)
             }
-            Stmt::Assign { name, expr } => {
+            Stmt::Assign { target, expr } => {
                 let v = self.eval_expr(expr).await?;
-                let mut env = self.env.lock().await;
-                if !env.assign_existing(name, v.clone()) {
-                    env.set(name, v);
-                }
+                self.assign_to_expr(target, v).await?;
                 Ok(Flow::None)
             }
-            Stmt::AugAssign { name, op, expr } => {
+            Stmt::AugAssign { target, op, expr } => {
                 let rhs = self
                     .resolve_deferred_only(self.eval_expr(expr).await?)
                     .await?;
-                let env = self.env.lock().await;
-                let cur = env.get(name)?;
-                drop(env);
+                let cur = self.eval_expr(target).await?;
                 let cur = self.resolve_if_needed(cur).await?;
                 let out = match op {
                     AugOp::Add => bin_add(cur, rhs)?,
                     AugOp::Sub => bin_sub(cur, rhs)?,
                 };
-                let mut env = self.env.lock().await;
-                if !env.assign_existing(name, out.clone()) {
-                    env.set(name, out);
-                }
+                self.assign_to_expr(target, out).await?;
                 Ok(Flow::None)
             }
             Stmt::DestructureAssign { targets, expr } => {
@@ -1855,6 +1854,35 @@ impl Evaluator {
         }
     }
 
+    #[async_recursion]
+    async fn assign_to_expr(&self, target_expr: &Expr, value: Value) -> RResult<()> {
+        match target_expr {
+            Expr::Ident(name) => {
+                let mut env = self.env.lock().await;
+                if !env.assign_existing(name, value.clone()) {
+                    env.set(name, value);
+                }
+                Ok(())
+            }
+            Expr::Member { object, name } => {
+                let object_value = self
+                    .resolve_if_needed(self.eval_expr(object).await?)
+                    .await?;
+                let updated = assign_member_value(object_value, name, value)?;
+                self.assign_to_expr(object, updated).await
+            }
+            Expr::Index { target, index } => {
+                let container = self
+                    .resolve_if_needed(self.eval_expr(target).await?)
+                    .await?;
+                let idx = self.resolve_if_needed(self.eval_expr(index).await?).await?;
+                let updated = assign_index_value(container, idx, value)?;
+                self.assign_to_expr(target, updated).await
+            }
+            _ => Err(RelayError::Type("Invalid assignment target".into())),
+        }
+    }
+
     #[async_recursion]
     async fn eval_expr(&self, e: &Expr) -> RResult<Value> {
         match e {
@@ -1956,14 +1984,10 @@ impl Evaluator {
                         .nth(i as usize)
                         .map(|c| Value::Str(c.to_string()))
                         .ok_or_else(|| RelayError::Runtime("Index out of range".into())),
-                    (Value::Dict(m), k) => m
-                        .get(&k.repr())
-                        .cloned()
-                        .ok_or_else(|| RelayError::Runtime("Key not found".into())),
-                    (Value::Json(j), Value::Str(k)) => Ok(match j.get(&k) {
-                        Some(v) => Value::Json(v.clone()),
-                        None => return Err(RelayError::Runtime("Key not found".into())),
-                    }),
+                    (Value::Dict(m), k) => Ok(m.get(&k.repr()).cloned().unwrap_or(Value::None)),
+                    (Value::Json(j), Value::Str(k)) => {
+                        Ok(j.get(&k).cloned().map(Value::Json).unwrap_or(Value::None))
+                    }
                     _ => Err(RelayError::Type(
                         "Indexing expects list/string/dict/json".into(),
                     )),
@@ -2307,6 +2331,54 @@ fn cmp_num(v: &Value) -> RResult<f64> {
         _ => Err(RelayError::Type("Comparison expects numbers".into())),
     }
 }
+
+fn assign_member_value(object: Value, name: &str, value: Value) -> RResult<Value> {
+    match object {
+        Value::Dict(mut map) => {
+            map.insert(name.to_string(), value);
+            Ok(Value::Dict(map))
+        }
+        Value::Json(mut j) => {
+            let obj = j.as_object_mut().ok_or_else(|| {
+                RelayError::Type("Member assignment on JSON expects object value".into())
+            })?;
+            obj.insert(name.to_string(), value_to_json(&value));
+            Ok(Value::Json(j))
+        }
+        _ => Err(RelayError::Type(
+            "Member assignment expects dict or JSON object".into(),
+        )),
+    }
+}
+
+fn assign_index_value(container: Value, index: Value, value: Value) -> RResult<Value> {
+    match (container, index) {
+        (Value::List(mut values), Value::Int(i)) => {
+            let idx = usize::try_from(i)
+                .map_err(|_| RelayError::Runtime("Index out of range".into()))?;
+            let slot = values
+                .get_mut(idx)
+                .ok_or_else(|| RelayError::Runtime("Index out of range".into()))?;
+            *slot = value;
+            Ok(Value::List(values))
+        }
+        (Value::Dict(mut map), key) => {
+            map.insert(key.repr(), value);
+            Ok(Value::Dict(map))
+        }
+        (Value::Json(mut j), Value::Str(key)) => {
+            let obj = j.as_object_mut().ok_or_else(|| {
+                RelayError::Type("JSON indexing expects object value".into())
+            })?;
+            obj.insert(key, value_to_json(&value));
+            Ok(Value::Json(j))
+        }
+        _ => Err(RelayError::Type(
+            "Indexed assignment expects list/dict/json".into(),
+        )),
+    }
+}
+
 fn bin_add(l: Value, r: Value) -> RResult<Value> {
     match (l, r) {
         (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a + b)),
@@ -4397,4 +4469,50 @@ mod tests {
             RelayError::Syntax { msg, .. } if msg.contains("without matching if")
         ));
     }
+
+    #[tokio::test]
+    async fn supports_index_assignment_and_missing_dict_keys() {
+        let src = r#"numbers = [1, 2, 3]
+numbers[0] = 10
+session = {}
+session["user"] = "alice"
+is_missing_none = (session["missing"] == None)
+"#;
+
+        let program = parse_src(src).expect("program should parse");
+        let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
+        let evaluator = Arc::new(Evaluator::new(env.clone()));
+        {
+            let mut env_lock = env.lock().await;
+            install_stdlib(&mut env_lock, evaluator.clone()).expect("stdlib install should work");
+        }
+
+        evaluator
+            .eval_program(&program)
+            .await
+            .expect("program should evaluate");
+
+        let env_lock = env.lock().await;
+
+        let numbers = env_lock.get("numbers").expect("numbers should exist");
+        match numbers {
+            Value::List(values) => {
+                assert!(matches!(values.first(), Some(Value::Int(10))));
+            }
+            _ => panic!("numbers should be a list"),
+        }
+
+        let session = env_lock.get("session").expect("session should exist");
+        match session {
+            Value::Dict(map) => {
+                assert!(matches!(map.get("user"), Some(Value::Str(s)) if s == "alice"));
+            }
+            _ => panic!("session should be a dict"),
+        }
+
+        let missing = env_lock
+            .get("is_missing_none")
+            .expect("is_missing_none should exist");
+        assert!(matches!(missing, Value::Bool(true)));
+    }
 }
diff --git a/test.ry b/test.ry
index 3c073bb..e5873a8 100644
--- a/test.ry
+++ b/test.ry
@@ -1,2 +1,19 @@
-content = read_file("test.ry")
-print(content)
+app = WebApp()
+server = WebServer()
+
[email protected]("/login")
+fn login(user: str = "guest")
+    session["user"] = user
+    return app.redirect("/u/" + user)
+
+fn auth()
+    if (request.path == "/u/<user>" && session["user"] == None)
+        return app.redirect("/login")
+
+app.use(auth)
+
[email protected]("/u/<user>")
+fn index(user: str)
+    return "Hello, {{ user }}"
+
+server.run(app)