Skip to content

Commit 71ba501

Browse files
authored
Merge pull request #230 from coolreader18/master
Improve wasm demo website
2 parents 950dbd1 + e77f223 commit 71ba501

11 files changed

+251
-75
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ __pycache__
66
**/*.pytest_cache
77
.*sw*
88
.repl_history.txt
9+
wasm-pack.log

wasm/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bin/
2+
pkg/

wasm/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wasm/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ rustpython_parser = {path = "../parser"}
1414
rustpython_vm = {path = "../vm"}
1515
cfg-if = "0.1.2"
1616
wasm-bindgen = "0.2"
17+
js-sys = "0.3"
1718

1819
[dependencies.web-sys]
1920
version = "0.3"

wasm/app/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
node_modules/

wasm/app/.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"singleQuote": true,
3+
"tabWidth": 4
4+
}

wasm/app/bootstrap.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// A dependency graph that contains any wasm must all be imported
22
// asynchronously. This `bootstrap.js` file does the single async import, so
33
// that no one else needs to worry about it again.
4-
import("./index.js")
5-
.catch(e => console.error("Error importing `index.js`:", e));
4+
import('./index.js').catch(e =>
5+
console.error('Error importing `index.js`:', e)
6+
);

wasm/app/index.html

+76-38
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,50 @@
11
<!DOCTYPE html>
22
<html>
3-
<head>
4-
<meta charset="utf-8">
5-
<title>RustPython Demo</title>
6-
<style type="text/css" media="screen">
7-
textarea {
8-
font-family: monospace;
9-
}
10-
11-
#code {
12-
height: 35vh;
13-
width: 95vw;
14-
}
15-
16-
#console {
17-
height: 35vh;
18-
width: 95vw;
19-
}
20-
21-
#run-btn {
22-
width: 6em;
23-
height: 2em;
24-
font-size: 24px;
25-
}
26-
</style>
27-
</head>
28-
<body>
29-
<h1>RustPython Demo</h1>
30-
<p>RustPython is a Python interpreter writter in Rust. This demo is compiled from Rust to WebAssembly so it runs in the browser</p>
31-
<p>Please input your python code below and click <kbd>Run</kbd>:</p>
32-
<textarea id="code">n1 = 0
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>RustPython Demo</title>
6+
<style type="text/css" media="screen">
7+
textarea {
8+
font-family: monospace;
9+
resize: vertical;
10+
}
11+
12+
#code {
13+
height: 35vh;
14+
width: 95vw;
15+
}
16+
17+
#console {
18+
height: 35vh;
19+
width: 95vw;
20+
}
21+
22+
#run-btn {
23+
width: 6em;
24+
height: 2em;
25+
font-size: 24px;
26+
}
27+
28+
#error {
29+
color: tomato;
30+
margin-top: 10px;
31+
font-family: monospace;
32+
}
33+
</style>
34+
</head>
35+
<body>
36+
<h1>RustPython Demo</h1>
37+
<p>
38+
RustPython is a Python interpreter writter in Rust. This demo is
39+
compiled from Rust to WebAssembly so it runs in the browser
40+
</p>
41+
<p>Please input your python code below and click <kbd>Run</kbd>:</p>
42+
<p>
43+
Alternatively, open up your browser's devtools and play with
44+
<code>rp.eval_py('print("a")')</code>
45+
</p>
46+
<textarea id="code">
47+
n1 = 0
3348
n2 = 1
3449
count = 0
3550
until = 10
@@ -40,12 +55,35 @@ <h1>RustPython Demo</h1>
4055
print(n1)
4156
n1, n2 = n2, n1 + n2
4257
count += 1
43-
</textarea>
44-
<button id="run-btn">Run &#9655;</button>
45-
<script src="./bootstrap.js"></script>
46-
<h3>Standard Output</h3>
47-
<textarea id="console">Loading...</textarea>
48-
49-
<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>
50-
</body>
58+
59+
</textarea>
60+
<button id="run-btn">Run &#9655;</button>
61+
<div id="error"></div>
62+
<script src="./bootstrap.js"></script>
63+
<h3>Standard Output</h3>
64+
<textarea id="console">Loading...</textarea>
65+
66+
<p>Here's some info regarding the <code>rp.eval_py()</code> function</p>
67+
<ul>
68+
<li>
69+
You can return variables from python and get them returned to
70+
JS, with the only requirement being that they're serializable
71+
with <code>json.dumps</code>.
72+
</li>
73+
<li>
74+
You can pass an object as the second argument to the function,
75+
and that will be available in python as the variable
76+
<code>js_vars</code>. Again, only values that can be serialized
77+
with <code>JSON.stringify()</code> will go through.
78+
</li>
79+
</ul>
80+
81+
<!-- "Fork me on GitHub" banner -->
82+
<a href="https://github.com/RustPython/RustPython"
83+
><img
84+
style="position: absolute; top: 0; right: 0; border: 0;"
85+
src="https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png"
86+
alt="Fork me on GitHub"
87+
/></a>
88+
</body>
5189
</html>

wasm/app/index.js

+19-18
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
1-
import * as rp from "rustpython_wasm";
1+
import * as rp from 'rustpython_wasm';
22

3-
function runCodeFromTextarea(_) {
4-
const consoleElement = document.getElementById('console');
5-
// Clean the console
6-
consoleElement.value = '';
7-
8-
const code = document.getElementById('code').value;
9-
try {
10-
if (!code.endsWith('\n')) { // HACK: if the code doesn't end with newline it crashes.
11-
rp.run_code(code + '\n');
12-
return;
13-
}
3+
// so people can play around with it
4+
window.rp = rp;
145

15-
rp.run_code(code);
6+
function runCodeFromTextarea(_) {
7+
const consoleElement = document.getElementById('console');
8+
const errorElement = document.getElementById('error');
169

17-
} catch(e) {
18-
consoleElement.value = 'Execution failed. Please check if your Python code has any syntax error.';
19-
console.error(e);
20-
}
10+
// Clean the console and errors
11+
consoleElement.value = '';
12+
errorElement.textContent = '';
2113

14+
const code = document.getElementById('code').value;
15+
try {
16+
rp.run_from_textbox(code);
17+
} catch (e) {
18+
errorElement.textContent = e;
19+
console.error(e);
20+
}
2221
}
2322

24-
document.getElementById('run-btn').addEventListener('click', runCodeFromTextarea);
23+
document
24+
.getElementById('run-btn')
25+
.addEventListener('click', runCodeFromTextarea);
2526

2627
runCodeFromTextarea(); // Run once for demo

wasm/src/lib.rs

+122-12
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,143 @@
11
mod wasm_builtins;
22

3+
extern crate js_sys;
34
extern crate rustpython_vm;
45
extern crate wasm_bindgen;
56
extern crate web_sys;
67

7-
use rustpython_vm::VirtualMachine;
88
use rustpython_vm::compile;
9-
use rustpython_vm::pyobject::AttributeProtocol;
9+
use rustpython_vm::pyobject::{self, PyObjectRef, PyResult};
10+
use rustpython_vm::VirtualMachine;
1011
use wasm_bindgen::prelude::*;
1112
use web_sys::console;
1213

14+
fn py_str_err(vm: &mut VirtualMachine, py_err: &PyObjectRef) -> String {
15+
vm.to_pystr(&py_err)
16+
.unwrap_or_else(|_| "Error, and error getting error message".into())
17+
}
18+
19+
fn py_to_js(vm: &mut VirtualMachine, py_obj: PyObjectRef) -> JsValue {
20+
let dumps = rustpython_vm::import::import(
21+
vm,
22+
std::path::PathBuf::default(),
23+
"json",
24+
&Some("dumps".into()),
25+
)
26+
.expect("Couldn't get json.dumps function");
27+
match vm.invoke(dumps, pyobject::PyFuncArgs::new(vec![py_obj], vec![])) {
28+
Ok(value) => {
29+
let json = vm.to_pystr(&value).unwrap();
30+
js_sys::JSON::parse(&json).unwrap_or(JsValue::UNDEFINED)
31+
}
32+
Err(_) => JsValue::UNDEFINED,
33+
}
34+
}
35+
36+
fn js_to_py(vm: &mut VirtualMachine, js_val: JsValue) -> PyObjectRef {
37+
let json = match js_sys::JSON::stringify(&js_val) {
38+
Ok(json) => String::from(json),
39+
Err(_) => return vm.get_none(),
40+
};
41+
42+
let loads = rustpython_vm::import::import(
43+
vm,
44+
std::path::PathBuf::default(),
45+
"json",
46+
&Some("loads".into()),
47+
)
48+
.expect("Couldn't get json.loads function");
49+
50+
let py_json = vm.new_str(json);
51+
52+
vm.invoke(loads, pyobject::PyFuncArgs::new(vec![py_json], vec![]))
53+
// can safely unwrap because we know it's valid JSON
54+
.unwrap()
55+
}
56+
57+
fn eval<F>(vm: &mut VirtualMachine, source: &str, setup_scope: F) -> PyResult
58+
where
59+
F: Fn(&mut VirtualMachine, &PyObjectRef),
60+
{
61+
// HACK: if the code doesn't end with newline it crashes.
62+
let mut source = source.to_string();
63+
if !source.ends_with('\n') {
64+
source.push('\n');
65+
}
66+
67+
let code_obj = compile::compile(vm, &source, compile::Mode::Exec, None)?;
68+
69+
let builtins = vm.get_builtin_scope();
70+
let mut vars = vm.context().new_scope(Some(builtins));
71+
72+
setup_scope(vm, &mut vars);
73+
74+
vm.run_code_obj(code_obj, vars)
75+
}
76+
77+
#[wasm_bindgen]
78+
pub fn eval_py(source: &str, js_injections: Option<js_sys::Object>) -> Result<JsValue, JsValue> {
79+
if let Some(js_injections) = js_injections.clone() {
80+
if !js_injections.is_object() {
81+
return Err(js_sys::TypeError::new("The second argument must be an object").into());
82+
}
83+
}
84+
85+
let mut vm = VirtualMachine::new();
86+
87+
vm.ctx.set_attr(
88+
&vm.builtins,
89+
"print",
90+
vm.context()
91+
.new_rustfunc(wasm_builtins::builtin_print_console),
92+
);
93+
94+
let res = eval(&mut vm, source, |vm, vars| {
95+
let injections = if let Some(js_injections) = js_injections.clone() {
96+
js_to_py(vm, js_injections.into())
97+
} else {
98+
vm.new_dict()
99+
};
100+
101+
vm.ctx.set_item(vars, "js_vars", injections);
102+
});
103+
104+
res.map(|value| py_to_js(&mut vm, value))
105+
.map_err(|err| py_str_err(&mut vm, &err).into())
106+
}
107+
13108
#[wasm_bindgen]
14-
pub fn run_code(source: &str) -> () {
109+
pub fn run_from_textbox(source: &str) -> Result<JsValue, JsValue> {
15110
//add hash in here
16111
console::log_1(&"Running RustPython".into());
17112
console::log_1(&"Running code:".into());
18113
console::log_1(&source.to_string().into());
19114

20115
let mut vm = VirtualMachine::new();
21-
// We are monkey-patching the builtin print to use console.log
22-
// TODO: moneky-patch sys.stdout instead, after print actually uses sys.stdout
23-
vm.builtins.set_attr("print", vm.context().new_rustfunc(wasm_builtins::builtin_print));
24116

25-
let code_obj = compile::compile(&mut vm, &source.to_string(), compile::Mode::Exec, None);
117+
// We are monkey-patching the builtin print to use console.log
118+
// TODO: monkey-patch sys.stdout instead, after print actually uses sys.stdout
119+
vm.ctx.set_attr(
120+
&vm.builtins,
121+
"print",
122+
vm.context().new_rustfunc(wasm_builtins::builtin_print_html),
123+
);
26124

27-
let builtins = vm.get_builtin_scope();
28-
let vars = vm.context().new_scope(Some(builtins));
29-
match vm.run_code_obj(code_obj.unwrap(), vars) {
30-
Ok(_value) => console::log_1(&"Execution successful".into()),
31-
Err(_) => console::log_1(&"Execution failed".into()),
125+
match eval(&mut vm, source, |_, _| {}) {
126+
Ok(value) => {
127+
console::log_1(&"Execution successful".into());
128+
match value.borrow().kind {
129+
pyobject::PyObjectKind::None => {}
130+
_ => {
131+
if let Ok(text) = vm.to_pystr(&value) {
132+
wasm_builtins::print_to_html(&text);
133+
}
134+
}
135+
}
136+
Ok(JsValue::UNDEFINED)
137+
}
138+
Err(err) => {
139+
console::log_1(&"Execution failed".into());
140+
Err(py_str_err(&mut vm, &err).into())
141+
}
32142
}
33143
}

0 commit comments

Comments
 (0)