2024-09-24 09:03:34 -07:00
|
|
|
use std::fmt::{self, Debug, Display};
|
|
|
|
|
use std::rc::Rc;
|
|
|
|
|
|
|
|
|
|
use gc::{Finalize, Trace};
|
|
|
|
|
|
|
|
|
|
use crate::obj::macros::*;
|
Remove BaseObj and fix Object::equals
* BaseObj felt a bit redundant. For everything that BaseObj did, we use
Obj instead.
* Object::equals was a little weird. It was used for giving back
equality, except when it wasn't. It's a little better defined now,
here's what I'm shooting for:
* *In general*, Object::equals will return true when two objects
refer to the same object.
* The exception to this rule is for "constant" objects, or "copy on
write" objects. These include, but are not limited to: Int, Float,
Bool, Nil, Str. Their base values are immutable and are the heart
of object equality.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-10 20:30:24 -07:00
|
|
|
use crate::obj::prelude::*;
|
Add function yielding and resuming
Sometimes, a builtin function may need to call out to another function
(user-defined or otherwise). Previously, we were just calling the
function and popping the stack frame, leaving no room for the new
function to be called. This introduces a `FunctionResult` and
`FunctionState` that get passed between these builtin functions. A
builtin function will receive a FunctionState that tells it whether it
is currently beginning or being resumed and can act accordingly. A
builtin function will in turn return a FunctionResult, which can either
be to return and push a value to the stack, return without pushing a
value (value is already on top of the stack), or yield execution back to
the VM (implying that a new stack frame has been pushed with a new
function to execute).
Having to call a new function and resume is a bit unwieldy and
un-ergonomic, and making a macro to help write these would be nice, but
it looks like a procedural macro may be required to really enable this.
For now, we will write these yields by hand and once it becomes truly
too much, we can start looking at writing a macro library to handle this
case.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 11:34:07 -07:00
|
|
|
use crate::vm::{Argc, Chunk, Frame, Function, Vm};
|
|
|
|
|
|
2024-09-24 09:03:34 -07:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
2024-09-26 11:07:12 -07:00
|
|
|
// BuiltinFunction
|
2024-09-24 09:03:34 -07:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
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;
|
2024-09-24 09:03:34 -07:00
|
|
|
|
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)]
|
2024-09-26 11:07:12 -07:00
|
|
|
pub struct BuiltinFunction {
|
Remove BaseObj and fix Object::equals
* BaseObj felt a bit redundant. For everything that BaseObj did, we use
Obj instead.
* Object::equals was a little weird. It was used for giving back
equality, except when it wasn't. It's a little better defined now,
here's what I'm shooting for:
* *In general*, Object::equals will return true when two objects
refer to the same object.
* The exception to this rule is for "constant" objects, or "copy on
write" objects. These include, but are not limited to: Int, Float,
Bool, Nil, Str. Their base values are immutable and are the heart
of object equality.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-10 20:30:24 -07:00
|
|
|
base: Obj,
|
Add function yielding and resuming
Sometimes, a builtin function may need to call out to another function
(user-defined or otherwise). Previously, we were just calling the
function and popping the stack frame, leaving no room for the new
function to be called. This introduces a `FunctionResult` and
`FunctionState` that get passed between these builtin functions. A
builtin function will receive a FunctionState that tells it whether it
is currently beginning or being resumed and can act accordingly. A
builtin function will in turn return a FunctionResult, which can either
be to return and push a value to the stack, return without pushing a
value (value is already on top of the stack), or yield execution back to
the VM (implying that a new stack frame has been pushed with a new
function to execute).
Having to call a new function and resume is a bit unwieldy and
un-ergonomic, and making a macro to help write these would be nice, but
it looks like a procedural macro may be required to really enable this.
For now, we will write these yields by hand and once it becomes truly
too much, we can start looking at writing a macro library to handle this
case.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 11:34:07 -07:00
|
|
|
#[unsafe_ignore_trace]
|
|
|
|
|
name: Rc<String>,
|
2024-09-24 09:03:34 -07:00
|
|
|
#[unsafe_ignore_trace]
|
|
|
|
|
function: BuiltinFunctionPtr,
|
|
|
|
|
arity: Argc,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
impl BuiltinFunction {
|
2024-09-24 09:03:34 -07:00
|
|
|
pub fn new(name: impl ToString, function: BuiltinFunctionPtr, arity: Argc) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
base: Default::default(),
|
Add function yielding and resuming
Sometimes, a builtin function may need to call out to another function
(user-defined or otherwise). Previously, we were just calling the
function and popping the stack frame, leaving no room for the new
function to be called. This introduces a `FunctionResult` and
`FunctionState` that get passed between these builtin functions. A
builtin function will receive a FunctionState that tells it whether it
is currently beginning or being resumed and can act accordingly. A
builtin function will in turn return a FunctionResult, which can either
be to return and push a value to the stack, return without pushing a
value (value is already on top of the stack), or yield execution back to
the VM (implying that a new stack frame has been pushed with a new
function to execute).
Having to call a new function and resume is a bit unwieldy and
un-ergonomic, and making a macro to help write these would be nice, but
it looks like a procedural macro may be required to really enable this.
For now, we will write these yields by hand and once it becomes truly
too much, we can start looking at writing a macro library to handle this
case.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 11:34:07 -07:00
|
|
|
name: Rc::new(name.to_string()),
|
2024-09-24 09:03:34 -07:00
|
|
|
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> {
|
2024-09-24 09:03:34 -07:00
|
|
|
&self.name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
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 {
|
2024-09-24 09:03:34 -07:00
|
|
|
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}>",
|
2024-09-24 09:03:34 -07:00
|
|
|
self.name(),
|
|
|
|
|
self.arity().unwrap(),
|
|
|
|
|
self.function as *const BuiltinFunctionPtr as usize
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
impl Object for BuiltinFunction {
|
2024-09-24 09:03:34 -07:00
|
|
|
fn arity(&self) -> Option<Argc> {
|
|
|
|
|
Some(self.arity)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn call(&self, vm: &mut Vm, argc: Argc) {
|
Add function yielding and resuming
Sometimes, a builtin function may need to call out to another function
(user-defined or otherwise). Previously, we were just calling the
function and popping the stack frame, leaving no room for the new
function to be called. This introduces a `FunctionResult` and
`FunctionState` that get passed between these builtin functions. A
builtin function will receive a FunctionState that tells it whether it
is currently beginning or being resumed and can act accordingly. A
builtin function will in turn return a FunctionResult, which can either
be to return and push a value to the stack, return without pushing a
value (value is already on top of the stack), or yield execution back to
the VM (implying that a new stack frame has been pushed with a new
function to execute).
Having to call a new function and resume is a bit unwieldy and
un-ergonomic, and making a macro to help write these would be nice, but
it looks like a procedural macro may be required to really enable this.
For now, we will write these yields by hand and once it becomes truly
too much, we can start looking at writing a macro library to handle this
case.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 11:34:07 -07:00
|
|
|
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),
|
Add function yielding and resuming
Sometimes, a builtin function may need to call out to another function
(user-defined or otherwise). Previously, we were just calling the
function and popping the stack frame, leaving no room for the new
function to be called. This introduces a `FunctionResult` and
`FunctionState` that get passed between these builtin functions. A
builtin function will receive a FunctionState that tells it whether it
is currently beginning or being resumed and can act accordingly. A
builtin function will in turn return a FunctionResult, which can either
be to return and push a value to the stack, return without pushing a
value (value is already on top of the stack), or yield execution back to
the VM (implying that a new stack frame has been pushed with a new
function to execute).
Having to call a new function and resume is a bit unwieldy and
un-ergonomic, and making a macro to help write these would be nice, but
it looks like a procedural macro may be required to really enable this.
For now, we will write these yields by hand and once it becomes truly
too much, we can start looking at writing a macro library to handle this
case.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 11:34:07 -07:00
|
|
|
vm.stack().len() - (argc as usize),
|
|
|
|
|
);
|
|
|
|
|
vm.push_frame(new_frame);
|
2024-09-24 09:03:34 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-25 10:22:03 -07:00
|
|
|
impl_base_obj!(BuiltinFunction);
|
2024-09-24 09:03:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
2024-09-26 11:07:12 -07:00
|
|
|
// UserFunction
|
2024-09-24 09:03:34 -07:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
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)]
|
2024-09-26 11:07:12 -07:00
|
|
|
pub struct UserFunction {
|
Remove BaseObj and fix Object::equals
* BaseObj felt a bit redundant. For everything that BaseObj did, we use
Obj instead.
* Object::equals was a little weird. It was used for giving back
equality, except when it wasn't. It's a little better defined now,
here's what I'm shooting for:
* *In general*, Object::equals will return true when two objects
refer to the same object.
* The exception to this rule is for "constant" objects, or "copy on
write" objects. These include, but are not limited to: Int, Float,
Bool, Nil, Str. Their base values are immutable and are the heart
of object equality.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-10 20:30:24 -07:00
|
|
|
base: Obj,
|
2024-09-24 09:03:34 -07:00
|
|
|
#[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]
|
2024-09-24 09:03:34 -07:00
|
|
|
name: Rc<String>,
|
|
|
|
|
#[unsafe_ignore_trace]
|
|
|
|
|
chunk: Rc<Chunk>,
|
|
|
|
|
arity: Argc,
|
|
|
|
|
captures: Vec<ObjP>,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
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 {
|
2024-09-24 09:03:34 -07:00
|
|
|
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()),
|
2024-09-24 09:03:34 -07:00
|
|
|
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
|
|
|
|
|
}
|
2024-09-24 09:03:34 -07:00
|
|
|
|
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> {
|
2024-09-24 09:03:34 -07:00
|
|
|
&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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
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 {
|
2024-09-24 09:03:34 -07:00
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
impl Object for UserFunction {
|
2024-09-24 09:03:34 -07:00
|
|
|
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");
|
Add function yielding and resuming
Sometimes, a builtin function may need to call out to another function
(user-defined or otherwise). Previously, we were just calling the
function and popping the stack frame, leaving no room for the new
function to be called. This introduces a `FunctionResult` and
`FunctionState` that get passed between these builtin functions. A
builtin function will receive a FunctionState that tells it whether it
is currently beginning or being resumed and can act accordingly. A
builtin function will in turn return a FunctionResult, which can either
be to return and push a value to the stack, return without pushing a
value (value is already on top of the stack), or yield execution back to
the VM (implying that a new stack frame has been pushed with a new
function to execute).
Having to call a new function and resume is a bit unwieldy and
un-ergonomic, and making a macro to help write these would be nice, but
it looks like a procedural macro may be required to really enable this.
For now, we will write these yields by hand and once it becomes truly
too much, we can start looking at writing a macro library to handle this
case.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-24 11:34:07 -07:00
|
|
|
let new_frame = Frame::new(
|
|
|
|
|
Rc::clone(&self.name),
|
|
|
|
|
Function::Chunk(Rc::clone(&self.chunk)),
|
|
|
|
|
vm.stack().len() - (argc as usize),
|
|
|
|
|
);
|
2024-09-24 09:03:34 -07:00
|
|
|
vm.push_frame(new_frame);
|
|
|
|
|
for capture in &self.captures {
|
|
|
|
|
vm.push(capture.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-25 10:22:03 -07:00
|
|
|
impl_base_obj!(UserFunction);
|
2024-09-24 09:03:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
2024-09-26 11:07:12 -07:00
|
|
|
// Method
|
2024-09-24 09:03:34 -07:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
2024-09-26 10:52:47 -07:00
|
|
|
#[derive(Trace, Finalize)]
|
2024-09-26 11:07:12 -07:00
|
|
|
pub struct Method {
|
Remove BaseObj and fix Object::equals
* BaseObj felt a bit redundant. For everything that BaseObj did, we use
Obj instead.
* Object::equals was a little weird. It was used for giving back
equality, except when it wasn't. It's a little better defined now,
here's what I'm shooting for:
* *In general*, Object::equals will return true when two objects
refer to the same object.
* The exception to this rule is for "constant" objects, or "copy on
write" objects. These include, but are not limited to: Int, Float,
Bool, Nil, Str. Their base values are immutable and are the heart
of object equality.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-10-10 20:30:24 -07:00
|
|
|
base: Obj,
|
2024-09-24 09:03:34 -07:00
|
|
|
self_binding: ObjP,
|
|
|
|
|
function: ObjP,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
impl Method {
|
2024-09-24 09:03:34 -07:00
|
|
|
pub fn new(self_binding: ObjP, function: ObjP) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
base: Default::default(),
|
|
|
|
|
self_binding,
|
|
|
|
|
function,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-25 10:22:03 -07:00
|
|
|
pub fn create(self_binding: ObjP, function: ObjP) -> ObjP {
|
2024-09-24 09:03:34 -07:00
|
|
|
let ptr = make_ptr(Self::new(self_binding, function));
|
2024-09-25 10:22:03 -07:00
|
|
|
ptr.borrow_mut().instantiate();
|
2024-09-24 09:03:34 -07:00
|
|
|
ptr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn self_binding(&self) -> &ObjP {
|
|
|
|
|
&self.self_binding
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
impl Display for Method {
|
2024-09-24 09:03:34 -07:00
|
|
|
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}>",
|
2024-10-01 09:31:27 -07:00
|
|
|
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
|
|
|
|
|
)
|
2024-09-24 09:03:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 11:07:12 -07:00
|
|
|
impl Object for Method {
|
2024-09-24 09:03:34 -07:00
|
|
|
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);
|
2024-10-01 09:31:27 -07:00
|
|
|
vm.stack_mut().insert(self_pos, self.self_binding().clone());
|
2024-09-24 09:03:34 -07:00
|
|
|
argc += 1;
|
|
|
|
|
|
|
|
|
|
self.function.borrow().call(vm, argc)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-25 10:22:03 -07:00
|
|
|
impl_base_obj!(Method);
|
2024-09-24 09:03:34 -07:00
|
|
|
}
|