Add struct methods - #845
Conversation
|
Thank you! Before I do a deeper review, did you see my draft in #800? That PR is mostly LLM-generated (I may have written some tests myself), which is why I haven't merged it yet. I mostly wanted to explore the design space here. I'm fine abandoning the I haven't checked this PR in detail, but there are some things that we should definitely be able to handle. For example, we need to distinguish between methods on generic structs and generic methods on (potentially generic) structs. Both of these should work as expected. There are also some implementation-related questions. For example, it would be good to share code between free functions and methods as much as possible. All that is to say: I would appreciate if we could discuss the design more before going ahead with one of these two implementations. |
|
Hey mate,
I wasn’t aware of #800 — I’ve just gone through it. It looks like it’s aiming to achieve essentially the same goal as this PR, but instead of using
In my version, instance and static/constructor methods are distinguished syntactically:
The call syntax mirrors Rust:
Fields remain immutable (consistent with the rest of the language), so instance methods cannot mutate fields.
So overall, this part intentionally mirrors Rust’s model.
Here’s my view on the I think With inline methods:
The main argument in favor of
If I understand correctly, my current implementation already handles this. The comprehensive example I posted in my original comment demonstrates generic structs and generic methods working together. Let me know if I’m misunderstanding the edge case you're referring to.
That was my initial approach. However, I didn’t like how much additional branching it introduced into the free-function handling code. Specifically, in To keep things cleaner, I separated method handling from free functions to avoid excessive branching. That said, I’m open to revisiting this — there may be a cleaner unification approach that I missed. Related to this, I’ve also been thinking about decorator-style annotations for methods, e.g.: This would allow something like: Users could define their own indexing behavior, which would let us implement Matrix and Tensor types as library-level abstractions (like in Rust), instead of introducing new primitive types. Keeping method handling separate from free functions would likely make future extensions like this cleaner and reduce branching complexity in the interpreter.
Absolutely — I agree that design alignment should come first. One additional thought: If the language is meant to be purely functional, we should consider the memory implications of returning We may want to think about how Let me know where you’d like to steer this — I’m happy to adjust the implementation once we settle on the design direction. |
Pre-register struct methods before compiling their bodies so forward and mutual method recursion resolves correctly at bytecode compile time.\n\nAdd VM support for compiling into reserved method chunks.\n\nExpand struct-method edge-case tests in parser, typechecker, and interpreter suites.
Allow 'Self { ... }' in struct-construction expressions when typechecking inside struct methods.
Add parser/typechecker/interpreter coverage for 'Self { ... }' usage, including constructor and instance method paths.
Previously, struct instances stored fields as Vec<Value> and VM field access used a mutable/remove-style path. Non-generic struct instantiation in the typechecker also rebuilt equivalent instantiated StructInfo values repeatedly.
This change:
- stores Value::StructInstance fields as Arc<[Value]>;
- builds struct instances directly as Arc<[Value]> in the VM;
- accesses struct fields via borrowed slice + clone of selected field (no swap_remove path);
- updates FFI struct construction/unpacking callsites for the shared immutable field storage;
- caches non-generic struct instantiations in the typechecker and invalidates on struct redefinition.
Example of prior inefficiency in Numbat:
struct Builder {
a: Scalar,
b: Scalar,
c: Scalar,
fn new() = Builder { a: 0, b: 0, c: 0 }
fn with_a(self, a: Scalar) = Self { a: a, b: self.b, c: self.c }
fn with_b(self, b: Scalar) = Self { a: self.a, b: b, c: self.c }
fn with_c(self, c: Scalar) = Self { a: self.a, b: self.b, c: c }
}
Builder::new().with_a(1).with_b(2).with_c(3)
In this chained builder pattern, cloning intermediate struct values is now much cheaper because struct payload clones are Arc refcount bumps rather than Vec payload clones.
Reduce overlapping struct-method coverage while preserving unique parser/typechecker/runtime behavior checks.
Test suite changes:
- Removed duplicated interpreter struct-method scenarios that were already covered by focused namespace/recursion tests.
- Removed duplicated typechecker success cases for simple Self-return updates now covered by broader nested/generic/where/Self scenarios.
- Kept distinct negative-path checks (constructor-vs-instance misuse, wrong arity, dimension mismatch, missing method, Self outside methods).
Docs/examples changes:
- Clarified struct method semantics in the book (Self { ... } support and immutable return semantics).
- Updated syntax example source to use Self { ... } for constructor/update methods.
- Regenerated the autogenerated syntax reference page from examples/numbat_syntax.nbt.
Validation:
- cargo test -p numbat
|
Closing this in favor of #847. The struct-method work from this branch has been consolidated into the draft PR, so this branch is now superseded. |
Add Inline Struct Methods With Struct-Scoped Dispatch
This PR adds inline struct methods (definition and calling) while keeping method resolution scoped to the struct.
Example Syntax
Calling Modes
Type::method(...)value.method(...)Type checking enforces that constructor methods are called with
::and instance methods are called with..Why This Design
I decided against a separate
implblock approach (like Rust/Swift) because I wanted methods defined alongside the struct body and to avoid introducing additional language surface and constraints.Method Metadata + Dispatch (Simple Overview)
Struct methods are represented and executed through a struct-scoped path from typechecking to runtime:
DefineFunctionfor global functionsDefineMethodfor struct methods(struct_name, method_name).This gives predictable struct-local lookup and avoids name collisions with global/prelude/imported functions.
MISC
This PR relates with #795 and the broader direction discussed with @sharkdp around stronger struct features, which are needed for larger library work (including differential-equation-oriented code). I recognize this might not be a desired feature for the language, but I think it's a natural extension of the current struct syntax and semantics, and it unlocks a lot of expressiveness and code organization benefits.