Skip to content
This repository has been archived by the owner on Nov 26, 2024. It is now read-only.

Rewrite Go JS core #166

Merged
merged 23 commits into from
Nov 29, 2023
Merged

Rewrite Go JS core #166

merged 23 commits into from
Nov 29, 2023

Conversation

PlasmaPower
Copy link
Contributor

This PR was co-authored with @rachel-bousfield so it should be reviewed by someone else.

This PR is currently a draft because it doesn't implement stylus-specific functionality like callbacks into Go (and comments out stylus tests for now). That said, I think it'd make sense to review this PR in its current state to make sure this approach makes sense before adding on Stylus callbacks. Everything implemented in this PR should be complete and ready for review.

Broadly speaking, this PR largely combines the go-stub wasm-libraries Go JS implementation and the JIT Go JS implementation into a single go-js library, and switches from an imperative programming style to a declarative style.

Before this PR, methods would be implemented with a match statement against the object ID and the method name:

    let value = match class {
        UINT8_ARRAY_ID => match args.get(0) {
            Some(JsValue::Number(size)) => DynamicObject::Uint8Array(vec![0; *size as usize]),
            _ => fail!("Go attempted to construct Uint8Array with bad args: {args:?}"),
        },
        DATE_ID => DynamicObject::Date,

This had two problems. First, code was grouped by things like "object construction" and "method calls", as opposed to being grouped by "filesystem" and "crypto" objects. Second, this relied heavily on assigning static IDs for everything, which didn't work out well as the system got more complicated. When manually assigning IDs, it's easy to mess up reference counting and such. The new approach separates the implementation of JS methods from the implementation details of the JS engine. Instead of the imperative style of a match statement for every possible method, this PR changes to a declarative style where objects and their methods are created as they would be in JS. This is slightly slower, but much clearer, and it's harder to make mistakes in this model. Here's an example:

/// in globals, we create the Date constructor
object.insert_func("Date", |_env, _go, _args| Ok(make_date_object()));

// make_date_object is defined as such:
fn make_date_object() -> JsValue {
    let date = JsObject::default();
    date.insert_func("getTimezoneOffset", |_env, _go, _args| {
        Ok(JsValue::Number(0.))
    });
    date.into()
}

The JS method implementations in runtime.rs never need to care about reference counting or object IDs. They can operate solely on high-level JS values.

Copy link
Contributor

@tsahee tsahee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very initial. didn't go over everything yet.

self.refresh(env, store)
}

/// Refreshes the stack pointer after potentially the Go runtime was potentially resumed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potentially *2

}

