git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit7a67dadad88c9746d6da010659e235445c97d30a
parent5fe1ec14c8
authorLucas Galante <[email protected]>
date2026-09-24 10:15
fix: a code parameter never becomes an expression

A kernel or a wrangle script whose whole text happens to read as a
channel — a one-line `ch("../a/Radius")` — is a program, not a
reference; flagging it would evaluate the script to a number before it
ran. `ParamDef::takes_expressions` gates the template loader, the pane
write-back and set_param alike.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/app.rs    | 15 +++++++++++++--
 src/main.rs   | 25 +++++++++++++++++++++++++
 src/window.rs |  2 +-
 3 files changed, 39 insertions(+), 3 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 877837a..6f04064 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -117,6 +117,17 @@ pub struct ParamDef {
     pub expr: bool,
 }
 
+impl ParamDef {
+    /// Whether a value that READS as a reference should become an
+    /// expression here. Not for a code parameter: a kernel or a wrangle
+    /// script is a program, and one whose whole text happens to be
+    /// `ch("../a/Radius")` is a one-line program, not a channel — flagging
+    /// it would evaluate the script to a number before it ever ran.
+    pub fn takes_expressions(&self) -> bool {
+        !(self.param_type == "code" || self.name == "Code")
+    }
+}
+
 fn default_param_type() -> String { "string".to_string() }
 
 /// Expand a leading `~` to the home directory. A path typed into a text field
@@ -763,7 +774,7 @@ impl Project {
 /// one a typed or scripted value gets — arithmetic alone is not enough.
 pub fn infer_template_exprs(node: &mut FsNode) {
     for p in &mut node.params {
-        if !p.expr && crate::expr::looks_like_expression(&p.default) {
+        if !p.expr && p.takes_expressions() && crate::expr::looks_like_expression(&p.default) {
             p.expr = true;
         }
     }
@@ -2905,7 +2916,7 @@ impl State {
                                 // A reference typed into a plain row becomes
                                 // an expression — the one way to make one
                                 // without the row menu.
-                                if !p.expr && crate::expr::looks_like_expression(&p.default) {
+                                if !p.expr && p.takes_expressions() && crate::expr::looks_like_expression(&p.default) {
                                     p.expr = true;
                                 }
                                 if p.param_type == "button" && p.default == "clicked" {
diff --git a/src/main.rs b/src/main.rs
index 85caa19..429202d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6300,6 +6300,31 @@ mod tests {
         assert_eq!(state.current_dir().children[ball].params.iter().find(|p| p.name == "Rows").unwrap().default, "chi(\"../orb/Rows\") * 2");
     }
 
+    /// A code parameter never becomes an expression, however its text reads:
+    /// a one-line script that IS `ch("../a/Radius")` is a program to run,
+    /// and flagging it would evaluate it to a number first. Neither the
+    /// template loader nor a scripted set_param flags one.
+    #[test]
+    fn a_code_parameter_is_never_an_expression() {
+        use crate::app::{infer_template_exprs, McpAction};
+        let mut node = ref_node("w", "w1", "opencl", vec![("Code", "code", "ch(\"../a/Radius\")"), ("Radius", "slider", "ch(\"../a/Radius\")")], vec![]);
+        for p in &mut node.params {
+            p.expr = false;
+        }
+        infer_template_exprs(&mut node);
+        assert!(!node.params[0].expr, "the code stays a program");
+        assert!(node.params[1].expr, "the slider becomes an expression");
+
+        let mut state = State::new(false);
+        let mut redraw = false;
+        state.apply_action(McpAction::AddNode { template_name: "OpenCL".into(), name: Some("k".into()), x: 3.0, y: 9.0 }, &mut redraw).unwrap();
+        let k = state.current_dir().children.iter().position(|c| c.name == "k").unwrap();
+        state.apply_action(McpAction::SetParam { slot: k, name: "Code".into(), value: "chf(\"../sphere1/Radius\")".into() }, &mut redraw).unwrap();
+        let code = state.current_dir().children[k].params.iter().find(|p| p.name == "Code").unwrap();
+        assert!(!code.expr);
+        assert_eq!(code.param_type, "code");
+    }
+
     /// Inside the SECOND instance of a subnet, a child wired to a sibling by
     /// name finds its own sibling, not the first instance's.
     #[test]
diff --git a/src/window.rs b/src/window.rs
index 28a4599..2721d4b 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -723,7 +723,7 @@ impl State {
                         p.default = value;
                         // A value that reads as a reference becomes an
                         // expression, as one typed into the pane does.
-                        if !p.expr && crate::expr::looks_like_expression(&p.default) {
+                        if !p.expr && p.takes_expressions() && crate::expr::looks_like_expression(&p.default) {
                             p.expr = true;
                         }
                         // Same sequence as the interactive param-pane