diff --git a/.gitignore b/.gitignore index b1df9e59fa..bdf973f033 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ **/*.pytest_cache .*sw* .repl_history.txt +wasm-pack.log diff --git a/wasm/.gitignore b/wasm/.gitignore new file mode 100644 index 0000000000..882d44992f --- /dev/null +++ b/wasm/.gitignore @@ -0,0 +1,2 @@ +bin/ +pkg/ diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index e4cc8e60e8..9bad7eebad 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -577,6 +577,7 @@ name = "rustpython_wasm" version = "0.1.0" dependencies = [ "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "rustpython_parser 0.0.1", "rustpython_vm 0.1.0", "wasm-bindgen 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index cfca2acb71..4be409dfc2 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -14,6 +14,7 @@ rustpython_parser = {path = "../parser"} rustpython_vm = {path = "../vm"} cfg-if = "0.1.2" wasm-bindgen = "0.2" +js-sys = "0.3" [dependencies.web-sys] version = "0.3" diff --git a/wasm/app/.gitignore b/wasm/app/.gitignore new file mode 100644 index 0000000000..1eae0cf670 --- /dev/null +++ b/wasm/app/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/wasm/app/.prettierrc b/wasm/app/.prettierrc new file mode 100644 index 0000000000..96c36f53c9 --- /dev/null +++ b/wasm/app/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "tabWidth": 4 +} diff --git a/wasm/app/bootstrap.js b/wasm/app/bootstrap.js index 7934d627e8..61136ee9b8 100644 --- a/wasm/app/bootstrap.js +++ b/wasm/app/bootstrap.js @@ -1,5 +1,6 @@ // A dependency graph that contains any wasm must all be imported // asynchronously. This `bootstrap.js` file does the single async import, so // that no one else needs to worry about it again. -import("./index.js") - .catch(e => console.error("Error importing `index.js`:", e)); +import('./index.js').catch(e => + console.error('Error importing `index.js`:', e) +); diff --git a/wasm/app/index.html b/wasm/app/index.html index bb7f2a98fe..f46ce1251a 100644 --- a/wasm/app/index.html +++ b/wasm/app/index.html @@ -1,35 +1,50 @@ - - - RustPython Demo - - - -

RustPython Demo

-

RustPython is a Python interpreter writter in Rust. This demo is compiled from Rust to WebAssembly so it runs in the browser

-

Please input your python code below and click Run:

- - - -

Standard Output

- - - Fork me on GitHub - + + + +
+ +

Standard Output

+ + +

Here's some info regarding the rp.eval_py() function

+ + + + Fork me on GitHub + diff --git a/wasm/app/index.js b/wasm/app/index.js index ef223a5a04..d21ac7a045 100644 --- a/wasm/app/index.js +++ b/wasm/app/index.js @@ -1,26 +1,27 @@ -import * as rp from "rustpython_wasm"; +import * as rp from 'rustpython_wasm'; -function runCodeFromTextarea(_) { - const consoleElement = document.getElementById('console'); - // Clean the console - consoleElement.value = ''; - - const code = document.getElementById('code').value; - try { - if (!code.endsWith('\n')) { // HACK: if the code doesn't end with newline it crashes. - rp.run_code(code + '\n'); - return; - } +// so people can play around with it +window.rp = rp; - rp.run_code(code); +function runCodeFromTextarea(_) { + const consoleElement = document.getElementById('console'); + const errorElement = document.getElementById('error'); - } catch(e) { - consoleElement.value = 'Execution failed. Please check if your Python code has any syntax error.'; - console.error(e); - } + // Clean the console and errors + consoleElement.value = ''; + errorElement.textContent = ''; + const code = document.getElementById('code').value; + try { + rp.run_from_textbox(code); + } catch (e) { + errorElement.textContent = e; + console.error(e); + } } -document.getElementById('run-btn').addEventListener('click', runCodeFromTextarea); +document + .getElementById('run-btn') + .addEventListener('click', runCodeFromTextarea); runCodeFromTextarea(); // Run once for demo diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 36cdf8d089..9ac5677d79 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -1,33 +1,143 @@ mod wasm_builtins; +extern crate js_sys; extern crate rustpython_vm; extern crate wasm_bindgen; extern crate web_sys; -use rustpython_vm::VirtualMachine; use rustpython_vm::compile; -use rustpython_vm::pyobject::AttributeProtocol; +use rustpython_vm::pyobject::{self, PyObjectRef, PyResult}; +use rustpython_vm::VirtualMachine; use wasm_bindgen::prelude::*; use web_sys::console; +fn py_str_err(vm: &mut VirtualMachine, py_err: &PyObjectRef) -> String { + vm.to_pystr(&py_err) + .unwrap_or_else(|_| "Error, and error getting error message".into()) +} + +fn py_to_js(vm: &mut VirtualMachine, py_obj: PyObjectRef) -> JsValue { + let dumps = rustpython_vm::import::import( + vm, + std::path::PathBuf::default(), + "json", + &Some("dumps".into()), + ) + .expect("Couldn't get json.dumps function"); + match vm.invoke(dumps, pyobject::PyFuncArgs::new(vec![py_obj], vec![])) { + Ok(value) => { + let json = vm.to_pystr(&value).unwrap(); + js_sys::JSON::parse(&json).unwrap_or(JsValue::UNDEFINED) + } + Err(_) => JsValue::UNDEFINED, + } +} + +fn js_to_py(vm: &mut VirtualMachine, js_val: JsValue) -> PyObjectRef { + let json = match js_sys::JSON::stringify(&js_val) { + Ok(json) => String::from(json), + Err(_) => return vm.get_none(), + }; + + let loads = rustpython_vm::import::import( + vm, + std::path::PathBuf::default(), + "json", + &Some("loads".into()), + ) + .expect("Couldn't get json.loads function"); + + let py_json = vm.new_str(json); + + vm.invoke(loads, pyobject::PyFuncArgs::new(vec![py_json], vec![])) + // can safely unwrap because we know it's valid JSON + .unwrap() +} + +fn eval(vm: &mut VirtualMachine, source: &str, setup_scope: F) -> PyResult +where + F: Fn(&mut VirtualMachine, &PyObjectRef), +{ + // HACK: if the code doesn't end with newline it crashes. + let mut source = source.to_string(); + if !source.ends_with('\n') { + source.push('\n'); + } + + let code_obj = compile::compile(vm, &source, compile::Mode::Exec, None)?; + + let builtins = vm.get_builtin_scope(); + let mut vars = vm.context().new_scope(Some(builtins)); + + setup_scope(vm, &mut vars); + + vm.run_code_obj(code_obj, vars) +} + +#[wasm_bindgen] +pub fn eval_py(source: &str, js_injections: Option) -> Result { + if let Some(js_injections) = js_injections.clone() { + if !js_injections.is_object() { + return Err(js_sys::TypeError::new("The second argument must be an object").into()); + } + } + + let mut vm = VirtualMachine::new(); + + vm.ctx.set_attr( + &vm.builtins, + "print", + vm.context() + .new_rustfunc(wasm_builtins::builtin_print_console), + ); + + let res = eval(&mut vm, source, |vm, vars| { + let injections = if let Some(js_injections) = js_injections.clone() { + js_to_py(vm, js_injections.into()) + } else { + vm.new_dict() + }; + + vm.ctx.set_item(vars, "js_vars", injections); + }); + + res.map(|value| py_to_js(&mut vm, value)) + .map_err(|err| py_str_err(&mut vm, &err).into()) +} + #[wasm_bindgen] -pub fn run_code(source: &str) -> () { +pub fn run_from_textbox(source: &str) -> Result { //add hash in here console::log_1(&"Running RustPython".into()); console::log_1(&"Running code:".into()); console::log_1(&source.to_string().into()); let mut vm = VirtualMachine::new(); - // We are monkey-patching the builtin print to use console.log - // TODO: moneky-patch sys.stdout instead, after print actually uses sys.stdout - vm.builtins.set_attr("print", vm.context().new_rustfunc(wasm_builtins::builtin_print)); - let code_obj = compile::compile(&mut vm, &source.to_string(), compile::Mode::Exec, None); + // We are monkey-patching the builtin print to use console.log + // TODO: monkey-patch sys.stdout instead, after print actually uses sys.stdout + vm.ctx.set_attr( + &vm.builtins, + "print", + vm.context().new_rustfunc(wasm_builtins::builtin_print_html), + ); - let builtins = vm.get_builtin_scope(); - let vars = vm.context().new_scope(Some(builtins)); - match vm.run_code_obj(code_obj.unwrap(), vars) { - Ok(_value) => console::log_1(&"Execution successful".into()), - Err(_) => console::log_1(&"Execution failed".into()), + match eval(&mut vm, source, |_, _| {}) { + Ok(value) => { + console::log_1(&"Execution successful".into()); + match value.borrow().kind { + pyobject::PyObjectKind::None => {} + _ => { + if let Ok(text) = vm.to_pystr(&value) { + wasm_builtins::print_to_html(&text); + } + } + } + Ok(JsValue::UNDEFINED) + } + Err(err) => { + console::log_1(&"Execution failed".into()); + Err(py_str_err(&mut vm, &err).into()) + } } } diff --git a/wasm/src/wasm_builtins.rs b/wasm/src/wasm_builtins.rs index c3d601a8cd..99e59f6bee 100644 --- a/wasm/src/wasm_builtins.rs +++ b/wasm/src/wasm_builtins.rs @@ -4,27 +4,31 @@ //! desktop. //! Implements functions listed here: https://docs.python.org/3/library/builtins.html //! +extern crate js_sys; extern crate wasm_bindgen; extern crate web_sys; +use js_sys::Array; use rustpython_vm::obj::objstr; +use rustpython_vm::pyobject::{PyFuncArgs, PyResult}; use rustpython_vm::VirtualMachine; -use rustpython_vm::pyobject::{ PyFuncArgs, PyResult }; use wasm_bindgen::JsCast; -use web_sys::{HtmlTextAreaElement, window}; +use web_sys::{console, window, HtmlTextAreaElement}; // The HTML id of the textarea element that act as our STDOUT const CONSOLE_ELEMENT_ID: &str = "console"; -fn print_to_html(text: &str) { +pub fn print_to_html(text: &str) { let document = window().unwrap().document().unwrap(); - let element = document.get_element_by_id(CONSOLE_ELEMENT_ID).expect("Can't find the console textarea"); + let element = document + .get_element_by_id(CONSOLE_ELEMENT_ID) + .expect("Can't find the console textarea"); let textarea = element.dyn_ref::().unwrap(); let value = textarea.value(); textarea.set_value(&format!("{}{}", value, text)); } -pub fn builtin_print(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { +pub fn builtin_print_html(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { let mut first = true; for a in args.args { if first { @@ -38,3 +42,14 @@ pub fn builtin_print(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { } Ok(vm.get_none()) } + +pub fn builtin_print_console(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { + let arr = Array::new(); + for a in args.args { + let v = vm.to_str(&a)?; + let s = objstr::get_value(&v); + arr.push(&s.into()); + } + console::log(&arr); + Ok(vm.get_none()) +}