From 8ca8fda0090c10a52078fbf80ce782f908f2b92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Chav=C3=A3o?= Date: Tue, 7 Apr 2026 11:52:08 -0300 Subject: [PATCH 1/5] Implement runtime list object and builtin list commands --- src/runtime/object.rs | 24 +++ src/runtime/object/list_commands.rs | 262 ++++++++++++++++++++++++++++ src/runtime/object/list_object.rs | 92 ++++++++++ src/runtime/scope.rs | 10 +- 4 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 src/runtime/object/list_commands.rs create mode 100644 src/runtime/object/list_object.rs diff --git a/src/runtime/object.rs b/src/runtime/object.rs index 5334171..5940903 100644 --- a/src/runtime/object.rs +++ b/src/runtime/object.rs @@ -1,4 +1,6 @@ mod object_commands; +mod list_commands; +mod list_object; mod object_strategies; use std::any::Any; @@ -16,6 +18,8 @@ pub use object_commands::{ new_block, new_echo, new_if, new_input, new_message_async, new_message_else, new_message_run, new_stdout, new_while, }; +pub use list_commands::{new_list_get, new_list_len, new_list_pop, new_list_push, new_list_set}; +pub use list_object::new_list; pub use object_strategies::{ new_boolean_operator, new_logic_operator, new_node, new_number, new_operator, new_string, }; @@ -71,6 +75,26 @@ pub trait GrapholObject { None } + fn list_len(&self) -> Option { + None + } + + fn list_get(&self, _index: usize) -> Option { + None + } + + fn list_set(&mut self, _index: usize, _value: Value) -> bool { + false + } + + fn list_pop(&mut self) -> Option { + None + } + + fn list_push(&mut self, _value: Value) -> bool { + false + } + fn as_any(&self) -> &dyn Any; } diff --git a/src/runtime/object/list_commands.rs b/src/runtime/object/list_commands.rs new file mode 100644 index 0000000..9c898f3 --- /dev/null +++ b/src/runtime/object/list_commands.rs @@ -0,0 +1,262 @@ +use std::any::Any; + +use super::super::host::ExecutionHost; +use super::super::value::{ObjectRef, ScalarValue, Value}; +use super::list_object::list_target; +use super::{GrapholObject, object_ref}; + +pub fn new_list_push() -> ObjectRef { + object_ref(ListPushCommand { target: None }) +} + +pub fn new_list_pop() -> ObjectRef { + object_ref(ListPopCommand { + target: None, + result: Value::Null, + }) +} + +pub fn new_list_get() -> ObjectRef { + object_ref(ListGetCommand { + target: None, + index: None, + result: Value::Null, + }) +} + +pub fn new_list_set() -> ObjectRef { + object_ref(ListSetCommand { + target: None, + index: None, + has_index: false, + }) +} + +pub fn new_list_len() -> ObjectRef { + object_ref(ListLenCommand { + consumed_target: false, + result: 0, + }) +} + +struct ListPushCommand { + target: Option, +} + +impl GrapholObject for ListPushCommand { + fn receive(&mut self, value: Value, _host: &mut dyn ExecutionHost) { + if self.target.is_none() { + self.target = list_target(&value); + return; + } + + if let Some(target) = &self.target { + target.borrow_mut().list_push(value); + } + } + + fn end(&mut self) { + self.target = None; + } + + fn get_type(&self) -> &'static str { + "command" + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +struct ListPopCommand { + target: Option, + result: Value, +} + +impl GrapholObject for ListPopCommand { + fn receive(&mut self, value: Value, _host: &mut dyn ExecutionHost) { + if self.target.is_some() { + return; + } + + self.target = list_target(&value); + self.result = self + .target + .as_ref() + .and_then(|target| target.borrow_mut().list_pop()) + .unwrap_or(Value::Null); + } + + fn end(&mut self) { + self.target = None; + } + + fn tonumber(&self) -> f64 { + self.result.as_number().unwrap_or(0.0) + } + + fn tostring(&self) -> String { + self.result.as_text() + } + + fn toboolean(&self) -> bool { + self.result.as_bool() + } + + fn get_type(&self) -> &'static str { + "command" + } + + fn get_value(&self) -> ScalarValue { + self.result.to_scalar() + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +struct ListGetCommand { + target: Option, + index: Option, + result: Value, +} + +impl GrapholObject for ListGetCommand { + fn receive(&mut self, value: Value, _host: &mut dyn ExecutionHost) { + if self.target.is_none() { + self.target = list_target(&value); + return; + } + + if self.index.is_none() { + self.index = value.as_number().and_then(float_to_index); + self.result = self + .target + .as_ref() + .and_then(|target| self.index.and_then(|index| target.borrow().list_get(index))) + .unwrap_or(Value::Null); + } + } + + fn end(&mut self) { + self.target = None; + self.index = None; + } + + fn tonumber(&self) -> f64 { + self.result.as_number().unwrap_or(0.0) + } + + fn tostring(&self) -> String { + self.result.as_text() + } + + fn toboolean(&self) -> bool { + self.result.as_bool() + } + + fn get_type(&self) -> &'static str { + "command" + } + + fn get_value(&self) -> ScalarValue { + self.result.to_scalar() + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +struct ListSetCommand { + target: Option, + index: Option, + has_index: bool, +} + +impl GrapholObject for ListSetCommand { + fn receive(&mut self, value: Value, _host: &mut dyn ExecutionHost) { + if self.target.is_none() { + self.target = list_target(&value); + return; + } + + if !self.has_index { + self.index = value.as_number().and_then(float_to_index); + self.has_index = true; + return; + } + + if let (Some(target), Some(index)) = (&self.target, self.index) { + target.borrow_mut().list_set(index, value); + } + } + + fn end(&mut self) { + self.target = None; + self.index = None; + self.has_index = false; + } + + fn get_type(&self) -> &'static str { + "command" + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +struct ListLenCommand { + consumed_target: bool, + result: usize, +} + +impl GrapholObject for ListLenCommand { + fn receive(&mut self, value: Value, _host: &mut dyn ExecutionHost) { + if self.consumed_target { + return; + } + + self.result = list_target(&value) + .and_then(|target| target.borrow().list_len()) + .unwrap_or(0); + self.consumed_target = true; + } + + fn end(&mut self) { + self.consumed_target = false; + } + + fn tonumber(&self) -> f64 { + self.result as f64 + } + + fn tostring(&self) -> String { + self.result.to_string() + } + + fn toboolean(&self) -> bool { + self.result > 0 + } + + fn get_type(&self) -> &'static str { + "command" + } + + fn get_value(&self) -> ScalarValue { + ScalarValue::Number(self.result as f64) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +fn float_to_index(value: f64) -> Option { + if !value.is_finite() || value < 0.0 || value.fract() != 0.0 { + return None; + } + Some(value as usize) +} diff --git a/src/runtime/object/list_object.rs b/src/runtime/object/list_object.rs new file mode 100644 index 0000000..56c8242 --- /dev/null +++ b/src/runtime/object/list_object.rs @@ -0,0 +1,92 @@ +use std::any::Any; + +use super::super::host::ExecutionHost; +use super::super::value::{ObjectRef, Value}; +use super::{GrapholObject, object_ref}; + +pub fn new_list() -> ObjectRef { + object_ref(ListObject { items: Vec::new() }) +} + +pub fn list_target(value: &Value) -> Option { + let Value::Obj(obj) = value else { + return None; + }; + + if obj.borrow().list_len().is_some() { + return Some(obj.clone()); + } + + None +} + +struct ListObject { + items: Vec, +} + +impl GrapholObject for ListObject { + fn receive(&mut self, value: Value, _host: &mut dyn ExecutionHost) { + self.items.push(value); + } + + fn tonumber(&self) -> f64 { + self.items.len() as f64 + } + + fn tostring(&self) -> String { + let values = self + .items + .iter() + .map(format_list_value) + .collect::>() + .join(", "); + format!("[{}]", values) + } + + fn toboolean(&self) -> bool { + !self.items.is_empty() + } + + fn get_type(&self) -> &'static str { + "list" + } + + fn list_len(&self) -> Option { + Some(self.items.len()) + } + + fn list_get(&self, index: usize) -> Option { + self.items.get(index).cloned() + } + + fn list_set(&mut self, index: usize, value: Value) -> bool { + if index >= self.items.len() { + return false; + } + self.items[index] = value; + true + } + + fn list_pop(&mut self) -> Option { + self.items.pop() + } + + fn list_push(&mut self, value: Value) -> bool { + self.items.push(value); + true + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +fn format_list_value(value: &Value) -> String { + match value { + Value::Number(number) => number.to_string(), + Value::Text(text) => format!("{:?}", text), + Value::Bool(boolean) => boolean.to_string(), + Value::Null => "null".to_string(), + Value::Obj(obj) => obj.borrow().tostring(), + } +} diff --git a/src/runtime/scope.rs b/src/runtime/scope.rs index dca9dcc..6ef1995 100644 --- a/src/runtime/scope.rs +++ b/src/runtime/scope.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; use std::rc::Rc; use super::object::{ - StdoutState, new_echo, new_if, new_input, new_message_async, new_message_else, new_message_run, - new_node, new_stdout, new_while, + StdoutState, new_echo, new_if, new_input, new_list_get, new_list_len, new_list_pop, + new_list_push, new_list_set, new_message_async, new_message_else, new_message_run, new_node, + new_stdout, new_while, }; use super::value::ObjectRef; @@ -26,6 +27,11 @@ impl Scope { values.insert("if".to_string(), new_if()); values.insert("else".to_string(), new_message_else()); values.insert("while".to_string(), new_while()); + values.insert("list_push".to_string(), new_list_push()); + values.insert("list_pop".to_string(), new_list_pop()); + values.insert("list_get".to_string(), new_list_get()); + values.insert("list_set".to_string(), new_list_set()); + values.insert("list_len".to_string(), new_list_len()); Rc::new(RefCell::new(Self { values, parent })) } From 128cef463f5778d07679bb8d74863b17145f96de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Chav=C3=A3o?= Date: Tue, 7 Apr 2026 11:52:11 -0300 Subject: [PATCH 2/5] Support literal evaluation and list-aware node behavior --- src/runtime/executor.rs | 12 +++++-- .../object_strategies/node_primitives.rs | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/runtime/executor.rs b/src/runtime/executor.rs index 37d18d0..5c0fa2c 100644 --- a/src/runtime/executor.rs +++ b/src/runtime/executor.rs @@ -6,7 +6,7 @@ use super::host::ExecutionHost; use super::io::{OutputEvent, OutputMode, RuntimeIo}; use super::object::{ BlockSnapshot, StdoutState, end_object, new_block, new_boolean_operator, new_logic_operator, - new_node, new_operator, receive_object, + new_list, new_node, new_operator, receive_object, }; use super::scope::{Scope, ScopeRef}; use super::value::{ObjectRef, Value}; @@ -265,6 +265,10 @@ impl RuntimeEngine { fn eval_root(&mut self, node: &NodeExpr, scope: &ScopeRef) -> Result { match node { NodeExpr::Identifier(name) => { + if name == "list" { + return Ok(new_list()); + } + if let Some(literal) = parse_literal(name) { let node_ref = new_node(); receive_object(&node_ref, literal, self); @@ -292,7 +296,11 @@ impl RuntimeEngine { fn eval_message(&mut self, node: &NodeExpr, scope: &ScopeRef) -> Result { let value = match node { NodeExpr::Identifier(name) => { - parse_literal(name).unwrap_or_else(|| Value::Obj(Scope::get(scope, name))) + if name == "list" { + Value::Obj(new_list()) + } else { + parse_literal(name).unwrap_or_else(|| Value::Obj(Scope::get(scope, name))) + } } NodeExpr::StringLiteral(text) => Value::Text(text.clone()), NodeExpr::Reserved(token) => Value::Obj(reserved_to_object(*token)?), diff --git a/src/runtime/object/object_strategies/node_primitives.rs b/src/runtime/object/object_strategies/node_primitives.rs index afd0fc6..f4fff69 100644 --- a/src/runtime/object/object_strategies/node_primitives.rs +++ b/src/runtime/object/object_strategies/node_primitives.rs @@ -83,6 +83,38 @@ impl GrapholObject for NodeObject { .unwrap_or(ScalarValue::Null) } + fn list_len(&self) -> Option { + self.strategy + .as_ref() + .and_then(|strategy| strategy.borrow().list_len()) + } + + fn list_get(&self, index: usize) -> Option { + self.strategy + .as_ref() + .and_then(|strategy| strategy.borrow().list_get(index)) + } + + fn list_set(&mut self, index: usize, value: Value) -> bool { + self.strategy + .as_ref() + .map(|strategy| strategy.borrow_mut().list_set(index, value)) + .unwrap_or(false) + } + + fn list_pop(&mut self) -> Option { + self.strategy + .as_ref() + .and_then(|strategy| strategy.borrow_mut().list_pop()) + } + + fn list_push(&mut self, value: Value) -> bool { + self.strategy + .as_ref() + .map(|strategy| strategy.borrow_mut().list_push(value)) + .unwrap_or(false) + } + fn as_any(&self) -> &dyn Any { self } @@ -150,6 +182,7 @@ fn strategy_factory(value: Value, host: &mut dyn ExecutionHost) -> ObjectRef { let kind = obj.borrow().get_type().to_string(); match kind.as_str() { "block" => obj, + "list" => obj, "boolean" => object_ref(BooleanStrategy { value: obj.borrow().toboolean(), }), From 76a179e83bb453c2d6d5a7f508f820c7ef8bbbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Chav=C3=A3o?= Date: Tue, 7 Apr 2026 11:52:16 -0300 Subject: [PATCH 3/5] Embed list runtime modules in generated Rust output --- src/compiler/codegen.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/compiler/codegen.rs b/src/compiler/codegen.rs index b4d129d..6783b23 100644 --- a/src/compiler/codegen.rs +++ b/src/compiler/codegen.rs @@ -9,6 +9,8 @@ const IO_SOURCE: &str = include_str!("../runtime/io.rs"); const VALUE_SOURCE: &str = include_str!("../runtime/value.rs"); const OBJECT_SOURCE: &str = include_str!("../runtime/object.rs"); const OBJECT_COMMANDS_SOURCE: &str = include_str!("../runtime/object/object_commands.rs"); +const LIST_OBJECT_SOURCE: &str = include_str!("../runtime/object/list_object.rs"); +const LIST_COMMANDS_SOURCE: &str = include_str!("../runtime/object/list_commands.rs"); const NODE_PRIMITIVES_SOURCE: &str = include_str!("../runtime/object/object_strategies/node_primitives.rs"); const NUMERIC_OPS_SOURCE: &str = include_str!("../runtime/object/object_strategies/numeric_ops.rs"); @@ -60,6 +62,8 @@ fn runtime_module_source() -> String { out.push_str(" pub mod object {\n"); push_nested_module(&mut out, "object_commands", OBJECT_COMMANDS_SOURCE, 2); + push_nested_module(&mut out, "list_object", LIST_OBJECT_SOURCE, 2); + push_nested_module(&mut out, "list_commands", LIST_COMMANDS_SOURCE, 2); out.push_str(" mod object_strategies {\n"); out.push_str(" mod strategy_core {\n"); @@ -120,7 +124,10 @@ fn strip_object_module_decls(source: &str) -> String { .lines() .filter(|line| { let trimmed = line.trim(); - trimmed != "mod object_commands;" && trimmed != "mod object_strategies;" + trimmed != "mod object_commands;" + && trimmed != "mod list_object;" + && trimmed != "mod list_commands;" + && trimmed != "mod object_strategies;" }) .collect::>() .join("\n") From dbddd9fbdbea6ea4be33f5337300e3fe1a149335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Chav=C3=A3o?= Date: Tue, 7 Apr 2026 11:52:19 -0300 Subject: [PATCH 4/5] Add runtime coverage for list operations and null-safe edges --- tests/graphol_runtime.rs | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/graphol_runtime.rs b/tests/graphol_runtime.rs index b2ccac6..bc16a1b 100644 --- a/tests/graphol_runtime.rs +++ b/tests/graphol_runtime.rs @@ -283,6 +283,51 @@ fn executes_program_with_include_from_file() { ); } +#[test] +fn executes_list_operations() { + let source = r#" +nums (list 1 2 3) +list_push nums 4 +echo nums +echo (list_len nums) +echo (list_get nums 0) +list_set nums 1 99 +echo nums +echo (list_pop nums) +echo nums +list_set nums 9 123 +echo (list_len nums) +"#; + + let stdout = compile_and_run(source, &[], &[]); + assert_eq!( + output_lines(&stdout), + vec![ + "[1, 2, 3, 4]", + "4", + "1", + "[1, 99, 3, 4]", + "4", + "[1, 99, 3]", + "3", + ] + ); +} + +#[test] +fn list_pop_on_empty_is_falsy() { + let source = r#" +if (list_pop (list)) { + echo "unexpected" +} else { + echo "empty" +} +"#; + + let stdout = compile_and_run(source, &[], &[]); + assert_eq!(output_lines(&stdout), vec!["empty"]); +} + fn create_temp_dir(name: &str) -> PathBuf { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) From 85475377a7233a169b621790252999d7dec3f22a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Chav=C3=A3o?= Date: Tue, 7 Apr 2026 11:52:24 -0300 Subject: [PATCH 5/5] Add list-focused example programs for core and edge-case flows --- examples/program10.graphol | 19 +++++++++++++++++++ examples/program8.graphol | 18 ++++++++++++++++++ examples/program9.graphol | 18 ++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 examples/program10.graphol create mode 100644 examples/program8.graphol create mode 100644 examples/program9.graphol diff --git a/examples/program10.graphol b/examples/program10.graphol new file mode 100644 index 0000000..66a1830 --- /dev/null +++ b/examples/program10.graphol @@ -0,0 +1,19 @@ +// Composition examples: pass list to blocks and mutate nested lists. + +show_list { + data inbox + echo ("received -> " data) + echo ("first item -> " (list_get data 0)) + echo ("len -> " (list_len data)) +} + +payload (list "graph" "oriented" "language") +show_list payload run + +mixed (list 1 "two" true (list 3 4)) +echo ("mixed -> " mixed) +echo ("nested before push -> " (list_get mixed 3)) + +list_push (list_get mixed 3) 5 +echo ("nested after push -> " (list_get mixed 3)) +echo ("mixed after nested push -> " mixed) diff --git a/examples/program8.graphol b/examples/program8.graphol new file mode 100644 index 0000000..8bcafb6 --- /dev/null +++ b/examples/program8.graphol @@ -0,0 +1,18 @@ +// List basics: create, push, get, set, pop, len. + +nums (list 1 2 3) +echo ("initial -> " nums) + +list_push nums 4 +echo ("after push -> " nums) +echo ("len -> " (list_len nums)) + +echo ("index 0 -> " (list_get nums 0)) +echo ("index 2 -> " (list_get nums 2)) + +list_set nums 1 99 +echo ("after set index 1 -> " nums) + +last (list_pop nums) +echo ("popped -> " last) +echo ("after pop -> " nums) diff --git a/examples/program9.graphol b/examples/program9.graphol new file mode 100644 index 0000000..cfde415 --- /dev/null +++ b/examples/program9.graphol @@ -0,0 +1,18 @@ +// Safe edge cases: empty pop, out-of-range get/set. + +empty (list) +echo ("empty len -> " (list_len empty)) + +if (list_pop empty) { + echo "unexpected" +} else { + echo "pop on empty returns null (falsy)" +} + +nums (list 10 20) +echo ("before invalid operations -> " nums) + +echo ("out-of-range get (index 9) -> " (list_get nums 9)) +list_set nums 9 777 + +echo ("after invalid set (should be unchanged) -> " nums)