Skip to content

Commit 1913f03

Browse files
committed
Support eval and exec mode in wasm
- Expose `pyEval`, `pyExec`, `pyExecSingle` in wasm module
1 parent 3aa09dd commit 1913f03

File tree

4 files changed

+100
-22
lines changed

4 files changed

+100
-22
lines changed

wasm/demo/src/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function runCodeFromTextarea() {
3636

3737
const code = editor.getValue();
3838
try {
39-
rp.pyEval(code, {
39+
rp.pyExec(code, {
4040
stdout: output => {
4141
const shouldScroll =
4242
consoleElement.scrollHeight - consoleElement.scrollTop ===

wasm/lib/src/lib.rs

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ extern crate futures;
88
extern crate js_sys;
99
#[macro_use]
1010
extern crate rustpython_vm;
11+
extern crate rustpython_compiler;
1112
extern crate wasm_bindgen;
1213
extern crate wasm_bindgen_futures;
1314
extern crate web_sys;
1415

1516
use js_sys::{Object, Reflect, TypeError};
17+
use rustpython_compiler::compile::Mode;
1618
use std::panic;
1719
use wasm_bindgen::prelude::*;
1820

@@ -47,14 +49,35 @@ pub fn setup_console_error() {
4749
#[wasm_bindgen(typescript_custom_section)]
4850
const TS_CMT_START: &'static str = "/*";
4951

52+
fn run_py(source: &str, options: Option<Object>, mode: Mode) -> Result<JsValue, JsValue> {
53+
let vm = VMStore::init(PY_EVAL_VM_ID.into(), Some(true));
54+
let options = options.unwrap_or_else(Object::new);
55+
let js_vars = {
56+
let prop = Reflect::get(&options, &"vars".into())?;
57+
if prop.is_undefined() {
58+
None
59+
} else if prop.is_object() {
60+
Some(Object::from(prop))
61+
} else {
62+
return Err(TypeError::new("vars must be an object").into());
63+
}
64+
};
65+
66+
vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?;
67+
68+
if let Some(js_vars) = js_vars {
69+
vm.add_to_scope("js_vars".into(), js_vars.into())?;
70+
}
71+
vm.run(source, mode)
72+
}
5073
#[wasm_bindgen(js_name = pyEval)]
5174
/// Evaluate Python code
5275
///
5376
/// ```js
54-
/// pyEval(code, options?);
77+
/// var result = pyEval(code, options?);
5578
/// ```
5679
///
57-
/// `code`: `string`: The Python code to run
80+
/// `code`: `string`: The Python code to run in eval mode
5881
///
5982
/// `options`:
6083
///
@@ -66,26 +89,35 @@ const TS_CMT_START: &'static str = "/*";
6689
/// `undefined` or "console", and it will be a dumb function when giving null.
6790
6891
pub fn eval_py(source: &str, options: Option<Object>) -> Result<JsValue, JsValue> {
69-
let options = options.unwrap_or_else(Object::new);
70-
let js_vars = {
71-
let prop = Reflect::get(&options, &"vars".into())?;
72-
if prop.is_undefined() {
73-
None
74-
} else if prop.is_object() {
75-
Some(Object::from(prop))
76-
} else {
77-
return Err(TypeError::new("vars must be an object").into());
78-
}
79-
};
80-
let vm = VMStore::init(PY_EVAL_VM_ID.into(), Some(true));
81-
82-
vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?;
92+
run_py(source, options, Mode::Eval)
93+
}
8394

84-
if let Some(js_vars) = js_vars {
85-
vm.add_to_scope("js_vars".into(), js_vars.into())?;
86-
}
95+
#[wasm_bindgen(js_name = pyExec)]
96+
/// Evaluate Python code
97+
///
98+
/// ```js
99+
/// pyExec(code, options?);
100+
/// ```
101+
///
102+
/// `code`: `string`: The Python code to run in exec mode
103+
///
104+
/// `options`: The options are the same as eval mode
105+
pub fn exec_py(source: &str, options: Option<Object>) {
106+
let _ = run_py(source, options, Mode::Exec);
107+
}
87108

88-
vm.exec(source)
109+
#[wasm_bindgen(js_name = pyExecSingle)]
110+
/// Evaluate Python code
111+
///
112+
/// ```js
113+
/// var result = pyExecSingle(code, options?);
114+
/// ```
115+
///
116+
/// `code`: `string`: The Python code to run in exec single mode
117+
///
118+
/// `options`: The options are the same as eval mode
119+
pub fn exec_single_py(source: &str, options: Option<Object>) -> Result<JsValue, JsValue> {
120+
run_py(source, options, Mode::Single)
89121
}
90122

91123
#[wasm_bindgen(typescript_custom_section)]

wasm/lib/src/vm_class.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ impl WASMVirtualMachine {
278278
})?
279279
}
280280

281-
fn run(&self, source: &str, mode: compile::Mode) -> Result<JsValue, JsValue> {
281+
pub(crate) fn run(&self, source: &str, mode: compile::Mode) -> Result<JsValue, JsValue> {
282282
self.assert_valid()?;
283283
self.with_unchecked(
284284
|StoredVirtualMachine {

wasm/tests/test_exec_mode.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import time
2+
import sys
3+
4+
from selenium import webdriver
5+
from selenium.webdriver.firefox.options import Options
6+
import pytest
7+
8+
def print_stack(driver):
9+
stack = driver.execute_script(
10+
"return window.__RUSTPYTHON_ERROR_MSG + '\\n' + window.__RUSTPYTHON_ERROR_STACK"
11+
)
12+
print(f"RustPython error stack:\n{stack}", file=sys.stderr)
13+
14+
15+
@pytest.fixture(scope="module")
16+
def driver(request):
17+
options = Options()
18+
options.add_argument('-headless')
19+
driver = webdriver.Firefox(options=options)
20+
try:
21+
driver.get("http://localhost:8080")
22+
except Exception as e:
23+
print_stack(driver)
24+
raise
25+
time.sleep(5)
26+
yield driver
27+
driver.close()
28+
29+
30+
def test_eval_mode(driver):
31+
assert driver.execute_script("return window.rp.pyEval('1+1')") == 2
32+
33+
def test_exec_mode(driver):
34+
assert driver.execute_script("return window.rp.pyExec('1+1')") is None
35+
36+
def test_exec_single_mode(driver):
37+
assert driver.execute_script("return window.rp.pyExecSingle('1+1')") == 2
38+
assert driver.execute_script(
39+
"""
40+
var output = [];
41+
save_output = function(text) {{
42+
output.push(text)
43+
}};
44+
window.rp.pyExecSingle('1+1\\n2+2',{stdout: save_output});
45+
return output;
46+
""") == ['2\n', '4\n']

0 commit comments

Comments
 (0)