Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement virtual callstack for functions #3087

Closed
wants to merge 1 commit into from

Conversation

jedel1043
Copy link
Member

@jedel1043 jedel1043 commented Jun 29, 2023

This is the first prototype of a vm design that uses a virtual callstack for JS functions and Rust functions.

Right now it's full of hacks and compatibility patches in order to be able to progressively experiment and bugfix, but that'll be solved when this PR is finished.

It currently uses the next-gen crate to emulate generators in stable, which is the way this design unifies JS calls with Rust calls.

By the way, this is probably blocked on rust-lang/rust#68923, which is why I drafted this implementation for the moment.

Design notes

All functions now return a JsResult<CallResult> struct, defined as:

pub enum CallResult<T> {
    ///
    Value(T),
    ///
    Coroutine(JsCoroutine),
    ///
    DirectCall(CallContext),
}

pub type JsCoroutine =
    Pin<Box<dyn Generator<JsResult<JsValue>, Yield = CallContext, Return = JsResult<JsValue>>>>;

pub struct CallContext {
    pub(crate) f: JsObject,
    pub(crate) this: JsValue,
    pub(crate) args: ThinVec<JsValue>,
}

This new return value can signal the VM about what to do on several cases:

  • If a function doesn't need to call any JS functions, it can directly return Value, which indicates to the VM that the function finished executing and doesn't need to be pushed to the virtual callstack.
  • If a function returns DirectCall(CallContext), it's a tail call, meaning the VM can directly call the inner function without storing the caller's context.
  • If a function returns Coroutine (JsCoroutine), it signals the VM that the callee has inner calls to other JS functions.

The coroutine design looks a bit daunting, but it's essentially divided in steps:

  1. The native Rust function returns a generator with inner yields instead of direct calls to JS functions. Each yield corresponds to a JS call that needs to be resolved by the VM.
  2. The VM receives the coroutine and pushes it forward until it yields, receiving the JS function that needs to be called in order for the coroutine to progress.
  3. The VM prepares the JS call and stores the coroutine in a coroutine stack for future resumption.
  4. The JS function is called, returning a result.
  5. The VM resumes the coroutine with the result of the JS call.
  6. The VM repeats steps 2 to 5 if the coroutine yields again.
  7. The coroutine returns its final result.

This bridges the gap between Rust functions and JS functions, making it possible to track both of them with a single callstack :D

cc @HalidOdat

@jedel1043 jedel1043 added the blocked Waiting for another code change label Jun 29, 2023
@jedel1043 jedel1043 changed the title Implement virtual callstack for functions Implement virtual callstack for functions with coroutines Jun 29, 2023
@HalidOdat
Copy link
Member

HalidOdat commented Jun 30, 2023

Took a look at the implementation looks good, but I had some concerns about complicating the the implementation of functions and that it would make #3041 impossible or very hard (since we need to store function pointer in a list that can be indexed, we can't serialize and deserialize the pointer but we can an index). using the #[generator] macro really obscures this and the state can't be serialized/deserialized (this may/may not apply to closure functions)

Since we have a solution for JavaScript functions (as I mentioned in #3044), So I thought we could have some built-in functions implemented in JavaScript (at least the ones that call functions a lot, like forEach).

Benefits:

  • Easier to implement built-in functions
  • Once we implement inlining optimization we will be able to inline builtin functions (since they have bytecode)
  • Faster rust build-times, less code to compile.
  • No need to add generator function type.
  • Makes implementing snapshots a bit easier since we don't have to a big array of function pointers.

Disadvantages:

  • We would have to parse and compile the code at runtime (once we fully implement snapshots, this wont be an issue)
  • Maybe? increased memory usage, instead of storing machine code, we store a CodeBlock (we have a lot of room to improve our bytecode) also having Reusing Codeblocks between Contexts is not properly supported #2626 would allow use to share it between Contexts
  • Less efficient functions, probably will be the case until we implement jiting, we could generate better bytecode though, having intrisics like ToNumber so when calling ToNumber(arg) it directly emits GetName "arg" followed by ToNumber opcode, like wise for very common spec function's ToString, RequireObjectCoercible, this would require small change in the bytecompiler (we already do this for direct eval calls that emits EvalCall opcode)

I checked some engines to see what they do, and apparently they have something very simular to this, spidermonkey has this, v8 had this too, but deprecated it in favor of creating a domain specific language Torque

What do you guys think? @boa-dev/maintainers

@jedel1043
Copy link
Member Author

jedel1043 commented Jun 30, 2023

@HalidOdat Yeah, completely agree. This was mostly an experiment to see if we could leverage generator crates for the virtual stack, but seeing the many difficulties I had while implementing this, I think we'll have to wait until there's proper support for coroutines in Rust.

I'll remove the coroutine parts of this PR and only leave the virtual stack for JS to JS calls.

@jedel1043 jedel1043 changed the title Implement virtual callstack for functions with coroutines Implement virtual callstack for functions Jun 30, 2023
@jedel1043
Copy link
Member Author

Closing this since Rust still needs to support this properly, and rebasing this will require a lot more work than writing it from scratch.

@jedel1043 jedel1043 closed this Nov 29, 2023
@jedel1043 jedel1043 deleted the rust-js-call-refactor branch November 29, 2023 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blocked Waiting for another code change
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants