From 5a47c7b783b8bd3d1a11ab520f22158883632e3c Mon Sep 17 00:00:00 2001 From: shubhiscoding Date: Mon, 30 Mar 2026 20:05:05 +0530 Subject: [PATCH 1/4] Implement object literals and property access in parser and runtime --- src/ast/mod.rs | 24 ++++- src/compiler/mod.rs | 30 ++++++ src/lexer/mod.rs | 16 +++- src/parser/mod.rs | 182 ++++++++++++++++++++++++++---------- src/vm/mod.rs | 47 ++++++++++ tests/integration_tests.rs | 183 +++++++++++++++++++++++++++++++++++++ 6 files changed, 431 insertions(+), 51 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index daa17cc..901c783 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::{cell::RefCell, rc::Rc}; #[derive(Debug)] @@ -44,6 +45,11 @@ pub enum Statement { index: Vec, value: Expression, }, + AssignmentProperty { + object: String, + property: Vec, + value: Expression, + }, } #[derive(Debug, Clone, PartialEq)] @@ -83,11 +89,27 @@ pub enum Expression { array: Box, index: Box, }, + ObjectLiteral(Vec<(String, Expression)>), + PropertyAccess { + object: Box, + property: String, + }, } -#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Clone, PartialEq)] pub enum Value { Number(i32), String(String), Array(Rc>>), + Object(Rc>>), +} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Value::Number(a), Value::Number(b)) => a.partial_cmp(b), + (Value::String(a), Value::String(b)) => a.partial_cmp(b), + _ => None, + } + } } diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index 88c27e2..05d529c 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -46,6 +46,9 @@ pub enum Instruction { CreateArray(usize), LoadIndex, StoreIndex, + CreateObject(usize), + LoadProperty, + StoreProperty, } fn compile_expr(instructions: &mut Vec, expr: &Expression) { @@ -114,6 +117,19 @@ fn compile_expr(instructions: &mut Vec, expr: &Expression) { compile_expr(instructions, index); instructions.push(Instruction::LoadIndex); } + Expression::ObjectLiteral(objs) => { + for obj in objs { + let (key, value) = obj; + instructions.push(Instruction::LoadConst(Value::String(key.to_owned()))); + compile_expr(instructions, value); + } + instructions.push(Instruction::CreateObject(objs.len())); + } + Expression::PropertyAccess { object, property } => { + compile_expr(instructions, object); + instructions.push(Instruction::LoadConst(Value::String(property.to_owned()))); + instructions.push(Instruction::LoadProperty); + } } } @@ -271,6 +287,20 @@ pub fn compile_statements( compile_expr(instructions, &value); instructions.push(Instruction::StoreIndex); } + Statement::AssignmentProperty { + object, + property, + value, + } => { + instructions.push(Instruction::LoadVar(object)); + for prop in property { + instructions.push(Instruction::LoadConst(Value::String(prop.to_owned()))); + instructions.push(Instruction::LoadProperty); + } + instructions.pop(); + compile_expr(instructions, &value); + instructions.push(Instruction::StoreProperty); + } } } } diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index b3871d5..4f0a99b 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -33,6 +33,8 @@ pub enum Token { Return, SquareLeft, SquareRight, + Dot, + Colon, } pub fn tokenize(input: &str) -> Vec { @@ -221,11 +223,21 @@ pub fn tokenize(input: &str) -> Vec { } } - 'a'..='z' | 'A'..='Z' => { + '.' => { + tokens.push(Token::Dot); + chars.next(); + } + + ':' => { + tokens.push(Token::Colon); + chars.next(); + } + + 'a'..='z' | 'A'..='Z' | '_' => { let mut ident = String::new(); while let Some(&c) = chars.peek() { - if c.is_alphanumeric() { + if c.is_alphanumeric() || c == '_' { ident.push(c); chars.next(); } else { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0a833d6..b8e9363 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -358,6 +358,63 @@ impl Parser { self.parse_primary() } + fn parse_array_assignment(&mut self, name: String) -> Statement { + let mut indices = Vec::new(); + while Some(&Token::SquareLeft) == self.peek() { + self.advance(); + indices.push(self.parse_first()); + match self.advance() { + Some(Token::SquareRight) => {} + _ => panic!("Expected ']' after array index"), + } + } + match self.advance() { + Some(Token::Equals) => {} + _ => panic!("Expected '=' after array index"), + } + let value = self.parse_first(); + match self.advance() { + Some(Token::Semicolon) => {} + _ => panic!("Expected ';' after array assignment"), + } + Statement::AssignmentIndex { + array: name, + index: indices, + value, + } + } + + fn parse_obj_assignment(&mut self, name: String) -> Statement { + let mut properties = Vec::new(); + while let Some(token) = self.peek() { + if *token != Token::Dot { + break; + } + self.advance(); + let prop_name = match self.advance() { + Some(Token::Identifier(name)) => name.to_owned(), + _ => panic!("Expected identifier"), + }; + properties.push(prop_name); + } + + match self.advance() { + Some(Token::Equals) => {} + _ => panic!("Expected '=' after object property"), + } + let value = self.parse_first(); + match self.advance() { + Some(Token::Semicolon) => {} + _ => panic!("Expected ';' after object assignment"), + } + + Statement::AssignmentProperty { + object: name, + property: properties, + value, + } + } + fn parse_call(&mut self, name: String) -> Statement { self.advance(); // consume the identifier if let Some(Token::LeftParentheses) = self.peek() { @@ -369,29 +426,9 @@ impl Parser { } Statement::Expression(call_expr) } else if let Some(Token::SquareLeft) = self.peek() { - let mut indices = Vec::new(); - while Some(&Token::SquareLeft) == self.peek() { - self.advance(); - indices.push(self.parse_first()); - match self.advance() { - Some(Token::SquareRight) => {} - _ => panic!("Expected ']' after array index"), - } - } - match self.advance() { - Some(Token::Equals) => {} - _ => panic!("Expected '=' after array index"), - } - let value = self.parse_first(); - match self.advance() { - Some(Token::Semicolon) => {} - _ => panic!("Expected ';' after array assignment"), - } - Statement::AssignmentIndex { - array: name, - index: indices, - value, - } + self.parse_array_assignment(name) + } else if let Some(Token::Dot) = self.peek() { + self.parse_obj_assignment(name) } else { self.parse_assignment(name) } @@ -421,6 +458,60 @@ impl Parser { Expression::Call { name, args } } + fn parse_object(&mut self) -> Expression { + let mut properties = Vec::new(); + while let Some(token) = self.peek() { + if *token == Token::RightBrace { + break; + } + let key = match self.advance() { + Some(Token::Identifier(name)) => name.to_owned(), + _ => panic!("Expected identifier"), + }; + match self.advance() { + Some(Token::Colon) => {} + _ => panic!("Expected ':'"), + } + let value = self.parse_first(); + properties.push((key, value)); + if let Some(Token::Comma) = self.peek() { + self.advance(); + } + } + + match self.advance() { + Some(Token::RightBrace) => {} + _ => panic!("Expected '}}'"), + } + + Expression::ObjectLiteral(properties) + } + + fn parse_index_access(&mut self, array: Expression) -> Expression { + self.advance(); // consume '[' + let index_expr = self.parse_first(); + match self.advance() { + Some(Token::SquareRight) => {} + _ => panic!("Expected ']' after array index"), + } + Expression::Index { + array: Box::new(array), + index: Box::new(index_expr), + } + } + + fn parse_property_access(&mut self, object: Expression) -> Expression { + self.advance(); // consume '.' + let prop_name = match self.advance() { + Some(Token::Identifier(name)) => name.to_owned(), + _ => panic!("Expected identifier after '.'"), + }; + Expression::PropertyAccess { + object: Box::new(object), + property: prop_name, + } + } + fn parse_primary(&mut self) -> Expression { match self.advance() { Some(Token::LeftParentheses) => { @@ -433,39 +524,34 @@ impl Parser { } Some(Token::Number(n)) => Expression::Number(*n), Some(Token::Identifier(name)) => { - let name = name.clone(); - // If followed by '(', it's a function call expression - if let Some(Token::LeftParentheses) = self.peek() { - self.parse_call_args(name) - } else if let Some(Token::SquareLeft) = self.peek() { - self.parse_index(name) - } else { - Expression::Identifier(name) + let mut expr = Expression::Identifier(name.clone()); + loop { + match self.peek() { + Some(Token::LeftParentheses) => { + let name = match expr { + Expression::Identifier(ref n) => n.clone(), + _ => panic!("Expected function name before '('"), + }; + expr = self.parse_call_args(name); + } + Some(Token::SquareLeft) => { + expr = self.parse_index_access(expr); + } + Some(Token::Dot) => { + expr = self.parse_property_access(expr); + } + _ => break, + } } + expr } Some(Token::SquareLeft) => self.parse_array_literal(), + Some(Token::LeftBrace) => self.parse_object(), Some(Token::String(s)) => Expression::String(s.clone()), _ => panic!("Invalid expression"), } } - fn parse_index(&mut self, array_name: String) -> Expression { - let mut result = Expression::Identifier(array_name.clone()); - while Some(&Token::SquareLeft) == self.peek() { - self.advance(); // consume '[' - let index_expr = self.parse_first(); - match self.advance() { - Some(Token::SquareRight) => {} - _ => panic!("Expected ']' after array index"), - } - result = Expression::Index { - array: Box::new(result), - index: Box::new(index_expr), - }; - } - result - } - fn parse_array_literal(&mut self) -> Expression { let mut elements = Vec::new(); while let Some(token) = self.peek() { diff --git a/src/vm/mod.rs b/src/vm/mod.rs index 48a6cef..16d96c1 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -19,6 +19,15 @@ impl fmt::Display for Value { .join(", "); write!(f, "[{}]", elements) } + Value::Object(objs) => { + let props = objs + .borrow() + .iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", "); + write!(f, "{{{}}}", props) + } } } } @@ -348,6 +357,44 @@ pub fn execute(program: Program, runtime: &mut Runtime) { panic!("StoreIndex requires an array and a number index"); } } + Instruction::CreateObject(len) => { + let objs = Rc::new(RefCell::new(HashMap::::new())); + for _ in 0..len { + let value = runtime.pop_or_panic_stack(); + let key = runtime.pop_or_panic_stack(); + if let (Value::String(key), value) = (key, value) { + objs.borrow_mut().insert(key, value); + } else { + panic!("CreateObject requires string keys and values"); + } + } + runtime.stack.push(Value::Object(objs)); + } + Instruction::LoadProperty => { + let property = runtime.pop_or_panic_stack(); + let object = runtime.pop_or_panic_stack(); + if let (Value::Object(obj), Value::String(prop)) = (&object, &property) { + let map = obj.borrow(); + if let Some(value) = map.get(prop) { + runtime.stack.push(value.clone()); + } else { + panic!("Property '{}' not found", prop); + } + } else { + panic!("LoadProperty requires an object and a string property"); + } + } + Instruction::StoreProperty => { + let value = runtime.pop_or_panic_stack(); + let property = runtime.pop_or_panic_stack(); + let object = runtime.pop_or_panic_stack(); + if let (Value::Object(obj), Value::String(prop)) = (&object, &property) { + let mut map = obj.borrow_mut(); + map.insert(prop.clone(), value); + } else { + panic!("StoreProperty requires an object and a string property"); + } + } } runtime.frames.last_mut().unwrap().ip += 1; // Move to next instruction } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index cbdb16f..c297129 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1109,3 +1109,186 @@ print g; let out = run_rts(code); assert_eq!(out, "[[1, 99], [3, 4]]"); } + +// ========== Objects ========== + +#[test] +fn test_object_property_read() { + let code = r#" +let obj = {x: 10, y: 20}; +print obj.x; +print obj.y; +"#; + let out = run_rts(code); + assert_eq!(out, "10\n20"); +} + +#[test] +fn test_object_property_write() { + let code = r#" +let obj = {a: 1, b: 2}; +obj.a = 99; +print obj.a; +"#; + let out = run_rts(code); + assert_eq!(out, "99"); +} + +#[test] +fn test_object_property_write_preserves_others() { + let code = r#" +let obj = {a: 1, b: 2}; +obj.a = 99; +print obj.b; +"#; + let out = run_rts(code); + assert_eq!(out, "2"); +} + +#[test] +fn test_object_add_new_property() { + let code = r#" +let obj = {x: 1}; +obj.y = 2; +print obj.x; +print obj.y; +"#; + let out = run_rts(code); + assert_eq!(out, "1\n2"); +} + +#[test] +fn test_object_string_values() { + let code = r#" +let obj = {greeting: "hello"}; +print obj.greeting + " world"; +"#; + let out = run_rts(code); + assert_eq!(out, "hello world"); +} + +#[test] +fn test_object_in_expression() { + let code = r#" +let obj = {a: 10, b: 20}; +print obj.a + obj.b; +"#; + let out = run_rts(code); + assert_eq!(out, "30"); +} + +#[test] +fn test_object_in_variable() { + let code = r#" +let obj = {val: 42}; +let x = obj.val + 8; +print x; +"#; + let out = run_rts(code); + assert_eq!(out, "50"); +} + +#[test] +fn test_object_nested() { + let code = r#" +let obj = {inner: {x: 5}}; +print obj.inner.x; +"#; + let out = run_rts(code); + assert_eq!(out, "5"); +} + +#[test] +fn test_object_nested_write() { + let code = r#" +let obj = {inner: {x: 5}}; +obj.inner.x = 99; +print obj.inner.x; +"#; + let out = run_rts(code); + assert_eq!(out, "99"); +} + +#[test] +fn test_object_passed_to_function() { + let code = r#" +function getX(obj) { + return obj.x; +} +let p = {x: 42, y: 10}; +print getX(p); +"#; + let out = run_rts(code); + assert_eq!(out, "42"); +} + +#[test] +fn test_object_mutated_in_function() { + let code = r#" +function setX(obj, val) { + obj.x = val; + return 0; +} +let p = {x: 1, y: 2}; +setX(p, 99); +print p.x; +"#; + let out = run_rts(code); + assert_eq!(out, "99"); +} + +#[test] +fn test_object_with_array_value() { + let code = r#" +let obj = {items: [10, 20, 30]}; +print obj.items[1]; +"#; + let out = run_rts(code); + assert_eq!(out, "20"); +} + +#[test] +fn test_object_property_in_condition() { + let code = r#" +let obj = {score: 85}; +if (obj.score > 50) { + print "pass"; +} else { + print "fail"; +} +"#; + let out = run_rts(code); + assert_eq!(out, "pass"); +} + +#[test] +fn test_object_property_in_loop() { + let code = r#" +let obj = {count: 3}; +while (obj.count > 0) { + print obj.count; + obj.count = obj.count - 1; +} +"#; + let out = run_rts(code); + assert_eq!(out, "3\n2\n1"); +} + +#[test] +fn test_object_underscore_property() { + let code = r#" +let obj = {my_value: 42}; +print obj.my_value; +"#; + let out = run_rts(code); + assert_eq!(out, "42"); +} + +#[test] +fn test_object_undefined_property_fails() { + let stderr = run_rts_should_fail( + r#"let obj = {a: 1}; +print obj.b;"#, + ); + assert!(stderr.contains("not found")); +} From 9416a3e94f9e3333e26239b67a5bb428a855f9fd Mon Sep 17 00:00:00 2001 From: shubhiscoding Date: Mon, 30 Mar 2026 20:09:47 +0530 Subject: [PATCH 2/4] Add support for object bracket access in runtime and integration tests --- src/vm/mod.rs | 48 +++++++++++++++++---------- tests/integration_tests.rs | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/src/vm/mod.rs b/src/vm/mod.rs index 16d96c1..824ef44 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -331,30 +331,44 @@ pub fn execute(program: Program, runtime: &mut Runtime) { } Instruction::LoadIndex => { let index = runtime.pop_or_panic_stack(); - let array = runtime.pop_or_panic_stack(); - if let (Value::Array(arr), Value::Number(idx)) = (array, index) { - let vec = arr.borrow(); - if idx < 0 || (idx as usize) >= vec.len() { - panic!("Array index out of bounds"); - } // immutable borrow - runtime.stack.push(vec[idx as usize].clone()); - } else { - panic!("LoadIndex requires an array and a number index"); + let target = runtime.pop_or_panic_stack(); + match (&target, &index) { + (Value::Array(arr), Value::Number(idx)) => { + let vec = arr.borrow(); + if *idx < 0 || (*idx as usize) >= vec.len() { + panic!("Array index out of bounds"); + } + runtime.stack.push(vec[*idx as usize].clone()); + } + (Value::Object(obj), Value::String(key)) => { + let map = obj.borrow(); + if let Some(value) = map.get(key) { + runtime.stack.push(value.clone()); + } else { + panic!("Property '{}' not found", key); + } + } + _ => panic!("Index access requires an array with number index or object with string key"), } } Instruction::StoreIndex => { let value = runtime.pop_or_panic_stack(); let index = runtime.pop_or_panic_stack(); - let array = runtime.pop_or_panic_stack(); + let target = runtime.pop_or_panic_stack(); - if let (Value::Array(arr), Value::Number(idx)) = (&array, index) { - let mut vec = arr.borrow_mut(); - if idx < 0 || (idx as usize) >= vec.len() { - panic!("Array index out of bounds"); + match (&target, &index) { + (Value::Array(arr), Value::Number(idx)) => { + let mut vec = arr.borrow_mut(); + if *idx < 0 || (*idx as usize) >= vec.len() { + panic!("Array index out of bounds"); + } + vec[*idx as usize] = value; } - vec[idx as usize] = value; - } else { - panic!("StoreIndex requires an array and a number index"); + (Value::Object(obj), Value::String(key)) => { + let mut map = obj.borrow_mut(); + map.insert(key.clone(), value); + } + _ => panic!("Index access requires an array with number index or object with string key"), } } Instruction::CreateObject(len) => { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index c297129..e605975 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1292,3 +1292,69 @@ print obj.b;"#, ); assert!(stderr.contains("not found")); } + +// ========== Object Bracket Access ========== + +#[test] +fn test_object_bracket_read() { + let code = r#" +let obj = {a: 10, b: 20}; +print obj["a"]; +print obj["b"]; +"#; + let out = run_rts(code); + assert_eq!(out, "10\n20"); +} + +#[test] +fn test_object_bracket_write() { + let code = r#" +let obj = {a: 1}; +obj["a"] = 99; +print obj.a; +"#; + let out = run_rts(code); + assert_eq!(out, "99"); +} + +#[test] +fn test_object_bracket_dynamic_key() { + let code = r#" +let obj = {x: 42}; +let key = "x"; +print obj[key]; +"#; + let out = run_rts(code); + assert_eq!(out, "42"); +} + +#[test] +fn test_object_bracket_add_new_property() { + let code = r#" +let obj = {a: 1}; +obj["b"] = 2; +print obj.b; +"#; + let out = run_rts(code); + assert_eq!(out, "2"); +} + +#[test] +fn test_object_bracket_nested() { + let code = r#" +let obj = {inner: {val: 5}}; +print obj["inner"]["val"]; +"#; + let out = run_rts(code); + assert_eq!(out, "5"); +} + +#[test] +fn test_object_bracket_mixed_with_dot() { + let code = r#" +let obj = {inner: {val: 5}}; +print obj.inner["val"]; +"#; + let out = run_rts(code); + assert_eq!(out, "5"); +} From 4e218c76035dcfd82ff7dd51429dd6a28bfdd91c Mon Sep 17 00:00:00 2001 From: shubhiscoding Date: Mon, 30 Mar 2026 20:17:04 +0530 Subject: [PATCH 3/4] Add depth-first search implementation with graph representation --- example/graph_dfs.rts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 example/graph_dfs.rts diff --git a/example/graph_dfs.rts b/example/graph_dfs.rts new file mode 100644 index 0000000..caec647 --- /dev/null +++ b/example/graph_dfs.rts @@ -0,0 +1,31 @@ +let graph = { + A: ["B", "C"], + B: ["D"], + C: ["E"], + D: ["F"], + E: [], + F: [] +}; + +let sizes = {A: 2, B: 1, C: 1, D: 1, E: 0, F: 0}; + +let visited = { + A: 0, B: 0, C: 0, D: 0, E: 0, F: 0 +}; + +function dfs(graph, sizes, visited, node) { + if (visited[node] == 1) { + return 0; + } + visited[node] = 1; + print node; + + let neighbors = graph[node]; + let count = sizes[node]; + for (let i = 0; i < count; i++) { + dfs(graph, sizes, visited, neighbors[i]); + } + return 0; +} + +dfs(graph, sizes, visited, "A"); From 20a783b1240e135c891809c4ff7413479053bd96 Mon Sep 17 00:00:00 2001 From: shubhiscoding Date: Mon, 30 Mar 2026 20:17:50 +0530 Subject: [PATCH 4/4] fixed formatting issue --- src/vm/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vm/mod.rs b/src/vm/mod.rs index 824ef44..a1a8be3 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -348,7 +348,9 @@ pub fn execute(program: Program, runtime: &mut Runtime) { panic!("Property '{}' not found", key); } } - _ => panic!("Index access requires an array with number index or object with string key"), + _ => panic!( + "Index access requires an array with number index or object with string key" + ), } } Instruction::StoreIndex => { @@ -368,7 +370,9 @@ pub fn execute(program: Program, runtime: &mut Runtime) { let mut map = obj.borrow_mut(); map.insert(key.clone(), value); } - _ => panic!("Index access requires an array with number index or object with string key"), + _ => panic!( + "Index access requires an array with number index or object with string key" + ), } } Instruction::CreateObject(len) => {