Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
4 changes: 2 additions & 2 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/syntaxes/modu.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}]
},

Expand Down
2 changes: 1 addition & 1 deletion extensions/zed/extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ repository = "https://github.com/cyteon/modu"

[grammars.modu]
repository = "https://github.com/cyteon/tree-sitter-modu"
rev = "e84f411c28e4792066dc3bf19015c48d9adc5e1a"
rev = "a320fb973d93ac5024ad0a9c608b92634603b4d3"
5 changes: 4 additions & 1 deletion extensions/zed/languages/modu/highlights.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion lang/examples/oop.modu
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,31 @@ print(counter.get());

counter.reset();
counter.decrement();
print(counter.value);
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);
2 changes: 2 additions & 0 deletions lang/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub enum Expr {
Identifier(String),
Bool(bool),
Return(Box<Spanned<Expr>>),

Null,
Break,
Continue,
Expand Down Expand Up @@ -139,6 +140,7 @@ pub enum Expr {
Class {
name: String,
methods: Vec<Spanned<Expr>>,
parent: Option<String>,
},

Try {
Expand Down
2 changes: 1 addition & 1 deletion lang/src/cli/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
37 changes: 27 additions & 10 deletions lang/src/compiler/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())),
}
}
Comment on lines 214 to +224

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For super.method() calls, the compiler hard-codes target_local = Some(0). Outside a class method this slot may not exist or won’t be the receiver being invoked, which can lead to incorrect self_target replacement behavior on return. Consider reusing the same compile-time validation as for super property access (only allow in methods where local 0 is self) and otherwise emit a compile error.

Copilot uses AI. Check for mistakes.
}

_ => (None, None),
Expand All @@ -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);
}
Comment on lines +237 to +243

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super property access compiles to LoadLocal(0) unconditionally. If super is used outside a class method (e.g. top-level or a normal fn without locals), the VM will try to read stack slot 0 and panic (out-of-bounds) before GetSuper can raise a friendly runtime error. Please gate this at compile-time (e.g. ensure self is defined at local slot 0 / you’re inside a class method) and otherwise return a compile error like "super can only be used inside class methods" (or compile to a safe runtime error without LoadLocal).

Copilot uses AI. Check for mistakes.
}


Expr::IndexAccess { object, index } => {
self.compile_expr(*object.clone())?;
self.compile_expr(*index.clone())?;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}

Expand Down
6 changes: 6 additions & 0 deletions lang/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ pub enum Token {
#[token("class")]
Class,

#[token("extends")]
Extends,

#[token("super")]
Super,

#[token("import")]
Import,

Expand Down
13 changes: 11 additions & 2 deletions lang/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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::<Vec<_>>())
.then(select! { (Token::RBrace, span) => span })
)
.map(|((start, name), ((_lbrace, methods), end)): ((Span, String), ((Span, Vec<SpannedExpr>), Span))| SpannedExpr {
node: Expr::Class { name, methods },
.map(|(((start, name), parent), ((_lbrace, methods), end)): (((Span, String), Option<String>), ((Span, Vec<SpannedExpr>), Span))| SpannedExpr {
node: Expr::Class { name, methods, parent },
span: Span::from(start.start..end.end),
})
.labelled("class declaration");
Expand Down
3 changes: 3 additions & 0 deletions lang/src/vm/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ pub enum Instruction {
MakeObject(usize),
MakeRange { inclusive: bool },

Extend,
GetSuper(String),

GetProperty(String),
SetProperty(String),
IndexGet,
Expand Down
12 changes: 9 additions & 3 deletions lang/src/vm/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ pub enum Value {
Class {
name: String,
methods: HashMap<String, Value>,
parent_methods: HashMap<String, Value>,
},

Instance {
class_name: String,
properties: HashMap<String, Value>,
parent_methods: HashMap<String, Value>,
},

InstanceFn {
Expand Down Expand Up @@ -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,

(
Expand Down
Loading
Loading