Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::rc::Rc;
|
2024-09-20 16:04:30 -07:00
|
|
|
|
2024-09-24 09:03:34 -07:00
|
|
|
use crate::obj::function::*;
|
2024-09-20 16:04:30 -07:00
|
|
|
use crate::obj::*;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
pub enum Op {
|
|
|
|
|
// Stack functions
|
|
|
|
|
Pop,
|
|
|
|
|
PushConstant(LongOpArg),
|
|
|
|
|
|
|
|
|
|
// Variables
|
|
|
|
|
GetLocal(LocalIndex),
|
|
|
|
|
SetLocal(LocalIndex),
|
|
|
|
|
GetGlobal(GlobalId),
|
|
|
|
|
SetGlobal(GlobalId),
|
|
|
|
|
|
|
|
|
|
// Attributes
|
|
|
|
|
GetAttr(ConstantId),
|
|
|
|
|
SetAttr(ConstantId),
|
|
|
|
|
|
|
|
|
|
// Jumps
|
|
|
|
|
Jump(JumpOpArg),
|
|
|
|
|
JumpFalse(JumpOpArg),
|
|
|
|
|
JumpTrue(JumpOpArg),
|
|
|
|
|
|
|
|
|
|
// Functions
|
|
|
|
|
Call(Argc),
|
|
|
|
|
Return,
|
|
|
|
|
CloseOver { depth: ShortOpArg, slot: ShortOpArg },
|
|
|
|
|
|
|
|
|
|
// VM control
|
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
|
|
|
Nop,
|
2024-09-20 16:04:30 -07:00
|
|
|
Halt,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub type LineRange = (usize, usize);
|
|
|
|
|
|
|
|
|
|
type ShortOpArg = u16;
|
|
|
|
|
type LongOpArg = u32;
|
|
|
|
|
|
|
|
|
|
pub type JumpOpArg = i32;
|
|
|
|
|
pub type LocalIndex = LongOpArg;
|
|
|
|
|
pub type LocalSlot = ShortOpArg;
|
|
|
|
|
pub type ConstantId = LongOpArg;
|
|
|
|
|
pub type GlobalId = LongOpArg;
|
|
|
|
|
pub type Argc = LongOpArg;
|
|
|
|
|
pub type FrameDepth = ShortOpArg;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Local {
|
|
|
|
|
pub(crate) slot: LocalSlot,
|
|
|
|
|
pub(crate) index: LocalIndex,
|
|
|
|
|
pub(crate) name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
|
|
|
pub struct Chunk {
|
|
|
|
|
pub(crate) code: Vec<Op>,
|
|
|
|
|
pub(crate) lines: Vec<LineRange>,
|
|
|
|
|
pub(crate) locals: Vec<Local>,
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub(crate) enum Function {
|
|
|
|
|
Chunk(Rc<Chunk>),
|
|
|
|
|
Builtin(BuiltinFunctionPtr, FunctionState),
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 16:04:30 -07:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Frame {
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
pub(crate) name: Rc<String>,
|
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
|
|
|
pub(crate) function: Function,
|
2024-09-20 16:04:30 -07:00
|
|
|
pub(crate) ip: usize,
|
|
|
|
|
pub(crate) stack_base: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Frame {
|
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
|
|
|
pub fn new(name: Rc<String>, function: Function, stack_base: usize) -> Self {
|
2024-09-20 16:04:30 -07:00
|
|
|
Self {
|
|
|
|
|
name,
|
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
|
|
|
function,
|
2024-09-20 16:04:30 -07:00
|
|
|
ip: 0,
|
|
|
|
|
stack_base,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Vm {
|
|
|
|
|
constants: Vec<ObjP>,
|
|
|
|
|
//global_names: Vec<String>,
|
|
|
|
|
globals: Vec<ObjP>,
|
|
|
|
|
stack: Vec<ObjP>,
|
|
|
|
|
frames: Vec<Frame>,
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
builtins: HashMap<String, ObjP>,
|
2024-09-20 16:04:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Vm {
|
|
|
|
|
/// Create a new virtual machine with the given chunk, constants, and global names.
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
pub fn new(
|
|
|
|
|
chunk: Rc<Chunk>,
|
|
|
|
|
constants: Vec<ObjP>,
|
|
|
|
|
global_names: Vec<String>,
|
|
|
|
|
builtins: HashMap<String, ObjP>,
|
|
|
|
|
) -> Self {
|
2024-09-20 16:04:30 -07:00
|
|
|
// set up globals
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
let nil = builtins.create_nil();
|
|
|
|
|
let mut globals: Vec<_> = global_names.iter().map(|_| ObjP::clone(&nil)).collect();
|
2024-09-20 16:04:30 -07:00
|
|
|
|
|
|
|
|
let mut register_global = |name: &str, value: ObjP| {
|
|
|
|
|
let index = global_names
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|global| global == name)
|
|
|
|
|
.expect("could not find global");
|
|
|
|
|
globals[index] = value;
|
|
|
|
|
};
|
|
|
|
|
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
for (name, builtin) in builtins.iter() {
|
|
|
|
|
register_global(&name, ObjP::clone(&builtin));
|
|
|
|
|
}
|
2024-09-20 16:04:30 -07:00
|
|
|
|
|
|
|
|
// stack and frames
|
|
|
|
|
let stack = Vec::new();
|
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 frames = vec![Frame::new(
|
|
|
|
|
"__main__".to_string().into(),
|
|
|
|
|
Function::Chunk(chunk),
|
|
|
|
|
0,
|
|
|
|
|
)];
|
2024-09-20 16:04:30 -07:00
|
|
|
|
|
|
|
|
Vm {
|
|
|
|
|
constants,
|
|
|
|
|
//global_names,
|
|
|
|
|
globals,
|
|
|
|
|
stack,
|
|
|
|
|
frames,
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
builtins,
|
2024-09-20 16:04:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the stack.
|
|
|
|
|
pub fn stack(&self) -> &Vec<ObjP> {
|
|
|
|
|
&self.stack
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-24 08:44:05 -07:00
|
|
|
/// Get the stack, mutably.
|
|
|
|
|
pub fn stack_mut(&mut self) -> &mut Vec<ObjP> {
|
|
|
|
|
&mut self.stack
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/// Gets the current stack, starting at the frame's stack base.
|
|
|
|
|
pub fn frame_stack(&self) -> &[ObjP] {
|
|
|
|
|
&self.stack()[self.frame().stack_base..]
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-20 16:04:30 -07:00
|
|
|
/// Current stack frame.
|
|
|
|
|
pub fn frame(&self) -> &Frame {
|
|
|
|
|
self.frames.last().unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Current stack frame, mutably.
|
|
|
|
|
pub fn frame_mut(&mut self) -> &mut Frame {
|
|
|
|
|
self.frames.last_mut().unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Push a new stack frame.
|
|
|
|
|
pub fn push_frame(&mut self, frame: Frame) {
|
|
|
|
|
self.frames.push(frame);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Pop the current stack frame.
|
|
|
|
|
pub fn pop_frame(&mut self) -> Frame {
|
|
|
|
|
self.frames.pop().expect("no frame")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets the chunk of the currently executing frame.
|
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
|
|
|
pub fn chunk(&self) -> Option<&Chunk> {
|
|
|
|
|
if let Function::Chunk(chunk) = &self.frame().function {
|
|
|
|
|
Some(chunk)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2024-09-20 16:04:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Instruction pointer of the current frame.
|
|
|
|
|
pub fn ip(&self) -> usize {
|
|
|
|
|
self.frame().ip
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Update the current instruction pointer.
|
|
|
|
|
pub fn set_ip(&mut self, ip: usize) {
|
|
|
|
|
self.frame_mut().ip = ip;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
/// Gets the line of the current instruction.
|
|
|
|
|
fn line(&self, offset: isize) -> LineRange {
|
|
|
|
|
let index = (((self.ip() as isize) + offset).max(0) as usize).min(self.chunk().lines.len());
|
|
|
|
|
self.chunk().lines[index]
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/// Get the current instruction and advance the IP.
|
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
|
|
|
///
|
|
|
|
|
/// This may actually end up calling a function on top of the stack, if it's a builtin function.
|
|
|
|
|
fn dispatch(&mut self) -> Op {
|
|
|
|
|
let mut ip = self.ip();
|
|
|
|
|
let op = match &self.frame().function {
|
|
|
|
|
Function::Chunk(chunk) => {
|
|
|
|
|
let op = chunk.code[ip];
|
|
|
|
|
ip += 1;
|
|
|
|
|
op
|
|
|
|
|
}
|
|
|
|
|
Function::Builtin(function, state) => {
|
|
|
|
|
// keep track of where the current frame index is in case we need to yield
|
|
|
|
|
let frame_index = self.frames.len() - 1;
|
|
|
|
|
let result = (function)(self, *state);
|
|
|
|
|
match result {
|
|
|
|
|
FunctionResult::ReturnPush(value) => {
|
|
|
|
|
// push value to the stack and let the VM handle return protocols
|
|
|
|
|
self.push(value);
|
|
|
|
|
Op::Return
|
|
|
|
|
}
|
|
|
|
|
// value is already on top of the stack, let the VM handle return protocols
|
|
|
|
|
FunctionResult::Return => Op::Return,
|
|
|
|
|
// new stack frame has been pushed, yield control while keeping track of the
|
|
|
|
|
// old state
|
|
|
|
|
FunctionResult::Yield(resume_state) => {
|
|
|
|
|
// update the current state
|
|
|
|
|
if let Function::Builtin(_function, ref mut state) =
|
|
|
|
|
&mut self.frames[frame_index].function
|
|
|
|
|
{
|
|
|
|
|
*state = FunctionState::Resume(resume_state)
|
|
|
|
|
} else {
|
|
|
|
|
panic!("function stack got really messed up - function stack was changed under us");
|
|
|
|
|
}
|
|
|
|
|
// inject a no-op so the VM can load a new instruction or dispatch a new
|
|
|
|
|
// instruction.
|
|
|
|
|
Op::Nop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
self.set_ip(ip);
|
|
|
|
|
op
|
2024-09-20 16:04:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Pop a value from the stack.
|
|
|
|
|
pub fn pop(&mut self) -> ObjP {
|
|
|
|
|
self.stack.pop().expect("stack empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Peek the top value of the stack.
|
|
|
|
|
pub fn peek(&self) -> ObjP {
|
|
|
|
|
self.stack.last().map(Ptr::clone).expect("stack empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Push a value to the stack.
|
|
|
|
|
pub fn push(&mut self, value: ObjP) {
|
|
|
|
|
self.stack.push(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn run(&mut self) {
|
2024-09-23 20:59:00 -07:00
|
|
|
// Cached pointers that we just always want to have on hand
|
|
|
|
|
let method_type = self.builtins().get("Method").unwrap().clone();
|
|
|
|
|
|
2024-09-20 16:04:30 -07:00
|
|
|
loop {
|
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
|
|
|
match self.dispatch() {
|
2024-09-20 16:04:30 -07:00
|
|
|
Op::Pop => {
|
|
|
|
|
self.pop();
|
|
|
|
|
}
|
|
|
|
|
Op::PushConstant(constant_id) => {
|
|
|
|
|
let constant = Ptr::clone(&self.constants[constant_id as usize]);
|
|
|
|
|
self.push(constant);
|
|
|
|
|
}
|
|
|
|
|
Op::GetLocal(local_index) => {
|
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 local = &self.chunk().expect("no chunk").locals[local_index as usize];
|
2024-09-20 16:04:30 -07:00
|
|
|
let value =
|
|
|
|
|
Ptr::clone(&self.stack[self.frame().stack_base + local.slot as usize]);
|
|
|
|
|
self.push(value);
|
|
|
|
|
}
|
|
|
|
|
Op::SetLocal(local_index) => {
|
|
|
|
|
let value = self.pop();
|
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 local = &self.chunk().expect("no chunk").locals[local_index as usize];
|
2024-09-20 16:04:30 -07:00
|
|
|
let index = self.frame().stack_base + local.slot as usize;
|
|
|
|
|
self.stack[index] = value;
|
|
|
|
|
}
|
|
|
|
|
Op::GetGlobal(global_index) => {
|
|
|
|
|
let value = Ptr::clone(&self.globals[global_index as usize]);
|
|
|
|
|
self.push(value);
|
|
|
|
|
}
|
|
|
|
|
Op::SetGlobal(global_index) => {
|
|
|
|
|
let value = self.pop();
|
|
|
|
|
self.globals[global_index as usize] = value;
|
|
|
|
|
}
|
|
|
|
|
Op::GetAttr(constant_id) => {
|
|
|
|
|
// need both declarations to borrow cell value
|
|
|
|
|
let name_obj = Ptr::clone(&self.constants[constant_id as usize]);
|
|
|
|
|
let name =
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
with_obj_downcast(name_obj, |name: &StrInst| Rc::clone(&name.str_value()));
|
2024-09-20 16:04:30 -07:00
|
|
|
let owner = self.pop();
|
2024-09-23 20:59:00 -07:00
|
|
|
let value =
|
|
|
|
|
owner
|
|
|
|
|
.borrow_mut()
|
|
|
|
|
.get_attr_lazy(owner.clone(), method_type.clone(), &name);
|
2024-09-20 16:04:30 -07:00
|
|
|
if let Some(value) = value {
|
|
|
|
|
self.push(value);
|
|
|
|
|
} else {
|
|
|
|
|
// TODO Vm::run, Op::GetAttr - throw an exception when the attribute
|
|
|
|
|
// doesn't exist
|
|
|
|
|
// BLOCKED-ON: exceptions
|
|
|
|
|
todo!(
|
|
|
|
|
"throw an error because we couldn't read attr '{}' on '{}'",
|
|
|
|
|
name,
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
owner.borrow(),
|
2024-09-20 16:04:30 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Op::SetAttr(constant_id) => {
|
|
|
|
|
let name_obj = Ptr::clone(&self.constants[constant_id as usize]);
|
|
|
|
|
let name =
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
with_obj_downcast(name_obj, |name: &StrInst| Rc::clone(&name.str_value()));
|
2024-09-20 16:04:30 -07:00
|
|
|
let value = self.pop();
|
|
|
|
|
let target = self.pop();
|
|
|
|
|
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
target.borrow_mut().set_attr(&name, value);
|
2024-09-20 16:04:30 -07:00
|
|
|
}
|
|
|
|
|
Op::Jump(offset) => {
|
|
|
|
|
let base = (self.ip() - 1) as JumpOpArg;
|
|
|
|
|
assert!(base + offset > 0, "tried to jump to negative IP");
|
|
|
|
|
self.set_ip((base + offset) as usize);
|
|
|
|
|
}
|
|
|
|
|
Op::JumpFalse(offset) => {
|
|
|
|
|
let base = (self.ip() - 1) as JumpOpArg;
|
|
|
|
|
let value = self.peek();
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
if !value.borrow().is_truthy() {
|
2024-09-20 16:04:30 -07:00
|
|
|
self.set_ip((base + offset) as usize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Op::JumpTrue(offset) => {
|
|
|
|
|
let base = (self.ip() - 1) as JumpOpArg;
|
|
|
|
|
let value = self.peek();
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
if value.borrow().is_truthy() {
|
2024-09-20 16:04:30 -07:00
|
|
|
self.set_ip((base + offset) as usize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Op::Call(argc) => {
|
2024-09-24 08:44:05 -07:00
|
|
|
let argc = argc;
|
|
|
|
|
let index = self.stack.len() - (argc as usize) - 1;
|
2024-09-20 16:04:30 -07:00
|
|
|
let fun_ptr = Ptr::clone(&self.stack[index]);
|
|
|
|
|
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
let arity = if let Some(arity) = fun_ptr.borrow().arity() {
|
2024-09-24 08:44:05 -07:00
|
|
|
arity
|
2024-09-20 16:04:30 -07:00
|
|
|
} else {
|
|
|
|
|
// TODO Vm::run, Op::Call - throw an exception when the value isn't
|
|
|
|
|
// callable
|
|
|
|
|
// BLOCKED-ON: exceptions
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
todo!(
|
|
|
|
|
"throw an error because we couldn't call {}",
|
|
|
|
|
fun_ptr.borrow()
|
|
|
|
|
);
|
2024-09-20 16:04:30 -07:00
|
|
|
};
|
|
|
|
|
|
2024-09-24 08:44:05 -07:00
|
|
|
// Check arity
|
2024-09-20 16:04:30 -07:00
|
|
|
if arity != argc {
|
|
|
|
|
// TODO Vm::run, Op::Call - throw an exception when the number of arguments
|
|
|
|
|
// does not match the function's arity
|
|
|
|
|
// BLOCKED-ON: exceptions
|
|
|
|
|
todo!(
|
|
|
|
|
"throw an error because we passed the wrong number of arguments to {}",
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
fun_ptr.borrow()
|
2024-09-20 16:04:30 -07:00
|
|
|
);
|
|
|
|
|
}
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
fun_ptr.borrow().call(self, argc as Argc);
|
2024-09-20 16:04:30 -07:00
|
|
|
}
|
|
|
|
|
Op::Return => {
|
|
|
|
|
let return_value = self.pop();
|
|
|
|
|
let old_frame = self.frames.pop().unwrap();
|
|
|
|
|
// stack_base is always going to be <= current stack size
|
|
|
|
|
self.stack
|
|
|
|
|
.resize_with(old_frame.stack_base, || unreachable!());
|
|
|
|
|
// also pop the function object off of the stack
|
|
|
|
|
self.stack.pop();
|
|
|
|
|
self.push(return_value);
|
|
|
|
|
}
|
|
|
|
|
Op::CloseOver { depth, slot } => {
|
|
|
|
|
// since we're closing over a value, and functions ultimately come from
|
|
|
|
|
// constants, we want to deep-clone this object so we don't alter any live
|
|
|
|
|
// objects.
|
|
|
|
|
// there is some room for optimization here so we aren't cloning the entire
|
|
|
|
|
// UserFunctionInst for every individual capture in a function.
|
|
|
|
|
let fun_ptr = self.pop();
|
|
|
|
|
let mut fun: UserFunctionInst =
|
|
|
|
|
with_obj_downcast(fun_ptr, UserFunctionInst::clone);
|
|
|
|
|
let frame_index = self.frames.len() - (depth as usize) - 1;
|
|
|
|
|
let stack_base = self.frames[frame_index].stack_base;
|
|
|
|
|
let value = Ptr::clone(&self.stack[stack_base + (slot as usize)]);
|
|
|
|
|
fun.push_capture(value);
|
2024-09-23 20:59:00 -07:00
|
|
|
self.push(make_ptr(fun));
|
2024-09-20 16:04:30 -07:00
|
|
|
}
|
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
|
|
|
Op::Nop => {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2024-09-20 16:04:30 -07:00
|
|
|
Op::Halt => {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
Revamp object system, start using `gc` crate
Wow, what a ride. I think everything should be working now. In short:
* Objects use the `gc` crate, which as a `Gc` garbage-collected pointer
type. I may choose to implement my own in contiguous memory in the
future. We will see.
* The type system is no longer global. This is a bit of a burden,
because now, whenever you want to create a new object, you need to
pass its type object into the `Obj::instantiate` method, as well as
its `::create` static method.
* This burden is somewhat alleviated by the `ObjFactory` trait, which
helps create new objects as long as you have access to a `builtins`
hashmap. So something that would normally look like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = upcast_obj(BuiltinFunctionInst::create(
ObjP::clone(&builtins.get("BuiltinFunction").unwrap()),
"print",
print,
1
);
builtins.insert("print".to_string(), print_builtin)
// other builtins inserted here...
}
now looks like this:
fn init_builtins(builtins: &mut HashMap<String, ObjP>) {
let print_builtin = builtins.create_builtin_function("print", print, 1);
builtins.insert("print".to_string(), print_builtin);
}
(turns out, if all you need is a HashMap<String, ObjP>, you can
implement ObjFactory for HashMap<String, ObjP> itself(!))
Overall, I'm happier with this design, and I think this is what is going
to get merged. It's a little weird to be querying type names that are
used in the language itself to get those type objects, but whatever
works, I guess.
Next up is vtables.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2024-09-23 18:12:32 -07:00
|
|
|
|
|
|
|
|
impl ObjFactory for Vm {
|
|
|
|
|
fn builtins(&self) -> &HashMap<String, ObjP> {
|
|
|
|
|
&self.builtins
|
|
|
|
|
}
|
|
|
|
|
}
|