1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
use crate::{
JSError,
core::drain_event_loop,
core::{
JSObjectDataPtr, Value, evaluate_statements, initialize_global_constructors, new_js_object_data, obj_get_key_value,
parse_statements, tokenize,
},
js_promise::PromiseState,
};
/// A small persistent REPL environment wrapper.
///
/// Notes:
/// - `Repl::new()` creates a persistent environment and initializes built-ins.
/// - `Repl::eval(&self, code)` evaluates the provided code in the persistent env
/// so variables, functions and imports persist between calls.
pub struct Repl {
env: JSObjectDataPtr,
}
impl Default for Repl {
fn default() -> Self {
Self::new()
}
}
impl Repl {
/// Create a new persistent REPL environment (with built-ins initialized).
pub fn new() -> Self {
let env: JSObjectDataPtr = new_js_object_data();
env.borrow_mut().is_function_scope = true;
// Initialize built-in constructors once for the persistent environment
initialize_global_constructors(&env).unwrap();
Repl { env }
}
/// Evaluate a script in the persistent environment.
/// Returns the evaluation result or an error.
pub fn eval<T: AsRef<str>>(&self, script: T) -> Result<Value, JSError> {
let script = script.as_ref();
// Parse tokens and statements
let mut tokens = tokenize(script)?;
let statements = parse_statements(&mut tokens)?;
match evaluate_statements(&self.env, &statements) {
Ok(v) => {
// If the result is a Promise object (wrapped in Object with __promise property), wait for it to resolve
if let Value::Object(obj) = &v
&& let Some(promise_val_rc) = obj_get_key_value(obj, &"__promise".into())?
&& let Value::Promise(promise) = &*promise_val_rc.borrow()
{
// Run the event loop until the promise is resolved
loop {
drain_event_loop()?;
let promise_borrow = promise.borrow();
match &promise_borrow.state {
PromiseState::Fulfilled(val) => return Ok(val.clone()),
PromiseState::Rejected(reason) => {
// Preserve the original rejected JS value instead of
// converting it to a string so callers can inspect
// the rejection reason (e.g., error objects).
return Err(raise_throw_error!(reason.clone()));
}
PromiseState::Pending => {
// Continue running the event loop
}
}
}
}
// Run event loop once to process any queued asynchronous tasks
drain_event_loop()?;
Ok(v)
}
Err(e) => Err(e),
}
}
/// Returns true when the given `input` looks like a complete JavaScript
/// top-level expression/program piece (i.e. brackets and template expressions
/// are balanced, strings/comments/regex literals are properly closed).
///
/// This uses heuristics (not a full parser) but covers common REPL cases:
/// - ignores brackets inside single/double-quoted strings
/// - supports template literals and nested ${ ... } expressions
/// - ignores brackets inside // and /* */ comments
/// - attempts to detect regex literals using a simple context heuristic and
/// ignores brackets inside them
pub fn is_complete_input(src: &str) -> bool {
let mut bracket_stack: Vec<char> = Vec::new();
let mut in_single = false;
let mut in_double = false;
let mut in_backtick = false;
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut in_regex = false;
let mut escape = false;
// small helper returns whether a char is considered a token that can
// precede a regex literal (heuristic).
fn can_start_regex(prev: Option<char>) -> bool {
match prev {
None => true,
Some(p) => matches!(
p,
'(' | ',' | '=' | ':' | '[' | '!' | '?' | '{' | '}' | ';' | '\n' | '\r' | '\t' | ' '
),
}
}
let mut prev_non_space: Option<char> = None;
let mut chars = src.chars().peekable();
while let Some(ch) = chars.next() {
// handle escaping inside strings/template/regex
if escape {
escape = false;
// don't treat escaped characters as structure
continue;
}
// start of line comment
if in_line_comment {
if ch == '\n' || ch == '\r' {
in_line_comment = false;
}
prev_non_space = Some(ch);
continue;
}
// inside block comment
if in_block_comment {
if ch == '*'
&& let Some('/') = chars.peek().copied()
{
// consume '/'
let _ = chars.next();
in_block_comment = false;
prev_non_space = Some('/');
continue;
}
prev_non_space = Some(ch);
continue;
}
// if inside a regex, look for unescaped trailing slash
if in_regex {
if ch == '\\' {
escape = true;
continue;
}
if ch == '/' {
// consume optional flags after regex
// we don't need to parse flags here, just stop regex mode
in_regex = false;
// consume following letters (flags) without affecting structure
while let Some(&f) = chars.peek() {
if f.is_ascii_alphabetic() {
chars.next();
} else {
break;
}
}
}
prev_non_space = Some(ch);
continue;
}
// top-level string / template handling
if in_single {
if ch == '\\' {
escape = true;
continue;
}
if ch == '\'' {
in_single = false;
}
prev_non_space = Some(ch);
continue;
}
if in_double {
if ch == '\\' {
escape = true;
continue;
}
if ch == '"' {
in_double = false;
}
prev_non_space = Some(ch);
continue;
}
if in_backtick {
if ch == '\\' {
escape = true;
continue;
}
if ch == '`' {
in_backtick = false;
prev_non_space = Some('`');
continue;
}
// template expression start: ${ ... }
if ch == '$' && chars.peek() == Some(&'{') {
// consume the '{'
let _ = chars.next();
// treat it as an opening brace in the normal bracket stack
bracket_stack.push('}');
prev_non_space = Some('{');
continue;
}
// a closing '}' may appear while still inside the template literal
// if it corresponds to a `${ ... }` expression — pop that marker
if ch == '}' {
if let Some(expected) = bracket_stack.pop() {
if expected != '}' {
return true; // mismatched - treat as complete and surface parse error
}
} else {
// unmatched closing brace inside template - treat as complete
return true;
}
prev_non_space = Some('}');
continue;
}
prev_non_space = Some(ch);
continue;
}
// not inside any obvious literal or comment
match ch {
'\'' => in_single = true,
'"' => in_double = true,
'`' => in_backtick = true,
'/' => {
// Could be line comment '//' or block comment '/*' or regex literal '/.../'
if let Some(&next) = chars.peek() {
if next == '/' {
// consume next and enter line comment
let _ = chars.next();
in_line_comment = true;
prev_non_space = Some('/');
continue;
} else if next == '*' {
// consume next and enter block comment
let _ = chars.next();
in_block_comment = true;
prev_non_space = Some('/');
continue;
}
}
// Heuristic: start regex when previous non-space allows it
if can_start_regex(prev_non_space) {
in_regex = true;
prev_non_space = Some('/');
continue;
}
// otherwise treat as division/operator and continue
}
'(' => bracket_stack.push(')'),
'[' => bracket_stack.push(']'),
'{' => bracket_stack.push('}'),
')' | ']' | '}' => {
if let Some(expected) = bracket_stack.pop() {
if expected != ch {
// mismatched closing; we treat the input as "complete"
// so a syntax error will be surfaced by the evaluator.
return true;
}
} else {
// extra closing bracket — still treat as complete so user gets a parse error
return true;
}
}
_ => {}
}
if !ch.is_whitespace() {
prev_non_space = Some(ch);
}
}
// Input is complete if we are not inside any multiline construct and there are
// no unmatched opening brackets remaining.
!in_single && !in_double && !in_backtick && !in_block_comment && !in_regex && bracket_stack.is_empty()
}
}