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 3abdd01..7ee40a5 100644 Binary files a/extensions/vscode/modu-lang-1.9.0.vsix and b/extensions/vscode/modu-lang-1.10.0.vsix differ 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/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/examples/oop.modu b/lang/examples/oop.modu index 32dc13d..81db9b1 100644 --- a/lang/examples/oop.modu +++ b/lang/examples/oop.modu @@ -33,4 +33,31 @@ 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); + print(super.init); + + 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..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, @@ -139,6 +140,7 @@ pub enum Expr { Class { name: String, methods: Vec>, + parent: Option, }, Try { 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(), diff --git a/lang/src/compiler/compiler.rs b/lang/src/compiler/compiler.rs index 09d7b0b..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), @@ -230,11 +234,15 @@ 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 } => { self.compile_expr(*object.clone())?; self.compile_expr(*index.clone())?; @@ -655,7 +663,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 { @@ -696,10 +704,19 @@ 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); + + 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..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 }, @@ -436,13 +438,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..922ef9a 100644 --- a/lang/src/vm/instruction.rs +++ b/lang/src/vm/instruction.rs @@ -31,6 +31,9 @@ pub enum Instruction { MakeObject(usize), MakeRange { inclusive: bool }, + Extend, + GetSuper(String), + GetProperty(String), SetProperty(String), IndexGet, 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 4777922..7e5338b 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") { @@ -635,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; } } @@ -941,7 +947,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 +1013,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 => { @@ -1219,6 +1225,52 @@ 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, .. }) => { + 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, parent_methods: saved_methods }); + } + + (_, _) => { + self.handle_error("class can only extend a class".to_string(), span)?; + continue; + } + } + } + + 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(); } @@ -1380,14 +1432,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 { 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 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; }