Files
not-python-rust/src/obj/function.rs

257 lines
6.3 KiB
Rust
Raw Normal View History

use std::fmt::{self, Debug, Display};
use std::rc::Rc;
use gc::{Finalize, Trace};
use crate::obj::macros::*;
use crate::obj::prelude::*;
use crate::vm::{Argc, Chunk, Frame, Function, Vm};
////////////////////////////////////////////////////////////////////////////////
// BuiltinFunction
////////////////////////////////////////////////////////////////////////////////
Change up how function calls are handled This is a big one. For a while, builtin functions were a bit cumbersome and not easily re-entrant. If you needed to call a function from within a builtin function, the only method of doing so was to take a `FunctionState` parameter, which would either be "Begin", meaning the function was being called for the first time, or "Resume", meaning the function was being re-entered. This meant that if we wanted to call another function within this function, we'd have to set up a whole `match` statement to figure out whether we were re-entering the function or starting out. It was a mess and not very ergonomic, and most importantly, made it very difficult to implement hashmaps. Now, builtin functions are handled a little more elegantly. A native function is pushed to the stack, where it is detected in the `Vm::dispatch()` function. It is then called, like normal. If the builtin function then needs to call *another* function, it will push that function to the stack and call it, and then call `Vm::resume()` to resume VM execution. `Vm::dispatch()` is then called again, this time with the current function on top of the stack. If it's another builtin function, the above is repeated. If it's a user-defined function, then bytecode is executed in the main `loop` inside of resume. Ultimately, we are able to compose builtin functions like we would any other internal function to the program. Overall this should speed things up a little, make them a whole lot easier to read, and make them a million times easier to compose with other builtin parts of Rust. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-11 16:22:19 -07:00
pub type BuiltinFunctionPtr = fn(vm: &mut Vm) -> ObjP;
Change to_repr/to_str implementation story Let's talk about to_repr and to_str. to_repr tries to do what Python's `repr` function does - that is, it converts an object into a developer-readable (but maybe not human-readable) string. This function is implemented for every object, and may very well just write out "<MyType at 0x12345678>". to_str, on the other hand, tries to turn an object into an explicitly human-readable format. In Python (which we are modeling a lot of our design after), the str() function usually will end up calling `repr()` itself, if no other implementation has been provided. Previously in our implementation, there was a bit of a disconnect between `to_repr` and `to_str`, versus `Debug` and `Display`. `to_repr` would kind of do its own thing, and then maybe call either `Display` or `Debug` to format an object. Consequently, `to_str` would kind of do its own thing too - usually calling `to_repr` but not always. This change attempts to strengthen the definitions of `to_repr` and `to_str`. *In general*, a call to `to_repr` should be calling an object's `Debug::fmt` function, and *in general* a call to `to_str()` should be calling an object's `Display::fmt` function. Often, the `Display::fmt` will just end up calling `Debug::fmt` itself, but now the `to_str()` and `to_repr()` interfaces are much better defined than they used to be. The only major downside is that we are giving up the `Debug` implementation for language logic, rather than debugging-the-language-itself logic. I can see this biting us down the road if we ever need a Rust-style `Debug` implementation, but for now, I think this is going to serve our needs just fine. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-27 08:10:09 -07:00
#[derive(Trace, Finalize)]
pub struct BuiltinFunction {
base: Obj,
#[unsafe_ignore_trace]
name: Rc<String>,
#[unsafe_ignore_trace]
function: BuiltinFunctionPtr,
arity: Argc,
}
impl BuiltinFunction {
pub fn new(name: impl ToString, function: BuiltinFunctionPtr, arity: Argc) -> Self {
Self {
base: Default::default(),
name: Rc::new(name.to_string()),
function,
arity,
}
}
impl_create!(
name: impl ToString,
function: BuiltinFunctionPtr,
arity: Argc,
);
Change to_repr/to_str implementation story Let's talk about to_repr and to_str. to_repr tries to do what Python's `repr` function does - that is, it converts an object into a developer-readable (but maybe not human-readable) string. This function is implemented for every object, and may very well just write out "<MyType at 0x12345678>". to_str, on the other hand, tries to turn an object into an explicitly human-readable format. In Python (which we are modeling a lot of our design after), the str() function usually will end up calling `repr()` itself, if no other implementation has been provided. Previously in our implementation, there was a bit of a disconnect between `to_repr` and `to_str`, versus `Debug` and `Display`. `to_repr` would kind of do its own thing, and then maybe call either `Display` or `Debug` to format an object. Consequently, `to_str` would kind of do its own thing too - usually calling `to_repr` but not always. This change attempts to strengthen the definitions of `to_repr` and `to_str`. *In general*, a call to `to_repr` should be calling an object's `Debug::fmt` function, and *in general* a call to `to_str()` should be calling an object's `Display::fmt` function. Often, the `Display::fmt` will just end up calling `Debug::fmt` itself, but now the `to_str()` and `to_repr()` interfaces are much better defined than they used to be. The only major downside is that we are giving up the `Debug` implementation for language logic, rather than debugging-the-language-itself logic. I can see this biting us down the road if we ever need a Rust-style `Debug` implementation, but for now, I think this is going to serve our needs just fine. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-27 08:10:09 -07:00
pub fn name(&self) -> &Rc<String> {
&self.name
}
}
impl Display for BuiltinFunction {
Change to_repr/to_str implementation story Let's talk about to_repr and to_str. to_repr tries to do what Python's `repr` function does - that is, it converts an object into a developer-readable (but maybe not human-readable) string. This function is implemented for every object, and may very well just write out "<MyType at 0x12345678>". to_str, on the other hand, tries to turn an object into an explicitly human-readable format. In Python (which we are modeling a lot of our design after), the str() function usually will end up calling `repr()` itself, if no other implementation has been provided. Previously in our implementation, there was a bit of a disconnect between `to_repr` and `to_str`, versus `Debug` and `Display`. `to_repr` would kind of do its own thing, and then maybe call either `Display` or `Debug` to format an object. Consequently, `to_str` would kind of do its own thing too - usually calling `to_repr` but not always. This change attempts to strengthen the definitions of `to_repr` and `to_str`. *In general*, a call to `to_repr` should be calling an object's `Debug::fmt` function, and *in general* a call to `to_str()` should be calling an object's `Display::fmt` function. Often, the `Display::fmt` will just end up calling `Debug::fmt` itself, but now the `to_str()` and `to_repr()` interfaces are much better defined than they used to be. The only major downside is that we are giving up the `Debug` implementation for language logic, rather than debugging-the-language-itself logic. I can see this biting us down the road if we ever need a Rust-style `Debug` implementation, but for now, I think this is going to serve our needs just fine. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-27 08:10:09 -07:00
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
Debug::fmt(self, fmt)
}
}
impl Debug for BuiltinFunction {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
Change up how function calls are handled This is a big one. For a while, builtin functions were a bit cumbersome and not easily re-entrant. If you needed to call a function from within a builtin function, the only method of doing so was to take a `FunctionState` parameter, which would either be "Begin", meaning the function was being called for the first time, or "Resume", meaning the function was being re-entered. This meant that if we wanted to call another function within this function, we'd have to set up a whole `match` statement to figure out whether we were re-entering the function or starting out. It was a mess and not very ergonomic, and most importantly, made it very difficult to implement hashmaps. Now, builtin functions are handled a little more elegantly. A native function is pushed to the stack, where it is detected in the `Vm::dispatch()` function. It is then called, like normal. If the builtin function then needs to call *another* function, it will push that function to the stack and call it, and then call `Vm::resume()` to resume VM execution. `Vm::dispatch()` is then called again, this time with the current function on top of the stack. If it's another builtin function, the above is repeated. If it's a user-defined function, then bytecode is executed in the main `loop` inside of resume. Ultimately, we are able to compose builtin functions like we would any other internal function to the program. Overall this should speed things up a little, make them a whole lot easier to read, and make them a million times easier to compose with other builtin parts of Rust. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-11 16:22:19 -07:00
"<BuiltinFunction {}/{} at {:#x}>",
self.name(),
self.arity().unwrap(),
self.function as *const BuiltinFunctionPtr as usize
)
}
}
impl Object for BuiltinFunction {
fn arity(&self) -> Option<Argc> {
Some(self.arity)
}
fn call(&self, vm: &mut Vm, argc: Argc) {
let new_frame = Frame::new(
Rc::clone(&self.name),
Change up how function calls are handled This is a big one. For a while, builtin functions were a bit cumbersome and not easily re-entrant. If you needed to call a function from within a builtin function, the only method of doing so was to take a `FunctionState` parameter, which would either be "Begin", meaning the function was being called for the first time, or "Resume", meaning the function was being re-entered. This meant that if we wanted to call another function within this function, we'd have to set up a whole `match` statement to figure out whether we were re-entering the function or starting out. It was a mess and not very ergonomic, and most importantly, made it very difficult to implement hashmaps. Now, builtin functions are handled a little more elegantly. A native function is pushed to the stack, where it is detected in the `Vm::dispatch()` function. It is then called, like normal. If the builtin function then needs to call *another* function, it will push that function to the stack and call it, and then call `Vm::resume()` to resume VM execution. `Vm::dispatch()` is then called again, this time with the current function on top of the stack. If it's another builtin function, the above is repeated. If it's a user-defined function, then bytecode is executed in the main `loop` inside of resume. Ultimately, we are able to compose builtin functions like we would any other internal function to the program. Overall this should speed things up a little, make them a whole lot easier to read, and make them a million times easier to compose with other builtin parts of Rust. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-11 16:22:19 -07:00
Function::Builtin(self.function),
vm.stack().len() - (argc as usize),
);
vm.push_frame(new_frame);
}
impl_base_obj!(BuiltinFunction);
}
////////////////////////////////////////////////////////////////////////////////
// UserFunction
////////////////////////////////////////////////////////////////////////////////
Change to_repr/to_str implementation story Let's talk about to_repr and to_str. to_repr tries to do what Python's `repr` function does - that is, it converts an object into a developer-readable (but maybe not human-readable) string. This function is implemented for every object, and may very well just write out "<MyType at 0x12345678>". to_str, on the other hand, tries to turn an object into an explicitly human-readable format. In Python (which we are modeling a lot of our design after), the str() function usually will end up calling `repr()` itself, if no other implementation has been provided. Previously in our implementation, there was a bit of a disconnect between `to_repr` and `to_str`, versus `Debug` and `Display`. `to_repr` would kind of do its own thing, and then maybe call either `Display` or `Debug` to format an object. Consequently, `to_str` would kind of do its own thing too - usually calling `to_repr` but not always. This change attempts to strengthen the definitions of `to_repr` and `to_str`. *In general*, a call to `to_repr` should be calling an object's `Debug::fmt` function, and *in general* a call to `to_str()` should be calling an object's `Display::fmt` function. Often, the `Display::fmt` will just end up calling `Debug::fmt` itself, but now the `to_str()` and `to_repr()` interfaces are much better defined than they used to be. The only major downside is that we are giving up the `Debug` implementation for language logic, rather than debugging-the-language-itself logic. I can see this biting us down the road if we ever need a Rust-style `Debug` implementation, but for now, I think this is going to serve our needs just fine. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-27 08:10:09 -07:00
#[derive(Clone, Trace, Finalize)]
pub struct UserFunction {
base: Obj,
#[unsafe_ignore_trace]
WIP: Add imports and modules This is a big change because it touches a lot of stuff, but here is the overview: * Import syntax: ``` import foo import bar from foo import bar from "foo.npp" import bar, baz from foo import * from foo import "foo.npp" ``` * These are all valid imports. They should be pretty straightforward, maybe with exception of the last item. If you are importing a path directly, but not importing any members from it, it does not insert anything into the current namespace, and just executes the file. This is probably going to be unused but I want to include it for completeness. We can always remove it later before a hypothetical 1.0 release. * The "from" keyword is only ever used as a keyword here, and I am allowing it to be used as an identifier elsewhere. Don't export it, because that's weird and wrong and won't work. * Modules: * Doing an `import foo` will look for "foo.npp" at compile-time, relative to the importer's directory, parse it, and compile it. The importer will then attempt to execute the module with the new `EnterModule` op. This instruction will execute the module kind of like a function, assigning the module's global namespace to an object that you can pass around. * `import bar from foo` and `import bar from "foo.npp"` et al syntax is not currently implemented in the compiler. * There is a new "Module" object that represents a potentially un-initialized module. This can't be referred to directly in code. * VM: * The VM operates around Module objects now. If you want to "call" a new module, you should call `enter_module`. This is how the main chunk is invoked. * TODOs: * `exit_module` function in the VM * Finish up module implementation in compiler * Built-in modules * Sub-modules - e.g. `import foo.bar` - how does naming work for this? * Module directories. In Python you have `foo/__init__.py` and in Rust you have `foo/mod.rs`. * Probably a "Namespace" object that explicitly denotes "this is an imported module that you're dealing with" * Tests, tests, tests Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-04 10:11:49 -07:00
path: Rc<String>,
#[unsafe_ignore_trace]
name: Rc<String>,
#[unsafe_ignore_trace]
chunk: Rc<Chunk>,
arity: Argc,
captures: Vec<ObjP>,
}
impl UserFunction {
WIP: Add imports and modules This is a big change because it touches a lot of stuff, but here is the overview: * Import syntax: ``` import foo import bar from foo import bar from "foo.npp" import bar, baz from foo import * from foo import "foo.npp" ``` * These are all valid imports. They should be pretty straightforward, maybe with exception of the last item. If you are importing a path directly, but not importing any members from it, it does not insert anything into the current namespace, and just executes the file. This is probably going to be unused but I want to include it for completeness. We can always remove it later before a hypothetical 1.0 release. * The "from" keyword is only ever used as a keyword here, and I am allowing it to be used as an identifier elsewhere. Don't export it, because that's weird and wrong and won't work. * Modules: * Doing an `import foo` will look for "foo.npp" at compile-time, relative to the importer's directory, parse it, and compile it. The importer will then attempt to execute the module with the new `EnterModule` op. This instruction will execute the module kind of like a function, assigning the module's global namespace to an object that you can pass around. * `import bar from foo` and `import bar from "foo.npp"` et al syntax is not currently implemented in the compiler. * There is a new "Module" object that represents a potentially un-initialized module. This can't be referred to directly in code. * VM: * The VM operates around Module objects now. If you want to "call" a new module, you should call `enter_module`. This is how the main chunk is invoked. * TODOs: * `exit_module` function in the VM * Finish up module implementation in compiler * Built-in modules * Sub-modules - e.g. `import foo.bar` - how does naming work for this? * Module directories. In Python you have `foo/__init__.py` and in Rust you have `foo/mod.rs`. * Probably a "Namespace" object that explicitly denotes "this is an imported module that you're dealing with" * Tests, tests, tests Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-04 10:11:49 -07:00
pub fn new(path: impl ToString, chunk: Chunk, arity: Argc) -> Self {
Self {
base: Default::default(),
WIP: Add imports and modules This is a big change because it touches a lot of stuff, but here is the overview: * Import syntax: ``` import foo import bar from foo import bar from "foo.npp" import bar, baz from foo import * from foo import "foo.npp" ``` * These are all valid imports. They should be pretty straightforward, maybe with exception of the last item. If you are importing a path directly, but not importing any members from it, it does not insert anything into the current namespace, and just executes the file. This is probably going to be unused but I want to include it for completeness. We can always remove it later before a hypothetical 1.0 release. * The "from" keyword is only ever used as a keyword here, and I am allowing it to be used as an identifier elsewhere. Don't export it, because that's weird and wrong and won't work. * Modules: * Doing an `import foo` will look for "foo.npp" at compile-time, relative to the importer's directory, parse it, and compile it. The importer will then attempt to execute the module with the new `EnterModule` op. This instruction will execute the module kind of like a function, assigning the module's global namespace to an object that you can pass around. * `import bar from foo` and `import bar from "foo.npp"` et al syntax is not currently implemented in the compiler. * There is a new "Module" object that represents a potentially un-initialized module. This can't be referred to directly in code. * VM: * The VM operates around Module objects now. If you want to "call" a new module, you should call `enter_module`. This is how the main chunk is invoked. * TODOs: * `exit_module` function in the VM * Finish up module implementation in compiler * Built-in modules * Sub-modules - e.g. `import foo.bar` - how does naming work for this? * Module directories. In Python you have `foo/__init__.py` and in Rust you have `foo/mod.rs`. * Probably a "Namespace" object that explicitly denotes "this is an imported module that you're dealing with" * Tests, tests, tests Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-04 10:11:49 -07:00
path: Rc::new(path.to_string()),
name: Rc::new("(anonymous)".to_string()),
chunk: Rc::new(chunk),
arity,
captures: Default::default(),
}
}
WIP: Add imports and modules This is a big change because it touches a lot of stuff, but here is the overview: * Import syntax: ``` import foo import bar from foo import bar from "foo.npp" import bar, baz from foo import * from foo import "foo.npp" ``` * These are all valid imports. They should be pretty straightforward, maybe with exception of the last item. If you are importing a path directly, but not importing any members from it, it does not insert anything into the current namespace, and just executes the file. This is probably going to be unused but I want to include it for completeness. We can always remove it later before a hypothetical 1.0 release. * The "from" keyword is only ever used as a keyword here, and I am allowing it to be used as an identifier elsewhere. Don't export it, because that's weird and wrong and won't work. * Modules: * Doing an `import foo` will look for "foo.npp" at compile-time, relative to the importer's directory, parse it, and compile it. The importer will then attempt to execute the module with the new `EnterModule` op. This instruction will execute the module kind of like a function, assigning the module's global namespace to an object that you can pass around. * `import bar from foo` and `import bar from "foo.npp"` et al syntax is not currently implemented in the compiler. * There is a new "Module" object that represents a potentially un-initialized module. This can't be referred to directly in code. * VM: * The VM operates around Module objects now. If you want to "call" a new module, you should call `enter_module`. This is how the main chunk is invoked. * TODOs: * `exit_module` function in the VM * Finish up module implementation in compiler * Built-in modules * Sub-modules - e.g. `import foo.bar` - how does naming work for this? * Module directories. In Python you have `foo/__init__.py` and in Rust you have `foo/mod.rs`. * Probably a "Namespace" object that explicitly denotes "this is an imported module that you're dealing with" * Tests, tests, tests Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-04 10:11:49 -07:00
impl_create!(path: impl ToString, chunk: Chunk, arity: Argc);
pub fn path(&self) -> &Rc<String> {
&self.path
}
Change to_repr/to_str implementation story Let's talk about to_repr and to_str. to_repr tries to do what Python's `repr` function does - that is, it converts an object into a developer-readable (but maybe not human-readable) string. This function is implemented for every object, and may very well just write out "<MyType at 0x12345678>". to_str, on the other hand, tries to turn an object into an explicitly human-readable format. In Python (which we are modeling a lot of our design after), the str() function usually will end up calling `repr()` itself, if no other implementation has been provided. Previously in our implementation, there was a bit of a disconnect between `to_repr` and `to_str`, versus `Debug` and `Display`. `to_repr` would kind of do its own thing, and then maybe call either `Display` or `Debug` to format an object. Consequently, `to_str` would kind of do its own thing too - usually calling `to_repr` but not always. This change attempts to strengthen the definitions of `to_repr` and `to_str`. *In general*, a call to `to_repr` should be calling an object's `Debug::fmt` function, and *in general* a call to `to_str()` should be calling an object's `Display::fmt` function. Often, the `Display::fmt` will just end up calling `Debug::fmt` itself, but now the `to_str()` and `to_repr()` interfaces are much better defined than they used to be. The only major downside is that we are giving up the `Debug` implementation for language logic, rather than debugging-the-language-itself logic. I can see this biting us down the road if we ever need a Rust-style `Debug` implementation, but for now, I think this is going to serve our needs just fine. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-27 08:10:09 -07:00
pub fn name(&self) -> &Rc<String> {
&self.name
}
pub fn set_name(&mut self, name: Rc<String>) {
self.name = name;
}
pub fn chunk(&self) -> &Chunk {
&self.chunk
}
pub fn push_capture(&mut self, value: ObjP) {
self.captures.push(value);
}
}
impl Display for UserFunction {
Change to_repr/to_str implementation story Let's talk about to_repr and to_str. to_repr tries to do what Python's `repr` function does - that is, it converts an object into a developer-readable (but maybe not human-readable) string. This function is implemented for every object, and may very well just write out "<MyType at 0x12345678>". to_str, on the other hand, tries to turn an object into an explicitly human-readable format. In Python (which we are modeling a lot of our design after), the str() function usually will end up calling `repr()` itself, if no other implementation has been provided. Previously in our implementation, there was a bit of a disconnect between `to_repr` and `to_str`, versus `Debug` and `Display`. `to_repr` would kind of do its own thing, and then maybe call either `Display` or `Debug` to format an object. Consequently, `to_str` would kind of do its own thing too - usually calling `to_repr` but not always. This change attempts to strengthen the definitions of `to_repr` and `to_str`. *In general*, a call to `to_repr` should be calling an object's `Debug::fmt` function, and *in general* a call to `to_str()` should be calling an object's `Display::fmt` function. Often, the `Display::fmt` will just end up calling `Debug::fmt` itself, but now the `to_str()` and `to_repr()` interfaces are much better defined than they used to be. The only major downside is that we are giving up the `Debug` implementation for language logic, rather than debugging-the-language-itself logic. I can see this biting us down the road if we ever need a Rust-style `Debug` implementation, but for now, I think this is going to serve our needs just fine. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-27 08:10:09 -07:00
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
Debug::fmt(self, fmt)
}
}
impl Debug for UserFunction {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"<UserFunction {}/{} at 0x{:x}>",
self.name(),
self.arity().unwrap(),
self as *const _ as usize
)
}
}
impl Object for UserFunction {
fn arity(&self) -> Option<Argc> {
Some(self.arity)
}
fn call(&self, vm: &mut Vm, argc: Argc) {
assert_eq!(argc, self.arity, "argc must match arity");
let new_frame = Frame::new(
Rc::clone(&self.name),
Function::Chunk(Rc::clone(&self.chunk)),
vm.stack().len() - (argc as usize),
);
vm.push_frame(new_frame);
for capture in &self.captures {
vm.push(capture.clone());
}
}
impl_base_obj!(UserFunction);
}
////////////////////////////////////////////////////////////////////////////////
// Method
////////////////////////////////////////////////////////////////////////////////
#[derive(Trace, Finalize)]
pub struct Method {
base: Obj,
self_binding: ObjP,
function: ObjP,
}
impl Method {
pub fn new(self_binding: ObjP, function: ObjP) -> Self {
Self {
base: Default::default(),
self_binding,
function,
}
}
pub fn create(self_binding: ObjP, function: ObjP) -> ObjP {
let ptr = make_ptr(Self::new(self_binding, function));
ptr.borrow_mut().instantiate();
ptr
}
pub fn self_binding(&self) -> &ObjP {
&self.self_binding
}
}
impl Display for Method {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
Change to_repr/to_str implementation story Let's talk about to_repr and to_str. to_repr tries to do what Python's `repr` function does - that is, it converts an object into a developer-readable (but maybe not human-readable) string. This function is implemented for every object, and may very well just write out "<MyType at 0x12345678>". to_str, on the other hand, tries to turn an object into an explicitly human-readable format. In Python (which we are modeling a lot of our design after), the str() function usually will end up calling `repr()` itself, if no other implementation has been provided. Previously in our implementation, there was a bit of a disconnect between `to_repr` and `to_str`, versus `Debug` and `Display`. `to_repr` would kind of do its own thing, and then maybe call either `Display` or `Debug` to format an object. Consequently, `to_str` would kind of do its own thing too - usually calling `to_repr` but not always. This change attempts to strengthen the definitions of `to_repr` and `to_str`. *In general*, a call to `to_repr` should be calling an object's `Debug::fmt` function, and *in general* a call to `to_str()` should be calling an object's `Display::fmt` function. Often, the `Display::fmt` will just end up calling `Debug::fmt` itself, but now the `to_str()` and `to_repr()` interfaces are much better defined than they used to be. The only major downside is that we are giving up the `Debug` implementation for language logic, rather than debugging-the-language-itself logic. I can see this biting us down the road if we ever need a Rust-style `Debug` implementation, but for now, I think this is going to serve our needs just fine. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-27 08:10:09 -07:00
Debug::fmt(self, fmt)
}
}
impl Debug for Method {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
let function_name: Rc<_> = if let Some(function) = self
.function
.borrow()
.as_any()
.downcast_ref::<BuiltinFunction>()
{
Rc::clone(&function.name())
} else if let Some(function) = self
.function
.borrow()
.as_any()
.downcast_ref::<UserFunction>()
{
function.name().clone()
} else {
unreachable!()
};
write!(
fmt,
Change up how function calls are handled This is a big one. For a while, builtin functions were a bit cumbersome and not easily re-entrant. If you needed to call a function from within a builtin function, the only method of doing so was to take a `FunctionState` parameter, which would either be "Begin", meaning the function was being called for the first time, or "Resume", meaning the function was being re-entered. This meant that if we wanted to call another function within this function, we'd have to set up a whole `match` statement to figure out whether we were re-entering the function or starting out. It was a mess and not very ergonomic, and most importantly, made it very difficult to implement hashmaps. Now, builtin functions are handled a little more elegantly. A native function is pushed to the stack, where it is detected in the `Vm::dispatch()` function. It is then called, like normal. If the builtin function then needs to call *another* function, it will push that function to the stack and call it, and then call `Vm::resume()` to resume VM execution. `Vm::dispatch()` is then called again, this time with the current function on top of the stack. If it's another builtin function, the above is repeated. If it's a user-defined function, then bytecode is executed in the main `loop` inside of resume. Ultimately, we are able to compose builtin functions like we would any other internal function to the program. Overall this should speed things up a little, make them a whole lot easier to read, and make them a million times easier to compose with other builtin parts of Rust. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-11 16:22:19 -07:00
"<Method {}.{}/{} at {:#x}>",
self.self_binding().borrow().ty_name(),
Change to_repr/to_str implementation story Let's talk about to_repr and to_str. to_repr tries to do what Python's `repr` function does - that is, it converts an object into a developer-readable (but maybe not human-readable) string. This function is implemented for every object, and may very well just write out "<MyType at 0x12345678>". to_str, on the other hand, tries to turn an object into an explicitly human-readable format. In Python (which we are modeling a lot of our design after), the str() function usually will end up calling `repr()` itself, if no other implementation has been provided. Previously in our implementation, there was a bit of a disconnect between `to_repr` and `to_str`, versus `Debug` and `Display`. `to_repr` would kind of do its own thing, and then maybe call either `Display` or `Debug` to format an object. Consequently, `to_str` would kind of do its own thing too - usually calling `to_repr` but not always. This change attempts to strengthen the definitions of `to_repr` and `to_str`. *In general*, a call to `to_repr` should be calling an object's `Debug::fmt` function, and *in general* a call to `to_str()` should be calling an object's `Display::fmt` function. Often, the `Display::fmt` will just end up calling `Debug::fmt` itself, but now the `to_str()` and `to_repr()` interfaces are much better defined than they used to be. The only major downside is that we are giving up the `Debug` implementation for language logic, rather than debugging-the-language-itself logic. I can see this biting us down the road if we ever need a Rust-style `Debug` implementation, but for now, I think this is going to serve our needs just fine. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-27 08:10:09 -07:00
function_name,
self.function.borrow().arity().unwrap(),
self as *const _ as usize
)
}
}
impl Object for Method {
fn arity(&self) -> Option<Argc> {
// Subtract one from the arity - this is because the VM uses arity() to check against the
// number of arguments passed.
self.function.borrow().arity().map(|arity| arity - 1)
}
fn call(&self, vm: &mut Vm, mut argc: Argc) {
let self_pos = vm.stack().len() - (argc as usize);
vm.stack_mut().insert(self_pos, self.self_binding().clone());
argc += 1;
self.function.borrow().call(vm, argc)
}
impl_base_obj!(Method);
}