impl PartialEq for JsValue {
fn eq(&self, other: &Self) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this SameValueZero (worth a comment)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also - maybe a small comment why you chose that

JsValue::String(x) => write!(f, "{x:?}"),
JsValue::Object(x) => write!(f, "{x:?}"),
JsValue::Uint8Array(x) => write!(f, "{x:?}"),
JsValue::Array(x) => write!(f, "{x:?}"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type should also be displayed somehow..

}

/// Warning: this increments the reference count for the returned id
pub fn value_to_id(&self, value: JsValue) -> JsValueId {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function name should reflect the fact we add a refcount

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to assign_id + a comment :)

// Treat all NaN values as equal
JsValue::Number(x) if x.is_nan() => JsValueEquality::Number(CANONICAL_NAN_BITS),
// Treat all zero values as equal
JsValue::Number(x) if *x == 0. => JsValueEquality::Number(0_f64.to_bits()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this works for both +0. and -0.? even so.. better to just check against both to make it apparent

@rachel-bousfield
Copy link
Contributor

I've added some additional commits, and think the PR is pretty much good to go.

Highlights include

  • the new EVM Api, which introduces a much safer model for calling Go closures in Rust.
  • making call-and-recover an atomic operation, avoiding manual (and error-prone) management of the stack pointer.
  • fixing fs.write and undefined.Type()

@rachel-bousfield rachel-bousfield marked this pull request as ready for review November 2, 2023 05:55
Copy link

codecov bot commented Nov 2, 2023

Codecov Report

Merging #166 (6a135b5) into stylus (0509a98) will decrease coverage by 1.50%.
Report is 2 commits behind head on stylus.
The diff coverage is 81.29%.

Additional details and impacted files
@@            Coverage Diff             @@
##           stylus     #166      +/-   ##
==========================================
- Coverage   59.80%   58.31%   -1.50%     
==========================================
  Files          56      280     +224     
  Lines       11763    43102   +31339     
==========================================
+ Hits         7035    25135   +18100     
- Misses       4728    15502   +10774     
- Partials        0     2465    +2465     

Copy link
Contributor

@tsahee tsahee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still quite initial review and requires some talking.. specifically about when we want to panic and when we don't

We should also add (at least) two tests:

  1. some version of the go-js test with necessary adaptations:
    https://go.dev/src/syscall/js/js_test.go
  2. a test for reference counting, which will create many objects, have them as fields in other objects/instances in arrays etc, probably pass them between different gofuncs, and have some hook to test we have many references in the middle and no unexpected references after all objects are destroyed,

match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => {
let bytes = e.as_bytes();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was that intentional? won't it just print the error twice in the print below?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second print calls Debug which includes extra info, and may change across updates to the stdlib. I've tweaked the message to provide the data in hex either way :)

arbitrator/jit/src/syscall.rs Outdated Show resolved Hide resolved
}

/// Assigns an id for the given value, incrementing its reference count if already present.
pub fn assign_id(&self, value: JsValue) -> JsValueId {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not mandatory in any way, but wanted to point out the original go-js implementation's version is "storeValue", which gets both the value and an address to store it. There isn't much representation through the code of value ids - only values that are stored/loaded as 8-bytes into/from memory. It has some downside, but makes the code overall less confusing, especially ref counting seems to make sense to me (because you only increase refcount when you actually store this reference)

pub fn copy_bytes_to_go(&self, src: JsValueId, write_bytes: impl FnOnce(&[u8])) {
match self.values.id_to_value(src) {
JsValue::Uint8Array(array) => write_bytes(&array.lock()),
x => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should return an error, caller should write 0 (instead of 1) to stack
should not panic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, refactored :)

match self.values.id_to_value(dest) {
JsValue::Uint8Array(array) => write_bytes(&mut array.lock()),
x => {
panic!("Go attempted to call copyBytesToJs on invalid type: {x:?}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should return an error, caller should write 0 (instead of 1) to stack
should not panic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, refactored :)

let len = match self.values.id_to_value(array) {
JsValue::Array(array) => array.lock().len(),
JsValue::Uint8Array(array) => array.lock().len(),
x => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exists for String
exists for functions
could exist for object
returns "undefined" for bool / number and doesn't panic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go expects an integer rather than a type, so we should probs panic in our case if the result is not a number. I've updated the panic message to clarify this a bit, as well as added an implementation for String.

env.js_state.pool.remove(id);
}
let js_env = &mut WasmerJsEnv::new(sp, &mut store, exports, &mut env.go_state)?;
let outs = js_state.call_stylus_func(api_id, method, args, js_env)?;
respond.send(outs).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in js_value_new (below):
is it o.k we're returning nil for error in case an error is returned? maybe better to create some simple error value?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry which part / error? Did you mean to add this comment somewhere else?

contracts Outdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably doesn't belong in this PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just bumps us to the latest stylus-contracts/stylus after a previous merge :)

go-ethereum Outdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably doesn't belong in this PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just bumps us to the latest stylus/stylus after a previous merge :)

@@ -40,6 +39,7 @@ import (
"github.com/offchainlabs/nitro/validator/valnode"
"github.com/wasmerio/wasmer-go/wasmer"

"github.com/ethereum/go-ethereum/eth/tracers"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove change.. or move tracers/js to be with the rest of go-ethereum imports

Rachel Bousfield and others added 2 commits November 10, 2023 16:05
Copy link

cla-bot bot commented Nov 15, 2023

We require contributors to sign our Contributor License Agreement. In order for us to review and merge your code, please sign the linked documents below to get yourself added. https://na3.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=b15c81cc-b5ea-42a6-9107-3992526f2898&env=na3&acct=6e152afc-6284-44af-a4c1-d8ef291db402&v=2

@cla-bot cla-bot bot removed the s label Nov 15, 2023
Copy link

cla-bot bot commented Nov 17, 2023

We require contributors to sign our Contributor License Agreement. In order for us to review and merge your code, please sign the linked documents below to get yourself added. https://na3.docusign.net/Member/PowerFormSigning.aspx?PowerFormId=b15c81cc-b5ea-42a6-9107-3992526f2898&env=na3&acct=6e152afc-6284-44af-a4c1-d8ef291db402&v=2

@cla-bot cla-bot bot added the s label Nov 28, 2023
@rachel-bousfield rachel-bousfield merged commit cebfdd6 into stylus Nov 29, 2023
8 checks passed
@rachel-bousfield rachel-bousfield deleted the rewrite-go-js branch November 29, 2023 19:31
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants