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

20230810 wasm interface #29

Merged
merged 23 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e96b7c8
Start adding test material for a better wasm interface
prozacchiwawa Aug 10, 2023
f219070
Get the tools loaded
prozacchiwawa Aug 10, 2023
edea912
Start hooking up the program object
prozacchiwawa Aug 11, 2023
ec2ba72
Start formalizing
prozacchiwawa Aug 14, 2023
9b2dbc0
Add some more, get the object cache off the ground
prozacchiwawa Aug 22, 2023
aa0a7c1
Most of the SExp.ts api
prozacchiwawa Aug 22, 2023
e343cfc
tests
prozacchiwawa Aug 22, 2023
2b044b6
Add run and from_hex
prozacchiwawa Aug 22, 2023
7aff57a
generalize support for objects with a serialize method
prozacchiwawa Aug 22, 2023
20986a6
Add 't' tuple function
prozacchiwawa Aug 22, 2023
ceb819f
Give tuple a prototype and a conversion function that can be recogniz…
prozacchiwawa Aug 22, 2023
7cbc9bd
Add as_bin
prozacchiwawa Aug 22, 2023
7574324
Add list_len
prozacchiwawa Aug 22, 2023
fe7f125
Add equal_to
prozacchiwawa Aug 22, 2023
a3a2a16
Add as_javascript, sort out improper list structure
prozacchiwawa Aug 22, 2023
5876f04
Ensure that we exercise a compilicated enough improper list
prozacchiwawa Aug 22, 2023
eb28763
Finalize new tests and test data.
prozacchiwawa Aug 25, 2023
618331d
Add an empty index.js ... this test is for the wasm, but it's from pr…
prozacchiwawa Aug 25, 2023
aa13c1d
change name
prozacchiwawa Aug 25, 2023
ec0104b
Add some documentation
prozacchiwawa Sep 7, 2023
73fe7d6
Add more docs
prozacchiwawa Sep 7, 2023
f01f9f2
Fix up clippy warnings in objects.rs
prozacchiwawa Sep 8, 2023
4bb3d77
Add a bit more docs
prozacchiwawa Sep 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ jobs:
- name: Test wasm
run: node wasm/tests/index.js

- name: Test clvm-js like wasm interface
run: |
cd wasm/tests/clvm-tools-interface && npm install && yarn test

- name: Upload npm pkg artifacts
uses: actions/upload-artifact@v3
with:
Expand Down
2 changes: 2 additions & 0 deletions wasm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ clvmr = { version = "0.2.5", features = ["pre-eval"] }
wasm-bindgen = "=0.2.83"
wasm-bindgen-test = "=0.3.25"
js-sys = "0.3.60"
num-bigint = "0.4.0"
num-traits = "0.2.15"
115 changes: 115 additions & 0 deletions wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,118 @@ Prerequisite:
# Make sure you're at <clvm_tools_rs root>/wasm
node ./tests/index.js
```

Program
===

Program is exported by ```clvm_tools_rs``` and contains a ```to``` function
among a few others. Its use is very like Program.to in the python code and
similar to chiaminejp's ```clvm_tools``` library. It produces a value that
can be used together with other such values, can be curried (and uncurried)
converted to the hex representation and run.

```Program.to(javascript_value)```

Converts javascript values to SExp objects which can be used together and run
as CLVM programs or used as program arguments. This conversion follows simple
conventions that were established in ```clvm_tools```.

- There's a tuple object returned by the ```t``` function (2 arguments) which
produces a cons.

- javascript arrays are treated as linear proper lists. Each element appears
as the first of a cons with the rest of the converted list as its tail. The
list is terminated by a nil.

- an object which has a serialize method treats the result of ```o.serialize()```
as an array-like object which specifies the byte values of the object's atom
representation. This covers bls primitives such as G1Element and G2Element.

- javascript numbers, bignums, strings and bools are treated as atoms.

- javascript objects which contain an array-like ```pair``` member are treated
the same as tuple objects above.

```Program.from_hex(hex_str)```

Converts a string of pairs of hex digits into the CLVM deserialized form of the
object.

```Program.null()```

Returns a null object.

The returned objects have these methods:

SExp methods
===

```SExp.toString()```

Convert the object to its hex representation.

```SExp.as_pair()```

If it is a cons, return a tuple-compatible object containing a ```pair``` array
with 2 elements, otherwise null.

```SExp.listp()```

Return true if the object is a cons.

```SExp.nullp()```

Return true if the object is falsey.

```SExp.as_int()```

Returns a javascript number that fits within the 32-bit integers representing the object's atom value, or throw.

```SExp.as_bigint()```

Returns a javascript big number representing the value of the given atom or throw.

```SExp.first()```

If the object is a cons, return its first or left component, or throw if not.

```SExp.rest()```

If the object is a cons, return its rest or right component, or throw if not.

```SExp.cons(other)```

Creates an SExp which is a cons of this sexp and other.

```SExp.run(env)```

Runs the indicated SExp as a program with the given environment.

```SExp.as_bin()```

Serialize the object into an array of byte values.

```SExp.list_len()```

Give the number of conses one needs to traverse until reaching a non-cons rest.
For a proper list, this gives the list's length.

```SExp.as_javascript()```

Return a javascript value that allows the given SExp to be inspected via
javascript.

```SExp.curry(a, b, c ...)```

Given a number of positional arguments, build a curried application that provides
values for the left arguments of some runnable CLVM code, giving code that can
be correctly called with fewer arguments. This is common for providing values to
the upper case parameters of chialisp programs, such as coin puzzles.

```SExp.uncurry(program) -> [inner_program, [args...]]```
```SExp.uncurry_error(program)```

Uncurry returns an array with the inner program and the retrievable arguments
separated out, or the original program and null. uncurry_error throws instead
of returning a value if the object wasn't a curried program.

79 changes: 42 additions & 37 deletions wasm/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::jsval::{
btreemap_to_object, get_property, js_object_from_sexp, js_pair, object_to_value,
read_string_to_string_map, sexp_from_js_object,
};
use crate::objects::Program;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
Expand Down Expand Up @@ -61,7 +62,7 @@ thread_local! {
};
}

fn get_next_id() -> i32 {
pub fn get_next_id() -> i32 {
NEXT_ID.with(|n| n.fetch_add(1, Ordering::SeqCst) as i32)
}

Expand Down Expand Up @@ -96,42 +97,39 @@ where
runcell.replace_with(|coll| {
let mut work_collection = HashMap::new();
swap(coll, &mut work_collection);
match work_collection.get_mut(&this_id) {
Some(r_ref) => {
result = f(r_ref);
}
_ => {}
if let Some(r_ref) = work_collection.get_mut(&this_id) {
result = f(r_ref);
}
work_collection
});
});
result
}

fn create_clvm_runner_err(error: String) -> JsValue {
pub fn create_clvm_runner_err(error: String) -> JsValue {
let array = js_sys::Array::new();
array.set(
0,
js_pair(JsValue::from_str("error"), JsValue::from_str(&error)),
);
return object_to_value(&js_sys::Object::from_entries(&array).unwrap());
object_to_value(&js_sys::Object::from_entries(&array).unwrap())
}

fn create_clvm_runner_run_failure(err: &RunFailure) -> JsValue {
match err {
RunFailure::RunErr(l, e) => {
return create_clvm_runner_err(format!("{}: Error {}", l.to_string(), e));
create_clvm_runner_err(format!("{}: Error {}", l, e))
}
RunFailure::RunExn(l, e) => {
return create_clvm_runner_err(format!("{}: Exn {}", l.to_string(), e.to_string()))
create_clvm_runner_err(format!("{}: Exn {}", l, e))
}
}
}

fn create_clvm_compile_failure(err: &CompileErr) -> JsValue {
match err {
CompileErr(l, e) => {
return create_clvm_runner_err(format!("{}: Error {}", l.to_string(), e));
create_clvm_runner_err(format!("{}: Error {}", l, e))
}
}
}
Expand All @@ -146,7 +144,7 @@ impl CldbSingleBespokeOverride for JsBespokeOverride {
// When the user returns, try to convert the result back to sexp.
fn get_override(&self, env: Rc<SExp>) -> Result<Rc<SExp>, RunFailure> {
let args = js_sys::Array::new();
args.set(0, js_object_from_sexp(env.clone()));
args.set(0, js_object_from_sexp(env.clone()).map_err(|_| RunFailure::RunErr(env.loc(), "error converting override value".to_string()))?);
self.fun
.apply(&JsValue::null(), &args)
.map_err(|e| {
Expand All @@ -158,7 +156,7 @@ impl CldbSingleBespokeOverride for JsBespokeOverride {
})
.and_then(|v| {
sexp_from_js_object(env.loc(), &v)
.map(|s| Ok(s))
.map(Ok)
.unwrap_or_else(|| {
Err(RunFailure::RunErr(
env.loc(),
Expand All @@ -182,8 +180,8 @@ pub fn create_clvm_runner(
) -> JsValue {
let mut allocator = Allocator::new();
let runner = Rc::new(DefaultProgramRunner::new());
let args_srcloc = Srcloc::start(&"*args*".to_string());
let prog_srcloc = Srcloc::start(&"*program*".to_string());
let args_srcloc = Srcloc::start("*args*");
let prog_srcloc = Srcloc::start("*program*");
let mut prim_map = HashMap::new();
let mut override_funs: HashMap<String, Box<dyn CldbSingleBespokeOverride>> = HashMap::new();

Expand All @@ -207,14 +205,11 @@ pub fn create_clvm_runner(
}
};

for ent in js_sys::Object::keys(&overrides).values() {
for ent in js_sys::Object::keys(overrides).values() {
let key = ent.unwrap().as_string().unwrap();
let val = get_property(&overrides, &key).unwrap();
match val.dyn_ref::<js_sys::Function>() {
Some(f) => {
override_funs.insert(key, Box::new(JsBespokeOverride { fun: f.clone() }));
}
_ => {}
let val = get_property(overrides, &key).unwrap();
if let Some(f) = val.dyn_ref::<js_sys::Function>() {
override_funs.insert(key, Box::new(JsBespokeOverride { fun: f.clone() }));
}
}

Expand Down Expand Up @@ -246,15 +241,15 @@ pub fn create_clvm_runner(
let this_id = get_next_id();
insert_runner(this_id, JsRunStep { allocator, cldbrun });

return JsValue::from(this_id);
JsValue::from(this_id)
}

#[wasm_bindgen]
pub fn final_value(runner: i32) -> JsValue {
with_runner(runner, |r| {
r.cldbrun.final_result().map(|v| js_object_from_sexp(v))
r.cldbrun.final_result().map(|v| js_object_from_sexp(v).unwrap_or_else(|e| e))
})
.unwrap_or_else(|| JsValue::null())
.unwrap_or_else(JsValue::null)
}

#[wasm_bindgen]
Expand All @@ -273,7 +268,7 @@ pub fn run_step(runner: i32) -> JsValue {
r.cldbrun.step(&mut r.allocator)
})
.map(|result_hash| btreemap_to_object(result_hash.iter()))
.unwrap_or_else(|| JsValue::null())
.unwrap_or_else(JsValue::null)
}

fn make_compile_output(result_stream: &Stream, symbol_table: &HashMap<String, String>) -> JsValue {
Expand All @@ -284,10 +279,8 @@ fn make_compile_output(result_stream: &Stream, symbol_table: &HashMap<String, St
js_pair(JsValue::from_str("hex"), JsValue::from_str(&output_hex)),
);
let symbol_array = js_sys::Array::new();
let mut idx = 0;
for (k, v) in symbol_table.iter() {
symbol_array.set(idx, js_pair(JsValue::from_str(&k), JsValue::from_str(&v)));
idx += 1;
for (idx, (k, v)) in symbol_table.iter().enumerate() {
symbol_array.set(idx as u32, js_pair(JsValue::from_str(k), JsValue::from_str(v)));
}
let symbol_object = object_to_value(&js_sys::Object::from_entries(&symbol_array).unwrap());
array.set(1, js_pair(JsValue::from_str("symbols"), symbol_object));
Expand Down Expand Up @@ -346,7 +339,7 @@ pub fn compose_run_function(
function_name: String,
) -> JsValue {
let mut allocator = Allocator::new();
let loc = Srcloc::start(&"*js*".to_string());
let loc = Srcloc::start("*js*");
let symbol_table = match read_string_to_string_map(symbol_table_js) {
Ok(s) => s,
Err(e) => {
Expand Down Expand Up @@ -386,7 +379,7 @@ pub fn compose_run_function(
},
Ok(x) => x,
};

let function_path = match path_to_function(main_env.1.clone(), &hash_bytes.data().clone()) {
Some(p) => p,
_ => {
Expand Down Expand Up @@ -416,7 +409,7 @@ pub fn compose_run_function(
#[wasm_bindgen]
pub fn create_repl() -> i32 {
let allocator = Allocator::new();
let opts = Rc::new(DefaultCompilerOpts::new(&"*repl*".to_string()));
let opts = Rc::new(DefaultCompilerOpts::new("*repl*"));
let runner = Rc::new(DefaultProgramRunner::new());
let repl = Repl::new(opts, runner.clone());
let new_id = get_next_id();
Expand Down Expand Up @@ -469,26 +462,38 @@ pub fn repl_run_string(repl_id: i32, input: String) -> JsValue {
r.process_line(a, input)
} else {
Err(CompileErr(
Srcloc::start(&"*repl*".to_string()),
Srcloc::start("*repl*"),
"no such repl".to_string(),
))
}
})
.map(|v| v.map(|v| js_object_from_sexp(v.to_sexp())))
.map(|v| v.map(|v| js_object_from_sexp(v.to_sexp()).unwrap_or_else(|e| e)))
.unwrap_or_else(|e| {
Some(create_clvm_runner_err(format!(
"{}: {}",
e.0.to_string(),
e.1
)))
})
.unwrap_or_else(|| JsValue::null())
.unwrap_or_else(JsValue::null)
}

#[wasm_bindgen]
pub fn sexp_to_string(v: &JsValue) -> JsValue {
let loc = Srcloc::start(&"*val*".to_string());
let loc = Srcloc::start("*val*");

sexp_from_js_object(loc, v)
.map(|s| JsValue::from_str(&s.to_string()))
.unwrap_or_else(|| create_clvm_runner_err("unable to convert to value".to_string()))
}

#[wasm_bindgen]
pub fn h(v: String) -> Result<Vec<u8>, JsValue> {
let hex_data = Bytes::new_validated(Some(UnvalidatedBytesFromType::Hex(v))).map_err(|_| js_sys::JsString::from("bad hex input"))?;
Ok(hex_data.data().clone())
}

#[wasm_bindgen]
pub fn t(a: &JsValue, b: &JsValue) -> Result<JsValue, JsValue> {
Program::as_pair_internal(&Program::cons_internal(&Program::to(a)?, &Program::to(b)?)?)
}
Loading
Loading