Skip to content

Commit 237ccc6

Browse files
Class inheritance: extends Parent + chained method dispatch
Sixth piece of the Python-ergonomics catch-up. Extends the class system from the previous commit with single inheritance via the `extends` keyword. Method dispatch walks the parent chain when the child class doesn't define the method. Syntax: class Animal { name; fn greet(self) { return concat_many(\"hi from \", dict_get(self, \"name\")); } fn species(self) { return \"generic animal\"; } } class Dog extends Animal { name; breed; fn species(self) { # override return concat_many(\"dog (\", dict_get(self, \"breed\"), \")\"); } fn bark(self) { return concat_many(dict_get(self, \"name\"), \" says woof\"); } } class Puppy extends Dog { ... } # multi-level chains work h d = Dog(\"rex\", \"lab\"); d.greet() # inherited from Animal d.species() # overridden in Dog → \"dog (lab)\" d.bark() # Dog's own method Implementation: AST: Statement::ClassDef gains `parent: Option<String>` field. Parser: Token::Extends keyword; parse_class_def reads optional `extends Parent` clause between name and the LBrace. Interpreter: - New `class_parents: HashMap<String, String>` table on the Interpreter, populated by execute_stmt when ClassDef runs. - Method dispatch in call_function walks the parent chain: try `<Child>__<method>`, then `<Parent>__<method>`, etc., bounded to 64 hops as a cycle-safety net. - Chain walk happens BEFORE module-qualified dispatch so instance method lookup still wins over dotted module calls. Formatter: Prints `class Name extends Parent { ... }` when parent is set. Tests (examples/tests/test_classes.omc — 4 new, all pass): - test_inherited_method: Dog inherits Animal.greet without redefining - test_override: Dog.species overrides Animal.species - test_own_method: Dog.bark not on Animal - test_multi_level_inheritance: Puppy chains 2 hops up to Animal.greet, 1 hop up to Dog.bark, overrides both at species What's NOT yet: - super() calls. Method overrides currently can't invoke the parent's implementation explicitly. Could be sugar for the mangled parent name lookup; deferred to a later session. - Multiple inheritance / mixins / MRO. Single-parent chain only. - Class methods / static methods. All methods are instance methods. - Construction calls parent's constructor automatically. Right now the child controls its own fields; no auto-init from parent. Regression: 11 class tests (was 7 + 4 new) + 8 exception + 10 fstring + 10 regex + 17 JSON + 57 substrate + 70 builtins + 18 harmonic libs + 16 heal = 217 OMC tests, all pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 222034f commit 237ccc6

5 files changed

Lines changed: 124 additions & 13 deletions

File tree

examples/tests/test_classes.omc

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,62 @@ fn test_self_mutation() {
119119
c.increment();
120120
assert_eq(dict_get(c, "value"), 3, "increment chain");
121121
}
122+
123+
# ---- Inheritance ----
124+
125+
class Animal {
126+
name;
127+
fn greet(self) {
128+
return concat_many("hi from ", dict_get(self, "name"));
129+
}
130+
fn species(self) {
131+
return "generic animal";
132+
}
133+
}
134+
135+
class Dog extends Animal {
136+
name;
137+
breed;
138+
fn species(self) {
139+
return concat_many("dog (", dict_get(self, "breed"), ")");
140+
}
141+
fn bark(self) {
142+
return concat_many(dict_get(self, "name"), " says woof");
143+
}
144+
}
145+
146+
class Puppy extends Dog {
147+
name;
148+
breed;
149+
fn species(self) {
150+
# Override even the Dog's override.
151+
return concat_many("puppy (", dict_get(self, "breed"), ")");
152+
}
153+
}
154+
155+
fn test_inherited_method() {
156+
h d = Dog("rex", "lab");
157+
# greet is defined on Animal; Dog doesn't override it.
158+
assert_eq(d.greet(), "hi from rex", "inherited greet");
159+
}
160+
161+
fn test_override() {
162+
h a = Animal("alice");
163+
h d = Dog("rex", "lab");
164+
assert_eq(a.species(), "generic animal", "Animal.species");
165+
assert_eq(d.species(), "dog (lab)", "Dog.species override");
166+
}
167+
168+
fn test_own_method() {
169+
h d = Dog("rex", "lab");
170+
assert_eq(d.bark(), "rex says woof", "Dog's own method");
171+
}
172+
173+
fn test_multi_level_inheritance() {
174+
h p = Puppy("buddy", "golden");
175+
# bark is from Dog (1 hop up), greet is from Animal (2 hops up),
176+
# species is Puppy's own (overrides both).
177+
assert_eq(p.bark(), "buddy says woof", "bark from Dog");
178+
assert_eq(p.greet(), "hi from buddy", "greet from Animal (2 hops)");
179+
assert_eq(p.species(), "puppy (golden)", "Puppy's own species");
180+
}

omnimcode-core/src/ast.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,27 @@ pub enum Statement {
106106
/// of stringifying, enabling typed-catch hierarchies.
107107
Throw(Expression),
108108
/// `class Name { field1; field2; fn method1(self, ...) { ... } }`
109-
/// Minimum-viable class system: each ClassDef desugars at
110-
/// register_user_functions time into:
109+
/// (optional `extends Parent` clause for inheritance).
110+
///
111+
/// Each ClassDef desugars at register_user_functions time into:
111112
/// - A constructor fn `Name(field1, field2, ...)` that builds a
112113
/// dict with __class__="Name" plus each field as a key.
113114
/// - One top-level fn per method, name-mangled as `Name__method`.
115+
///
114116
/// Method dispatch `obj.method(args)` works because the
115117
/// call-resolution path checks whether the receiver is a Dict
116118
/// carrying __class__ and routes to the mangled fn name with
117119
/// `obj` injected as the first argument (the `self` slot).
120+
///
121+
/// Inheritance: when `parent` is `Some("Parent")`, the
122+
/// instance's __class__ is still set to the child's name, but
123+
/// method dispatch falls back to the parent's mangled namespace
124+
/// (and recursively up the chain) if the child doesn't define
125+
/// the method. The Interpreter maintains a class_parents table
126+
/// for the lookup.
118127
ClassDef {
119128
name: String,
129+
parent: Option<String>,
120130
fields: Vec<String>,
121131
methods: Vec<Statement>, // each is a FunctionDef
122132
},

omnimcode-core/src/formatter.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,13 @@ fn format_stmt(stmt: &Statement, level: usize, out: &mut String) {
214214
indent_to(level, out);
215215
out.push_str("}\n");
216216
}
217-
Statement::ClassDef { name, fields, methods } => {
217+
Statement::ClassDef { name, parent, fields, methods } => {
218218
out.push_str("class ");
219219
out.push_str(name);
220+
if let Some(p) = parent {
221+
out.push_str(" extends ");
222+
out.push_str(p);
223+
}
220224
out.push_str(" {\n");
221225
for f in fields {
222226
indent_to(level + 1, out);

omnimcode-core/src/interpreter.rs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ pub type JitDispatch =
1515
pub struct Interpreter {
1616
globals: HashMap<String, Value>,
1717
functions: HashMap<String, (Vec<String>, Vec<Statement>)>,
18+
/// Class-parent table for `class Child extends Parent` inheritance.
19+
/// Maps child class name → parent class name. The instance-method
20+
/// dispatch path walks this chain when `<Child>__<method>` isn't
21+
/// found, trying `<Parent>__<method>` and so on.
22+
class_parents: HashMap<String, String>,
1823
/// Optional JIT dispatch hook. When set, `invoke_user_function_at`
1924
/// consults this BEFORE running the tree-walk body. If the hook
2025
/// returns `Some(result)`, that result wins; otherwise tree-walk
@@ -99,6 +104,7 @@ impl Interpreter {
99104
test_current_name: std::cell::RefCell::new(String::new()),
100105
call_stack: Vec::new(),
101106
host_builtins: HashMap::new(),
107+
class_parents: HashMap::new(),
102108
}
103109
}
104110

@@ -1452,11 +1458,14 @@ impl Interpreter {
14521458
self.functions.insert(name.clone(), (params.clone(), body.clone()));
14531459
Ok(())
14541460
}
1455-
Statement::ClassDef { name, fields, methods } => {
1461+
Statement::ClassDef { name, parent, fields, methods } => {
14561462
// Desugar at execute time so the tree-walker doesn't
14571463
// need register_user_functions to have been called.
14581464
// Same logic as register_user_functions::visit:
14591465
// synthesize a constructor + mangled methods.
1466+
if let Some(p) = parent {
1467+
self.class_parents.insert(name.clone(), p.clone());
1468+
}
14601469
let mut ctor_body: Vec<Statement> = Vec::new();
14611470
ctor_body.push(Statement::VarDecl {
14621471
name: "__obj".to_string(),
@@ -2146,6 +2155,10 @@ impl Interpreter {
21462155
// the mangled `<ClassName>__<method>` fn registered at class-
21472156
// definition time, with `obj` injected as the first arg.
21482157
//
2158+
// Inheritance: when the child class doesn't define <method>,
2159+
// walk up the class_parents chain trying `<Parent>__<method>`,
2160+
// `<Grandparent>__<method>`, and so on. First hit wins.
2161+
//
21492162
// This MUST be checked before module-qualified dispatch so
21502163
// that instance dicts aren't accidentally looked up as
21512164
// modules. Identified by: receiver-name is a local variable
@@ -2154,13 +2167,22 @@ impl Interpreter {
21542167
if let Some(Value::Dict(d)) = self.get_var(recv_name) {
21552168
let class_key = d.borrow().get("__class__").cloned();
21562169
if let Some(Value::String(class_name)) = class_key {
2157-
let mangled = format!("{}__{}", class_name, method_name);
2158-
if let Some((params, body)) = self.functions.get(&mangled).cloned() {
2159-
// Build the call arg list: receiver first
2160-
// (binding to `self` if that's the first param
2161-
// name), then the rest. We pass the receiver
2162-
// as a variable expression — the existing
2163-
// `recv_name` resolves to the same dict.
2170+
// Walk class → parent chain, bounded to avoid
2171+
// accidental cycles in a malformed class table.
2172+
let mut current_class: Option<String> = Some(class_name);
2173+
let mut hops = 0usize;
2174+
let mut hit: Option<(String, Vec<String>, Vec<Statement>)> = None;
2175+
while let Some(c) = current_class {
2176+
if hops > 64 { break; } // sanity bound
2177+
let mangled = format!("{}__{}", c, method_name);
2178+
if let Some((params, body)) = self.functions.get(&mangled).cloned() {
2179+
hit = Some((mangled, params, body));
2180+
break;
2181+
}
2182+
current_class = self.class_parents.get(&c).cloned();
2183+
hops += 1;
2184+
}
2185+
if let Some((mangled, params, body)) = hit {
21642186
let mut full_args: Vec<Expression> =
21652187
Vec::with_capacity(args.len() + 1);
21662188
full_args.push(Expression::Variable(recv_name.to_string()));
@@ -7122,7 +7144,14 @@ impl Interpreter {
71227144
fns.insert(name.clone(), (params.clone(), body.clone()));
71237145
for s in body { visit(s, fns); }
71247146
}
7125-
Statement::ClassDef { name, fields, methods } => {
7147+
Statement::ClassDef { name, parent: _parent, fields, methods } => {
7148+
// NOTE: parent registration happens in execute_stmt
7149+
// (which has access to &mut self). visit() only
7150+
// sees &mut HashMap<...> so it can't reach the
7151+
// class_parents table. For the VM-prep path, the
7152+
// class_parents update is made during execute_stmt
7153+
// when the statement actually executes.
7154+
//
71267155
// Desugar: build a constructor fn and one method fn
71277156
// per declared method. The constructor is a body of
71287157
// dict_set calls that populates a fresh dict with

omnimcode-core/src/parser.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub enum Token {
4242
Throw,
4343
Match,
4444
Class,
45+
Extends,
4546
/// f-string template — alternating literal and expression segments.
4647
/// Parser turns this into `concat_many(parts...)` at expression
4748
/// position.
@@ -475,6 +476,7 @@ impl Lexer {
475476
"finally" => Token::Finally,
476477
"throw" => Token::Throw,
477478
"class" => Token::Class,
479+
"extends" => Token::Extends,
478480
"match" => Token::Match,
479481
"and" => Token::And,
480482
"or" => Token::Or,
@@ -1099,6 +1101,13 @@ impl Parser {
10991101
fn parse_class_def(&mut self) -> Result<Statement, String> {
11001102
self.expect(Token::Class)?;
11011103
let name = self.parse_ident()?;
1104+
// Optional `extends Parent` clause.
1105+
let parent = if self.current() == Token::Extends {
1106+
self.advance();
1107+
Some(self.parse_ident()?)
1108+
} else {
1109+
None
1110+
};
11021111
self.expect(Token::LBrace)?;
11031112
let mut fields: Vec<String> = Vec::new();
11041113
let mut methods: Vec<Statement> = Vec::new();
@@ -1117,7 +1126,7 @@ impl Parser {
11171126
}
11181127
}
11191128
self.expect(Token::RBrace)?;
1120-
Ok(Statement::ClassDef { name, fields, methods })
1129+
Ok(Statement::ClassDef { name, parent, fields, methods })
11211130
}
11221131

11231132
/// `try { ... } catch err { ... }` with optional trailing

0 commit comments

Comments
 (0)