Skip to content

Improve wasm demo website #230

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

Merged
merged 8 commits into from
Dec 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ __pycache__
**/*.pytest_cache
.*sw*
.repl_history.txt
wasm-pack.log
2 changes: 2 additions & 0 deletions wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
pkg/
1 change: 1 addition & 0 deletions wasm/Cargo.lock

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

1 change: 1 addition & 0 deletions wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions wasm/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
node_modules/
4 changes: 4 additions & 0 deletions wasm/app/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"tabWidth": 4
}
5 changes: 3 additions & 2 deletions wasm/app/bootstrap.js
Original file line number Diff line number Diff line change
@@ -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)
);
114 changes: 76 additions & 38 deletions wasm/app/index.html
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>RustPython Demo</title>
<style type="text/css" media="screen">
textarea {
font-family: monospace;
}

#code {
height: 35vh;
width: 95vw;
}

#console {
height: 35vh;
width: 95vw;
}

#run-btn {
width: 6em;
height: 2em;
font-size: 24px;
}
</style>
</head>
<body>
<h1>RustPython Demo</h1>
<p>RustPython is a Python interpreter writter in Rust. This demo is compiled from Rust to WebAssembly so it runs in the browser</p>
<p>Please input your python code below and click <kbd>Run</kbd>:</p>
<textarea id="code">n1 = 0
<head>
<meta charset="utf-8" />
<title>RustPython Demo</title>
<style type="text/css" media="screen">
textarea {
font-family: monospace;
resize: vertical;
}

#code {
height: 35vh;
width: 95vw;
}

#console {
height: 35vh;
width: 95vw;
}

#run-btn {
width: 6em;
height: 2em;
font-size: 24px;
}

#error {
color: tomato;
margin-top: 10px;
font-family: monospace;
}
</style>
</head>
<body>
<h1>RustPython Demo</h1>
<p>
RustPython is a Python interpreter writter in Rust. This demo is
compiled from Rust to WebAssembly so it runs in the browser
</p>
<p>Please input your python code below and click <kbd>Run</kbd>:</p>
<p>
Alternatively, open up your browser's devtools and play with
<code>rp.eval_py('print("a")')</code>
</p>
<textarea id="code">
n1 = 0
n2 = 1
count = 0
until = 10
Expand All @@ -40,12 +55,35 @@ <h1>RustPython Demo</h1>
print(n1)
n1, n2 = n2, n1 + n2
count += 1
</textarea>
<button id="run-btn">Run &#9655;</button>
<script src="./bootstrap.js"></script>
<h3>Standard Output</h3>
<textarea id="console">Loading...</textarea>

<a href="https://github.com/RustPython/RustPython"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png" alt="Fork me on GitHub"></a>
</body>

</textarea>
<button id="run-btn">Run &#9655;</button>
<div id="error"></div>
<script src="./bootstrap.js"></script>
<h3>Standard Output</h3>
<textarea id="console">Loading...</textarea>

<p>Here's some info regarding the <code>rp.eval_py()</code> function</p>
<ul>
<li>
You can return variables from python and get them returned to
JS, with the only requirement being that they're serializable
with <code>json.dumps</code>.
</li>
<li>
You can pass an object as the second argument to the function,
and that will be available in python as the variable
<code>js_vars</code>. Again, only values that can be serialized
with <code>JSON.stringify()</code> will go through.
</li>
</ul>

<!-- "Fork me on GitHub" banner -->
<a href="https://github.com/RustPython/RustPython"
><img
style="position: absolute; top: 0; right: 0; border: 0;"
src="https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png"
alt="Fork me on GitHub"
/></a>
</body>
</html>
37 changes: 19 additions & 18 deletions wasm/app/index.js
Original file line number Diff line number Diff line change
@@ -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
134 changes: 122 additions & 12 deletions wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<F>(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<js_sys::Object>) -> Result<JsValue, JsValue> {
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<JsValue, JsValue> {
//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),
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice! I like this dual-function design of print_html + print_console

);

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())
}
}
}
Loading