From 230fbf630aee1c740e2e85577fa720c0ec9f9cd0 Mon Sep 17 00:00:00 2001 From: Cyteon Date: Wed, 25 Mar 2026 12:23:06 +0100 Subject: [PATCH 1/4] class extending --- ...-lang-1.9.0.vsix => modu-lang-1.10.0.vsix} | Bin 15806 -> 15818 bytes extensions/vscode/package.json | 4 +-- .../vscode/syntaxes/modu.tmLanguage.json | 2 +- lang/examples/oop.modu | 28 +++++++++++++++++- lang/src/ast.rs | 1 + lang/src/compiler/compiler.rs | 14 +++++++-- lang/src/lexer.rs | 6 ++++ lang/src/parser.rs | 11 +++++-- lang/src/vm/instruction.rs | 1 + lang/src/vm/vm.rs | 20 +++++++++++++ web/src/lib/tour/pages/classes.md | 19 ++++++++++-- 11 files changed, 95 insertions(+), 11 deletions(-) rename extensions/vscode/{modu-lang-1.9.0.vsix => modu-lang-1.10.0.vsix} (80%) diff --git a/extensions/vscode/modu-lang-1.9.0.vsix b/extensions/vscode/modu-lang-1.10.0.vsix similarity index 80% rename from extensions/vscode/modu-lang-1.9.0.vsix rename to extensions/vscode/modu-lang-1.10.0.vsix index 3abdd01b07f3027faf4cdb634c88c112e19c601a..7ee40a5f0725f6cb3e63903f6cb98e98c41c6de6 100644 GIT binary patch delta 2219 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 diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 07ced17..5d42a7a 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -2,7 +2,7 @@ "name": "modu-lang", "displayName": "Modu Lang", "description": "Syntax highlightning for the Modu Programming Language", - "version": "1.9.0", + "version": "1.10.0", "repository": { "type": "git", "url": "https://github.com/cyteon/modu" @@ -20,7 +20,7 @@ ], "scripts": { "test-vscode": "vscode-test", - "install": "vsce package && code --install-extension ./modu-lang-1.9.0.vsix", + "install": "vsce package && code --install-extension ./modu-lang-1.10.0.vsix", "vsce": "vsce package", "test": "npm run vsce && npm run install" }, diff --git a/extensions/vscode/syntaxes/modu.tmLanguage.json b/extensions/vscode/syntaxes/modu.tmLanguage.json index 9928ce0..1dd73a3 100644 --- a/extensions/vscode/syntaxes/modu.tmLanguage.json +++ b/extensions/vscode/syntaxes/modu.tmLanguage.json @@ -19,7 +19,7 @@ "keywords": { "patterns": [{ "name": "keyword.control.modu", - "match": "\\b(if|else|fn|let|const|import|as|return|loop|break|continue|for|while|and|or|in|not in|class|self|try|catch)\\b" + "match": "\\b(if|else|fn|let|const|import|as|return|loop|break|continue|for|while|and|or|in|not in|class|self|try|catch|extends|super)\\b" }] }, diff --git a/lang/examples/oop.modu b/lang/examples/oop.modu index 32dc13d..1ce8c9b 100644 --- a/lang/examples/oop.modu +++ b/lang/examples/oop.modu @@ -33,4 +33,30 @@ print(counter.get()); counter.reset(); counter.decrement(); -print(counter.value); \ No newline at end of file +print(counter.value); + +print("\n---\n"); + +class xCounter extends Counter { + fn init(start, mult) { + //super.init(start); TODO: add super + self.value = start; + self.mult = mult; + } + + fn mul() { + self.value *= self.mult; + } + + fn div() { + self.value /= self.mult; + } +} + +let xc = xCounter(8, 2); +print(xc.value); + +xc.mul(); +xc.increment(); + +print(xc.value); \ No newline at end of file diff --git a/lang/src/ast.rs b/lang/src/ast.rs index a64783f..cd9d24a 100644 --- a/lang/src/ast.rs +++ b/lang/src/ast.rs @@ -139,6 +139,7 @@ pub enum Expr { Class { name: String, methods: Vec>, + parent: Option, }, Try { diff --git a/lang/src/compiler/compiler.rs b/lang/src/compiler/compiler.rs index 09d7b0b..0e150c7 100644 --- a/lang/src/compiler/compiler.rs +++ b/lang/src/compiler/compiler.rs @@ -234,7 +234,6 @@ impl Compiler { self.emit(Instruction::GetProperty(property.clone()), span); } - Expr::IndexAccess { object, index } => { self.compile_expr(*object.clone())?; self.compile_expr(*index.clone())?; @@ -655,7 +654,7 @@ impl Compiler { self.emit(Instruction::Import { path: name.clone(), alias: alias.clone() }, span); } - Expr::Class { name, methods } => { + Expr::Class { name, methods, parent } => { let mut methods_map = HashMap::new(); for f in methods { @@ -698,8 +697,17 @@ impl Compiler { let class_value = Value::Class { name: name.clone(), methods: methods_map }; let index = self.add_constant(class_value); - self.emit(Instruction::Push(index), span); + + if let Some(name) = parent { + match self.scope.resolve(&name) { + Variable::Local(index) => self.emit(Instruction::LoadLocal(index), span), + Variable::Global(name) => self.emit(Instruction::LoadGlobal(name), span), + } + + self.emit(Instruction::Extend, span); + } + self.store_variable(name, span); } diff --git a/lang/src/lexer.rs b/lang/src/lexer.rs index 18cadd6..9595801 100644 --- a/lang/src/lexer.rs +++ b/lang/src/lexer.rs @@ -98,6 +98,12 @@ pub enum Token { #[token("class")] Class, + #[token("extends")] + Extends, + + #[token("super")] + Super, + #[token("import")] Import, diff --git a/lang/src/parser.rs b/lang/src/parser.rs index 12693e1..1bc81d5 100644 --- a/lang/src/parser.rs +++ b/lang/src/parser.rs @@ -436,13 +436,20 @@ fn parser<'src>() -> impl Parser< let class_stmt = select! { (Token::Class, span) => span } .then(select! { (Token::Identifier(name), _) => name }.labelled("class name")) + .then( + select! { (Token::Extends, _) } + .ignore_then( + select! { (Token::Identifier(name), _) => name } + ) + .or_not() + ) .then( select! { (Token::LBrace, span) => span } .then(fn_stmt.clone().repeated().collect::>()) .then(select! { (Token::RBrace, span) => span }) ) - .map(|((start, name), ((_lbrace, methods), end)): ((Span, String), ((Span, Vec), Span))| SpannedExpr { - node: Expr::Class { name, methods }, + .map(|(((start, name), parent), ((_lbrace, methods), end)): (((Span, String), Option), ((Span, Vec), Span))| SpannedExpr { + node: Expr::Class { name, methods, parent }, span: Span::from(start.start..end.end), }) .labelled("class declaration"); diff --git a/lang/src/vm/instruction.rs b/lang/src/vm/instruction.rs index e5303b0..03b7c1e 100644 --- a/lang/src/vm/instruction.rs +++ b/lang/src/vm/instruction.rs @@ -30,6 +30,7 @@ pub enum Instruction { MakeArray(usize), MakeObject(usize), MakeRange { inclusive: bool }, + Extend, GetProperty(String), SetProperty(String), diff --git a/lang/src/vm/vm.rs b/lang/src/vm/vm.rs index 4777922..47fe1bf 100644 --- a/lang/src/vm/vm.rs +++ b/lang/src/vm/vm.rs @@ -1219,6 +1219,26 @@ impl VM { self.error_handlers.pop(); } + Instruction::Extend => { + let parent = self.stack.pop().unwrap_or(Value::Null); + let child = self.stack.pop().unwrap_or(Value::Null); + + match (child, parent) { + (Value::Class { name, mut methods }, Value::Class { methods: parent_methods, .. }) => { + for (k, v) in parent_methods { + methods.entry(k).or_insert(v); + } + + self.stack.push(Value::Class { name, methods }); + } + + (_, _) => { + self.handle_error("class can only extend a class".to_string(), span)?; + continue; + } + } + } + Instruction::Pop => { self.stack.pop(); } diff --git a/web/src/lib/tour/pages/classes.md b/web/src/lib/tour/pages/classes.md index 328bd75..5dab0ee 100644 --- a/web/src/lib/tour/pages/classes.md +++ b/web/src/lib/tour/pages/classes.md @@ -1,14 +1,29 @@ ## Classes Classes are a core part of object-oriented programming, they are a way to create your own objects with custom functions. A class can have an `init(...)` function, which is ran when you initialize the class, and can be used to set up any properties on the class. + Initializing the class will require the args defined in the `init(...)` function, but if you don't define an `init(...)` function then you can initialize the class without any args. [CODE] -// creates a new class named Counter -class Counter { +// creates a new class named Hello +class Hello { + fn init() { + print("initing..."); + } + + fn hello() { + print("hi"); + } +} + +// the class Counter will extend Hello, so it will also have the function hello() +class Counter extends Hello { // this function is optional to add, and is ran when we initialize the class // initializing the class will require the args defined here fn init(start) { + // this will call the original init function from the Hello class + super.init(); + self.value = start; } From 3a12890fa454c8b1cbdfb13e5ec4273dab372193 Mon Sep 17 00:00:00 2001 From: Cyteon Date: Wed, 25 Mar 2026 13:26:32 +0100 Subject: [PATCH 2/4] implement super --- lang/examples/oop.modu | 5 ++-- lang/src/ast.rs | 1 + lang/src/compiler/compiler.rs | 11 ++++++-- lang/src/parser.rs | 2 ++ lang/src/vm/instruction.rs | 2 ++ lang/src/vm/value.rs | 12 ++++++-- lang/src/vm/vm.rs | 52 +++++++++++++++++++++++++++-------- 7 files changed, 66 insertions(+), 19 deletions(-) diff --git a/lang/examples/oop.modu b/lang/examples/oop.modu index 1ce8c9b..81db9b1 100644 --- a/lang/examples/oop.modu +++ b/lang/examples/oop.modu @@ -39,8 +39,9 @@ print("\n---\n"); class xCounter extends Counter { fn init(start, mult) { - //super.init(start); TODO: add super - self.value = start; + super.init(start); + print(super.init); + self.mult = mult; } diff --git a/lang/src/ast.rs b/lang/src/ast.rs index cd9d24a..c3f5cc8 100644 --- a/lang/src/ast.rs +++ b/lang/src/ast.rs @@ -25,6 +25,7 @@ pub enum Expr { Identifier(String), Bool(bool), Return(Box>), + Null, Break, Continue, diff --git a/lang/src/compiler/compiler.rs b/lang/src/compiler/compiler.rs index 0e150c7..d320f64 100644 --- a/lang/src/compiler/compiler.rs +++ b/lang/src/compiler/compiler.rs @@ -230,8 +230,13 @@ impl Compiler { } Expr::PropertyAccess { object, property } => { - self.compile_expr(*object.clone())?; - self.emit(Instruction::GetProperty(property.clone()), span); + if matches!(&object.node, Expr::Identifier(n) if n == "super") { + self.emit(Instruction::LoadLocal(0), span); + self.emit(Instruction::GetSuper(property.clone()), span); + } else { + self.compile_expr(*object.clone())?; + self.emit(Instruction::GetProperty(property.clone()), span); + } } Expr::IndexAccess { object, index } => { @@ -695,7 +700,7 @@ impl Compiler { } } - let class_value = Value::Class { name: name.clone(), methods: methods_map }; + let class_value = Value::Class { name: name.clone(), methods: methods_map, parent_methods: HashMap::new() }; let index = self.add_constant(class_value); self.emit(Instruction::Push(index), span); diff --git a/lang/src/parser.rs b/lang/src/parser.rs index 1bc81d5..7e69fb1 100644 --- a/lang/src/parser.rs +++ b/lang/src/parser.rs @@ -35,6 +35,8 @@ fn parser<'src>() -> impl Parser< (Token::String(name), span) => SpannedExpr { node: Expr::String(name), span }, (Token::Identifier(name), span) => SpannedExpr { node: Expr::Identifier(name), span }, (Token::Bool(b), span) => SpannedExpr { node: Expr::Bool(b), span }, + (Token::Super, span) => SpannedExpr { node: Expr::Identifier("super".to_string()), span }, + (Token::Null, span) => SpannedExpr { node: Expr::Null, span }, (Token::Break, span) => SpannedExpr { node: Expr::Break, span }, (Token::Continue, span) => SpannedExpr { node: Expr::Continue, span }, diff --git a/lang/src/vm/instruction.rs b/lang/src/vm/instruction.rs index 03b7c1e..922ef9a 100644 --- a/lang/src/vm/instruction.rs +++ b/lang/src/vm/instruction.rs @@ -30,7 +30,9 @@ pub enum Instruction { MakeArray(usize), MakeObject(usize), MakeRange { inclusive: bool }, + Extend, + GetSuper(String), GetProperty(String), SetProperty(String), diff --git a/lang/src/vm/value.rs b/lang/src/vm/value.rs index a93a6e0..d1892d0 100644 --- a/lang/src/vm/value.rs +++ b/lang/src/vm/value.rs @@ -27,11 +27,13 @@ pub enum Value { Class { name: String, methods: HashMap, + parent_methods: HashMap, }, Instance { class_name: String, properties: HashMap, + parent_methods: HashMap, }, InstanceFn { @@ -94,11 +96,15 @@ impl PartialEq for Value { (Value::Function { chunk_id: a_id, arity: a_arity }, Value::Function { chunk_id: b_id, arity: b_arity }) => a_id == b_id && a_arity == b_arity, (Value::NativeFn(a), Value::NativeFn(b)) => a.name == b.name, (Value::BuiltinFn(a), Value::BuiltinFn(b)) => a.name == b.name, - (Value::Class { name: a_name, methods: a_methods }, Value::Class { name: b_name, methods: b_methods }) => a_name == b_name && a_methods == b_methods, ( - Value::Instance { class_name: a_class, properties: a_props }, - Value::Instance { class_name: b_class, properties: b_props } + Value::Class { name: a_name, methods: a_methods, .. }, + Value::Class { name: b_name, methods: b_methods, .. } + ) => a_name == b_name && a_methods == b_methods, + + ( + Value::Instance { class_name: a_class, properties: a_props, .. }, + Value::Instance { class_name: b_class, properties: b_props, .. } ) => a_class == b_class && a_props == b_props, ( diff --git a/lang/src/vm/vm.rs b/lang/src/vm/vm.rs index 47fe1bf..59628ba 100644 --- a/lang/src/vm/vm.rs +++ b/lang/src/vm/vm.rs @@ -400,13 +400,14 @@ impl VM { }); } - Value::Class { name, methods } => { + Value::Class { name, methods, parent_methods } => { let args: Vec = self.stack.drain(self.stack.len() - argc..).collect(); self.stack.pop(); let instance = Value::Instance { class_name: name.clone(), properties: methods.clone(), + parent_methods: parent_methods.clone() }; if let Some(Value::Function { chunk_id, arity }) = methods.get("init") { @@ -543,13 +544,14 @@ impl VM { } } - Value::Class { name, methods } => { + Value::Class { name, methods, parent_methods } => { let args: Vec = self.stack.drain(self.stack.len() - argc..).collect(); self.stack.pop(); let instance = Value::Instance { class_name: name.clone(), properties: methods.clone(), + parent_methods: parent_methods.clone() }; if let Some(Value::Function { chunk_id, arity }) = methods.get("init") { @@ -941,7 +943,7 @@ impl VM { self.stack.push(Value::NativeFn(method)); } - Value::Instance { class_name, properties } => { + Value::Instance { class_name, properties, .. } => { if let Some(v) = properties.get(name) { match v { Value::Function { chunk_id, arity } => { @@ -1007,9 +1009,9 @@ impl VM { Value::Object(properties) } - Value::Instance { class_name, mut properties } => { + Value::Instance { class_name, mut properties, parent_methods } => { properties.insert(name.clone(), value); - Value::Instance { class_name, properties } + Value::Instance { class_name, properties, parent_methods } } t => { @@ -1224,12 +1226,14 @@ impl VM { let child = self.stack.pop().unwrap_or(Value::Null); match (child, parent) { - (Value::Class { name, mut methods }, Value::Class { methods: parent_methods, .. }) => { + (Value::Class { name, mut methods, .. }, Value::Class { methods: parent_methods, .. }) => { + let saved_methods = parent_methods.clone(); + for (k, v) in parent_methods { methods.entry(k).or_insert(v); } - self.stack.push(Value::Class { name, methods }); + self.stack.push(Value::Class { name, methods, parent_methods: saved_methods }); } (_, _) => { @@ -1239,6 +1243,30 @@ impl VM { } } + Instruction::GetSuper(name) => { + let inst = self.stack.pop().unwrap_or(Value::Null); + + match inst.clone() { + Value::Instance { parent_methods, .. } => { + if let Some(Value::Function { chunk_id, arity }) = parent_methods.get(name) { + self.stack.push(Value::InstanceFn { + instance: Box::new(inst), + chunk_id: *chunk_id, + arity: *arity, + }); + } else { + self.handle_error(format!("super has no method '{}'", name), span)?; + continue; + } + } + + _ => { + self.handle_error("super cannot be used outside of a class".to_string(), span)?; + continue; + } + } + } + Instruction::Pop => { self.stack.pop(); } @@ -1400,14 +1428,16 @@ fn remap(value: Value, offset: usize) -> Value { elems.into_iter().map(|v| remap(v, offset)).collect() ), - Value::Class { name, methods } => Value::Class { + Value::Class { name, methods, parent_methods } => Value::Class { name, - methods: methods.into_iter().map(|(k, v)| (k, remap(v, offset))).collect() + methods: methods.into_iter().map(|(k, v)| (k, remap(v, offset))).collect(), + parent_methods: parent_methods.into_iter().map(|(k, v)| (k, remap(v, offset))).collect() }, - Value::Instance { class_name, properties } => Value::Instance { + Value::Instance { class_name, properties, parent_methods } => Value::Instance { class_name, - properties: properties.into_iter().map(|(k, v)| (k, remap(v, offset))).collect() + properties: properties.into_iter().map(|(k, v)| (k, remap(v, offset))).collect(), + parent_methods: parent_methods.into_iter().map(|(k, v)| (k, remap(v, offset))).collect() }, Value::InstanceFn { instance, chunk_id, arity } => Value::InstanceFn { From 0b9aaf8075ac7b79579d406676502c70846975ca Mon Sep 17 00:00:00 2001 From: cyteon Date: Wed, 25 Mar 2026 19:06:31 +0100 Subject: [PATCH 3/4] fix compiler and vm bug with super --- lang/src/compiler/compiler.rs | 12 ++++++++---- lang/src/vm/vm.rs | 8 ++++++-- lang/tests/cases/oop.expected | 2 ++ lang/tests/cases/oop.modu | 19 ++++++++++++++++++- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lang/src/compiler/compiler.rs b/lang/src/compiler/compiler.rs index d320f64..b976043 100644 --- a/lang/src/compiler/compiler.rs +++ b/lang/src/compiler/compiler.rs @@ -214,10 +214,14 @@ impl Compiler { if let Expr::PropertyAccess { object, property: _ } = &callee.node { let (target_local, target_global) = match &object.node { Expr::Identifier(name) => { - match self.scope.resolve(name) { - Variable::Local(index) => (Some(index), None), - Variable::Global(_) => (None, Some(name.to_string())), - } + if name == "super" { + (Some(0), None) + } else { + match self.scope.resolve(name) { + Variable::Local(index) => (Some(index), None), + Variable::Global(_) => (None, Some(name.to_string())), + } + } } _ => (None, None), diff --git a/lang/src/vm/vm.rs b/lang/src/vm/vm.rs index 59628ba..7e5338b 100644 --- a/lang/src/vm/vm.rs +++ b/lang/src/vm/vm.rs @@ -637,8 +637,12 @@ impl VM { if matches!(ns, Value::Instance { .. }) { match target { Variable::Local(slot) => { - if *slot < self.stack.len() { - self.stack[*slot] = ns; + let caller_base = self.frames.last() + .map(|f| f.base) + .unwrap_or(0); + + if caller_base + *slot < self.stack.len() { + self.stack[caller_base + *slot] = ns; } } diff --git a/lang/tests/cases/oop.expected b/lang/tests/cases/oop.expected index 7e939e8..f8271cf 100644 --- a/lang/tests/cases/oop.expected +++ b/lang/tests/cases/oop.expected @@ -1,2 +1,4 @@ 2 -2 +8 +16 diff --git a/lang/tests/cases/oop.modu b/lang/tests/cases/oop.modu index e848b5e..ecb7e9f 100644 --- a/lang/tests/cases/oop.modu +++ b/lang/tests/cases/oop.modu @@ -20,4 +20,21 @@ print(counter.value); counter.value = -1; counter.dec(); -print(counter.value); \ No newline at end of file +print(counter.value); + +class xCounter extends Counter { + fn init(start, mult) { + super.init(start); + self.mult = mult; + } + + fn mul() { + self.value *= self.mult; + } +} + +let xc = xCounter(8, 2); +print(xc.value); + +xc.mul(); +print(xc.value); \ No newline at end of file From 5a3f7064dfaf241a3d6d657769ba405067da142b Mon Sep 17 00:00:00 2001 From: cyteon Date: Wed, 25 Mar 2026 19:13:01 +0100 Subject: [PATCH 4/4] syntax updates --- extensions/zed/extension.toml | 2 +- extensions/zed/languages/modu/highlights.scm | 5 ++++- lang/src/cli/repl.rs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/zed/extension.toml b/extensions/zed/extension.toml index cd295b9..478a0ee 100644 --- a/extensions/zed/extension.toml +++ b/extensions/zed/extension.toml @@ -8,4 +8,4 @@ repository = "https://github.com/cyteon/modu" [grammars.modu] repository = "https://github.com/cyteon/tree-sitter-modu" -rev = "e84f411c28e4792066dc3bf19015c48d9adc5e1a" +rev = "a320fb973d93ac5024ad0a9c608b92634603b4d3" diff --git a/extensions/zed/languages/modu/highlights.scm b/extensions/zed/languages/modu/highlights.scm index 7afddd5..f7310e6 100644 --- a/extensions/zed/languages/modu/highlights.scm +++ b/extensions/zed/languages/modu/highlights.scm @@ -16,11 +16,14 @@ "or" "try" "catch" + "extends" ] @keyword +(self) @variable.special +(super) @variable.special + (boolean) @constant.builtin (null) @constant.builtin -(self) @variable.special (string) @string (number) @number (comment) @comment diff --git a/lang/src/cli/repl.rs b/lang/src/cli/repl.rs index e8f142e..74961cc 100644 --- a/lang/src/cli/repl.rs +++ b/lang/src/cli/repl.rs @@ -22,7 +22,7 @@ pub struct Syntax { impl Syntax { pub fn new() -> Self { Self { - keyword_re: Regex::new(r"\b(if|else|fn|let|const|import|as|return|loop|break|continue|for|while|and|or|in|not in|class|self|try|catch)\b").unwrap(), + keyword_re: Regex::new(r"\b(if|else|fn|let|const|import|as|return|loop|break|continue|for|while|and|or|in|not in|class|self|try|catch|extends|super)\b").unwrap(), string_re: Regex::new(r#""([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap(), comment_re: Regex::new(r"//.*$|/\*.*?\*/").unwrap(), number_re: Regex::new(r"\b\d(?:_?\d)*\b").unwrap(),