Skip to content

Commit aaec075

Browse files
committed
examples: persistent REPL (Repl API); use persistent REPL in examples/js; add tests and docs
1 parent 437f47b commit aaec075

File tree

5 files changed

+128
-5
lines changed

5 files changed

+128
-5
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ The crate provides an example CLI binary with REPL support:
9696
```bash
9797
cargo run --example js -- -e "console.log('Hello World!')"
9898
cargo run --example js script.js
99-
cargo run --example js # no args -> enter a quick REPL (non-persistent environment)
99+
cargo run --example js # no args -> enter persistent REPL (state is retained across inputs)
100100
```
101101

102102
## API Reference

examples/js.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ fn main() {
3636
// No script argument -> start simple REPL (non-persistent environment per-line)
3737
// We intentionally use evaluate_script (safe API) which builds a fresh env per execution.
3838
use std::io::{self, Write};
39-
println!("JavaScript REPL (quick, non-persistent). Type 'exit' or Ctrl-D to quit.");
39+
println!("JavaScript REPL (persistent environment). Type 'exit' or Ctrl-D to quit.");
40+
// persistent environment so definitions persist across inputs
41+
let repl = javascript::Repl::new();
4042
loop {
4143
print!("js> ");
4244
let _ = io::stdout().flush();
@@ -55,7 +57,7 @@ fn main() {
5557
if line.is_empty() {
5658
continue;
5759
}
58-
match javascript::evaluate_script(line) {
60+
match repl.eval(line) {
5961
Ok(result) => print_eval_result(&result),
6062
Err(e) => eprintln!("Error: {:?}", e),
6163
}

src/core.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,100 @@ pub fn evaluate_script<T: AsRef<str>>(script: T) -> Result<Value, JSError> {
10211021
}
10221022
}
10231023

1024+
/// A small persistent REPL environment wrapper.
1025+
///
1026+
/// Notes:
1027+
/// - `Repl::new()` creates a persistent environment and initializes built-ins.
1028+
/// - `Repl::eval(&self, code)` evaluates the provided code in the persistent env
1029+
/// so variables, functions and imports persist between calls.
1030+
pub struct Repl {
1031+
env: JSObjectDataPtr,
1032+
}
1033+
1034+
impl Default for Repl {
1035+
fn default() -> Self {
1036+
Self::new()
1037+
}
1038+
}
1039+
1040+
impl Repl {
1041+
/// Create a new persistent REPL environment (with built-ins initialized).
1042+
pub fn new() -> Self {
1043+
let env: JSObjectDataPtr = Rc::new(RefCell::new(JSObjectData::new()));
1044+
env.borrow_mut().is_function_scope = true;
1045+
// Initialize built-in constructors once for the persistent environment
1046+
initialize_global_constructors(&env);
1047+
Repl { env }
1048+
}
1049+
1050+
/// Evaluate a script in the persistent environment.
1051+
/// Returns the evaluation result or an error.
1052+
pub fn eval<T: AsRef<str>>(&self, script: T) -> Result<Value, JSError> {
1053+
let script = script.as_ref();
1054+
let filtered = filter_input_script(script);
1055+
1056+
// Parse tokens and statements
1057+
let mut tokens = tokenize(&filtered)?;
1058+
let statements = parse_statements(&mut tokens)?;
1059+
1060+
// Inject simple host `std` / `os` shims when importing with the pattern:
1061+
// import * as NAME from "std";
1062+
for line in script.lines() {
1063+
let l = line.trim();
1064+
if l.starts_with("import * as")
1065+
&& l.contains("from")
1066+
&& let (Some(as_idx), Some(from_idx)) = (l.find("as"), l.find("from"))
1067+
{
1068+
let name_part = &l[as_idx + 2..from_idx].trim();
1069+
let name = PropertyKey::String(name_part.trim().to_string());
1070+
if let Some(start_quote) = l[from_idx..].find(|c: char| ['"', '\''].contains(&c)) {
1071+
let quote_char = l[from_idx + start_quote..].chars().next().unwrap();
1072+
let rest = &l[from_idx + start_quote + 1..];
1073+
if let Some(end_quote) = rest.find(quote_char) {
1074+
let module = &rest[..end_quote];
1075+
if module == "std" {
1076+
obj_set_value(&self.env, &name, Value::Object(crate::js_std::make_std_object()?))?;
1077+
} else if module == "os" {
1078+
obj_set_value(&self.env, &name, Value::Object(crate::js_os::make_os_object()?))?;
1079+
}
1080+
}
1081+
}
1082+
}
1083+
}
1084+
1085+
match evaluate_statements(&self.env, &statements) {
1086+
Ok(v) => {
1087+
// If the result is a Promise object (wrapped in Object with __promise property), wait for it to resolve
1088+
if let Value::Object(obj) = &v
1089+
&& let Some(promise_val_rc) = obj_get_value(obj, &"__promise".into())?
1090+
&& let Value::Promise(promise) = &*promise_val_rc.borrow()
1091+
{
1092+
// Run the event loop until the promise is resolved
1093+
loop {
1094+
run_event_loop()?;
1095+
let promise_borrow = promise.borrow();
1096+
match &promise_borrow.state {
1097+
PromiseState::Fulfilled(val) => return Ok(val.clone()),
1098+
PromiseState::Rejected(reason) => {
1099+
return Err(JSError::EvaluationError {
1100+
message: format!("Promise rejected: {}", value_to_string(reason)),
1101+
});
1102+
}
1103+
PromiseState::Pending => {
1104+
// Continue running the event loop
1105+
}
1106+
}
1107+
}
1108+
}
1109+
// Run event loop once to process any queued asynchronous tasks
1110+
run_event_loop()?;
1111+
Ok(v)
1112+
}
1113+
Err(e) => Err(e),
1114+
}
1115+
}
1116+
}
1117+
10241118
pub fn parse_statements(tokens: &mut Vec<Token>) -> Result<Vec<Statement>, JSError> {
10251119
let mut statements = Vec::new();
10261120
while !tokens.is_empty() && !matches!(tokens[0], Token::RBrace) {

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ pub(crate) mod tmpfile;
2525

2626
pub use core::{
2727
JS_DefinePropertyValue, JS_DupValue, JS_Eval, JS_FreeContext, JS_FreeRuntime, JS_FreeValue, JS_GetProperty, JS_NewContext,
28-
JS_NewObject, JS_NewRuntime, JS_NewString, JS_SetProperty, JSClassDef, JSObject, JSStackFrame, JSString, JSValue, PropertyKey, Value,
29-
evaluate_script, get_prop_env, obj_get_value, tokenize,
28+
JS_NewObject, JS_NewRuntime, JS_NewString, JS_SetProperty, JSClassDef, JSObject, JSStackFrame, JSString, JSValue, PropertyKey, Repl,
29+
Value, evaluate_script, get_prop_env, obj_get_value, tokenize,
3030
};
3131
pub use core::{
3232
JS_FLOAT64_NAN, JS_GC_OBJ_TYPE_ASYNC_FUNCTION, JS_GC_OBJ_TYPE_FUNCTION_BYTECODE, JS_GC_OBJ_TYPE_JS_CONTEXT, JS_GC_OBJ_TYPE_JS_OBJECT,

tests/repl_tests.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use javascript::Repl;
2+
// use javascript::evaluate_script; // Commenting out unused import
3+
4+
#[test]
5+
fn repl_persists_values_between_calls() {
6+
let repl = Repl::new();
7+
// define x
8+
let r1 = repl.eval("let x = 42;");
9+
assert!(r1.is_ok());
10+
// now retrieve
11+
let r2 = repl.eval("x");
12+
match r2 {
13+
Ok(javascript::Value::Number(n)) => assert_eq!(n, 42.0),
14+
other => panic!("Expected number 42 from repl, got {:?}", other),
15+
}
16+
}
17+
18+
#[test]
19+
fn repl_allows_function_persistence() {
20+
let repl = Repl::new();
21+
let _ = repl.eval("function add(a,b){ return a + b; }");
22+
let r = repl.eval("add(2,3)");
23+
match r {
24+
Ok(javascript::Value::Number(n)) => assert_eq!(n, 5.0),
25+
other => panic!("Expected number 5 from repl, got {:?}", other),
26+
}
27+
}

0 commit comments

Comments
 (0)