1use sim_kernel::{Error, Expr, Result};
2use sim_lib_standard_core::LanguageProfile;
3use std::collections::BTreeMap;
4
5#[derive(Clone, Debug, PartialEq)]
7pub enum JavascriptValue {
8 Undefined,
10 Null,
12 Bool(bool),
14 Number(f64),
16 BigInt(i64),
18 String(String),
20 Managed(sim_lib_mutation::ManagedHandle),
22}
23#[derive(Clone, Debug, PartialEq)]
25pub enum Completion {
26 Normal(JavascriptValue),
28 Break,
30 Continue,
32 Return(JavascriptValue),
34 Throw(JavascriptValue),
36}
37#[derive(Clone, Debug, Default)]
39pub struct JavascriptState {
40 bindings: BTreeMap<String, JavascriptValue>,
41}
42impl JavascriptState {
43 pub fn get(&self, name: &str) -> Option<&JavascriptValue> {
45 self.bindings.get(name)
46 }
47 pub fn set(&mut self, name: impl Into<String>, value: JavascriptValue) {
49 self.bindings.insert(name.into(), value);
50 }
51}
52#[derive(Clone, Debug)]
54pub struct JavascriptEvalPolicy {
55 profile: LanguageProfile,
56 max_steps: usize,
57}
58impl JavascriptEvalPolicy {
59 pub fn new(max_steps: usize) -> Result<Self> {
61 if max_steps == 0 {
62 return Err(Error::Eval(
63 "javascript direct evaluator requires a non-zero step bound".into(),
64 ));
65 }
66 Ok(Self {
67 profile: crate::javascript_core_profile(),
68 max_steps,
69 })
70 }
71 pub fn profile(&self) -> &LanguageProfile {
73 &self.profile
74 }
75 pub fn eval_lowered(&self, lowered: &Expr, state: &mut JavascriptState) -> Result<Completion> {
77 let tokens = lowered_tokens(lowered)?;
78 Parser {
79 tokens: &tokens,
80 at: 0,
81 steps: self.max_steps,
82 state,
83 }
84 .program()
85 }
86}
87fn lowered_tokens(expr: &Expr) -> Result<Vec<String>> {
88 let Expr::Call { operator, args } = expr else {
89 return Err(Error::Eval(
90 "javascript evaluator accepts only codec/javascript lowered forms".into(),
91 ));
92 };
93 let Expr::Symbol(head) = operator.as_ref() else {
94 return Err(Error::Eval("malformed javascript lowering".into()));
95 };
96 if head.namespace.as_deref().map(AsRef::as_ref) != Some("javascript")
97 || !matches!(head.name.as_ref(), "script" | "module")
98 {
99 return Err(Error::Eval(
100 "javascript evaluator accepts only codec/javascript Script or Module forms".into(),
101 ));
102 }
103 let mut out = Vec::new();
104 for arg in args {
105 if let Expr::Call { operator, args } = arg
106 && matches!(operator.as_ref(), Expr::Symbol(s) if s.namespace.as_deref().map(AsRef::as_ref)==Some("javascript") && s.name.as_ref()=="token")
107 {
108 if let [Expr::Symbol(kind), Expr::String(text), Expr::Bool(_)] = args.as_slice() {
109 if !matches!(kind.name.as_ref(), "trivia" | "end") {
110 out.push(text.clone());
111 }
112 } else {
113 return Err(Error::Eval("malformed javascript token".into()));
114 }
115 }
116 }
117 Ok(out)
118}
119struct Parser<'a> {
120 tokens: &'a [String],
121 at: usize,
122 steps: usize,
123 state: &'a mut JavascriptState,
124}
125impl Parser<'_> {
126 fn charge(&mut self) -> Result<()> {
127 if self.steps == 0 {
128 Err(Error::Eval(
129 "javascript direct evaluation step bound exhausted".into(),
130 ))
131 } else {
132 self.steps -= 1;
133 Ok(())
134 }
135 }
136 fn program(&mut self) -> Result<Completion> {
137 let mut last = JavascriptValue::Undefined;
138 while self.at < self.tokens.len() {
139 self.charge()?;
140 match self.statement()? {
141 Completion::Normal(v) => last = v,
142 abrupt => return Ok(abrupt),
143 }
144 }
145 Ok(Completion::Normal(last))
146 }
147 fn statement(&mut self) -> Result<Completion> {
148 if self.eat(";") {
149 return Ok(Completion::Normal(JavascriptValue::Undefined));
150 }
151 if self.eat("{") {
152 let mut last = JavascriptValue::Undefined;
153 while !self.eat("}") {
154 match self.statement()? {
155 Completion::Normal(v) => last = v,
156 c => return Ok(c),
157 }
158 }
159 return Ok(Completion::Normal(last));
160 }
161 if matches!(self.peek(), Some("let" | "const" | "var")) {
162 self.at += 1;
163 let v = self.declaration()?;
164 self.eat(";");
165 return Ok(Completion::Normal(v));
166 }
167 if self.eat("if") {
168 self.expect("(")?;
169 let c = self.expr(0)?;
170 self.expect(")")?;
171 let yes_start = self.at;
172 self.skip_statement()?;
173 let yes_end = self.at;
174 let no_range = if self.eat("else") {
175 let start = self.at;
176 self.skip_statement()?;
177 Some((start, self.at))
178 } else {
179 None
180 };
181 let selected = if truthy(&c) {
182 Some((yes_start, yes_end))
183 } else {
184 no_range
185 };
186 return if let Some((start, end)) = selected {
187 let mut branch = Parser {
188 tokens: &self.tokens[start..end],
189 at: 0,
190 steps: self.steps,
191 state: self.state,
192 };
193 let completion = branch.statement()?;
194 self.steps = branch.steps;
195 Ok(completion)
196 } else {
197 Ok(Completion::Normal(JavascriptValue::Undefined))
198 };
199 }
200 if self.eat("while") {
201 return self.while_loop();
202 }
203 if self.eat("break") {
204 self.eat(";");
205 return Ok(Completion::Break);
206 }
207 if self.eat("continue") {
208 self.eat(";");
209 return Ok(Completion::Continue);
210 }
211 if self.eat("return") {
212 let v = if self.peek() == Some(";") {
213 JavascriptValue::Undefined
214 } else {
215 self.expr(0)?
216 };
217 self.eat(";");
218 return Ok(Completion::Return(v));
219 }
220 if self.eat("throw") {
221 let v = self.expr(0)?;
222 self.eat(";");
223 return Ok(Completion::Throw(v));
224 }
225 if self.at + 1 < self.tokens.len()
226 && matches!(self.tokens[self.at + 1].as_str(), "=" | "+=" | "-=")
227 {
228 let name = self.tokens[self.at].clone();
229 let op = self.tokens[self.at + 1].clone();
230 self.at += 2;
231 let rhs = self.expr(0)?;
232 let value = if op == "=" {
233 rhs
234 } else {
235 binary(
236 &op[..1],
237 self.state.get(&name).cloned().ok_or_else(|| {
238 Error::Eval(format!("javascript reference {name} is not defined"))
239 })?,
240 rhs,
241 )?
242 };
243 self.state.set(name, value.clone());
244 self.eat(";");
245 return Ok(Completion::Normal(value));
246 }
247 let v = self.expr(0)?;
248 self.eat(";");
249 Ok(Completion::Normal(v))
250 }
251 fn declaration(&mut self) -> Result<JavascriptValue> {
252 if self.eat("[") {
253 let mut names = Vec::new();
254 while !self.eat("]") {
255 names.push(self.take()?);
256 self.eat(",");
257 }
258 self.expect("=")?;
259 self.expect("[")?;
260 let mut values = Vec::new();
261 while !self.eat("]") {
262 values.push(self.expr(0)?);
263 self.eat(",");
264 }
265 for (n, v) in names.into_iter().zip(values) {
266 self.state.set(n, v);
267 }
268 return Ok(JavascriptValue::Undefined);
269 }
270 let name = self.take()?;
271 let value = if self.eat("=") {
272 self.expr(0)?
273 } else {
274 JavascriptValue::Undefined
275 };
276 self.state.set(name, value.clone());
277 Ok(value)
278 }
279 fn while_loop(&mut self) -> Result<Completion> {
280 self.expect("(")?;
281 let cond_start = self.at;
282 let mut depth = 1;
283 while depth > 0 {
284 match self.take()?.as_str() {
285 "(" => depth += 1,
286 ")" => depth -= 1,
287 _ => {}
288 }
289 }
290 let cond_end = self.at - 1;
291 let body_start = self.at;
292 self.skip_statement()?;
293 let body_end = self.at;
294 let mut last = JavascriptValue::Undefined;
295 loop {
296 let cond_tokens = &self.tokens[cond_start..cond_end];
297 let cond = Parser {
298 tokens: cond_tokens,
299 at: 0,
300 steps: self.steps,
301 state: self.state,
302 }
303 .expr(0)?;
304 if !truthy(&cond) {
305 break;
306 }
307 let body_tokens = &self.tokens[body_start..body_end];
308 let mut body = Parser {
309 tokens: body_tokens,
310 at: 0,
311 steps: self.steps,
312 state: self.state,
313 };
314 match body.statement()? {
315 Completion::Normal(v) => last = v,
316 Completion::Continue => {}
317 Completion::Break => break,
318 c => return Ok(c),
319 }
320 self.steps = body.steps;
321 }
322 Ok(Completion::Normal(last))
323 }
324 fn expr(&mut self, min: u8) -> Result<JavascriptValue> {
325 self.charge()?;
326 let mut left = self.unary()?;
327 while let Some(op) = self.peek() {
328 let (l, r) = match op {
329 "||" => (1, 2),
330 "&&" => (3, 4),
331 "==" | "!=" | "===" | "!==" => (5, 6),
332 "<" | "<=" | ">" | ">=" => (7, 8),
333 "+" | "-" => (9, 10),
334 "*" | "/" | "%" => (11, 12),
335 _ => break,
336 };
337 if l < min {
338 break;
339 }
340 let op = op.to_owned();
341 self.at += 1;
342 let right = self.expr(r)?;
343 left = binary(&op, left, right)?;
344 }
345 Ok(left)
346 }
347 fn unary(&mut self) -> Result<JavascriptValue> {
348 if self.eat("!") {
349 return Ok(JavascriptValue::Bool(!truthy(&self.unary()?)));
350 }
351 if self.eat("-") {
352 return match self.unary()? {
353 JavascriptValue::Number(n) => Ok(JavascriptValue::Number(-n)),
354 JavascriptValue::BigInt(n) => Ok(JavascriptValue::BigInt(-n)),
355 v => Err(Error::Eval(format!("javascript unary - rejects {v:?}"))),
356 };
357 }
358 self.atom()
359 }
360 fn atom(&mut self) -> Result<JavascriptValue> {
361 let t = self.take()?;
362 match t.as_str() {
363 "undefined" => Ok(JavascriptValue::Undefined),
364 "null" => Ok(JavascriptValue::Null),
365 "true" => Ok(JavascriptValue::Bool(true)),
366 "false" => Ok(JavascriptValue::Bool(false)),
367 "(" => {
368 let v = self.expr(0)?;
369 self.expect(")")?;
370 Ok(v)
371 }
372 _ if t.ends_with('n') && t[..t.len() - 1].bytes().all(|b| b.is_ascii_digit()) => t
373 [..t.len() - 1]
374 .parse()
375 .map(JavascriptValue::BigInt)
376 .map_err(|_| Error::Eval("javascript BigInt exceeds installed domain".into())),
377 _ if t.as_bytes().first().is_some_and(u8::is_ascii_digit) => t
378 .parse()
379 .map(JavascriptValue::Number)
380 .map_err(|_| Error::Eval(format!("invalid javascript Number {t}"))),
381 _ if t.starts_with(['\'', '"']) => {
382 Ok(JavascriptValue::String(t[1..t.len() - 1].to_owned()))
383 }
384 _ => self
385 .state
386 .get(&t)
387 .cloned()
388 .ok_or_else(|| Error::Eval(format!("javascript reference {t} is not defined"))),
389 }
390 }
391 fn skip_statement(&mut self) -> Result<()> {
392 if self.eat("{") {
393 let mut d = 1;
394 while d > 0 {
395 match self.take()?.as_str() {
396 "{" => d += 1,
397 "}" => d -= 1,
398 _ => {}
399 }
400 }
401 } else {
402 while self.at < self.tokens.len() && !self.eat(";") {
403 self.at += 1;
404 }
405 }
406 Ok(())
407 }
408 fn peek(&self) -> Option<&str> {
409 self.tokens.get(self.at).map(String::as_str)
410 }
411 fn eat(&mut self, t: &str) -> bool {
412 if self.peek() == Some(t) {
413 self.at += 1;
414 true
415 } else {
416 false
417 }
418 }
419 fn expect(&mut self, t: &str) -> Result<()> {
420 if self.eat(t) {
421 Ok(())
422 } else {
423 Err(Error::Eval(format!("javascript expected {t}")))
424 }
425 }
426 fn take(&mut self) -> Result<String> {
427 let t = self
428 .tokens
429 .get(self.at)
430 .cloned()
431 .ok_or_else(|| Error::Eval("javascript unexpected end of input".into()))?;
432 self.at += 1;
433 Ok(t)
434 }
435}
436fn truthy(v: &JavascriptValue) -> bool {
437 match v {
438 JavascriptValue::Undefined | JavascriptValue::Null | JavascriptValue::Bool(false) => false,
439 JavascriptValue::Number(n) => *n != 0.0 && !n.is_nan(),
440 JavascriptValue::String(s) => !s.is_empty(),
441 _ => true,
442 }
443}
444fn to_number(v: &JavascriptValue) -> Result<f64> {
445 match v {
446 JavascriptValue::Number(n) => Ok(*n),
447 JavascriptValue::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
448 JavascriptValue::Null => Ok(0.0),
449 JavascriptValue::String(s) => s
450 .parse()
451 .map_err(|_| Error::Eval("javascript numeric coercion failed".into())),
452 _ => Err(Error::Eval("javascript numeric coercion failed".into())),
453 }
454}
455fn binary(op: &str, a: JavascriptValue, b: JavascriptValue) -> Result<JavascriptValue> {
456 use JavascriptValue::*;
457 match op {
458 "&&" => Ok(if truthy(&a) { b } else { a }),
459 "||" => Ok(if truthy(&a) { a } else { b }),
460 "===" => Ok(Bool(a == b)),
461 "!==" => Ok(Bool(a != b)),
462 "==" | "!=" => {
463 let eq = if a == b {
464 true
465 } else {
466 to_number(&a).ok() == to_number(&b).ok()
467 };
468 Ok(Bool(if op == "==" { eq } else { !eq }))
469 }
470 "+" => match (a, b) {
471 (String(a), b) => Ok(String(a + &display(&b))),
472 (a, String(b)) => Ok(String(display(&a) + &b)),
473 (BigInt(a), BigInt(b)) => a
474 .checked_add(b)
475 .map(BigInt)
476 .ok_or_else(|| Error::Eval("javascript BigInt exceeds installed domain".into())),
477 (a, b) => Ok(Number(to_number(&a)? + to_number(&b)?)),
478 },
479 "-" | "*" | "/" | "%" => {
480 let (a, b) = (to_number(&a)?, to_number(&b)?);
481 Ok(Number(match op {
482 "-" => a - b,
483 "*" => a * b,
484 "/" => a / b,
485 _ => a % b,
486 }))
487 }
488 "<" | "<=" | ">" | ">=" => {
489 let (a, b) = (to_number(&a)?, to_number(&b)?);
490 Ok(Bool(match op {
491 "<" => a < b,
492 "<=" => a <= b,
493 ">" => a > b,
494 _ => a >= b,
495 }))
496 }
497 _ => Err(Error::Eval(format!("unsupported javascript operator {op}"))),
498 }
499}
500fn display(v: &JavascriptValue) -> String {
501 match v {
502 JavascriptValue::Undefined => "undefined".into(),
503 JavascriptValue::Null => "null".into(),
504 JavascriptValue::Bool(v) => v.to_string(),
505 JavascriptValue::Number(v) => v.to_string(),
506 JavascriptValue::BigInt(v) => v.to_string(),
507 JavascriptValue::String(v) => v.clone(),
508 JavascriptValue::Managed(_) => "[object Object]".into(),
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use sim_kernel::Symbol;
516 fn form(n: &str, a: Vec<Expr>) -> Expr {
517 Expr::Call {
518 operator: Box::new(Expr::Symbol(Symbol::qualified("javascript", n))),
519 args: a,
520 }
521 }
522 fn tok(k: &str, t: &str) -> Expr {
523 form(
524 "token",
525 vec![
526 Expr::Symbol(Symbol::new(k)),
527 Expr::String(t.into()),
528 Expr::Bool(true),
529 ],
530 )
531 }
532 fn script(ts: &[&str]) -> Expr {
533 form(
534 "script",
535 ts.iter()
536 .map(|t| {
537 tok(
538 if t.chars().next().unwrap_or('_').is_ascii_digit() {
539 "number"
540 } else {
541 "punctuator"
542 },
543 t,
544 )
545 })
546 .collect(),
547 )
548 }
549 #[test]
550 fn evaluates_declarations_coercion_and_loops_directly() {
551 let e = script(&[
552 "let", "x", "=", "0", ";", "while", "(", "x", "<", "4", ")", "{", "x", "+=", "1", ";",
553 "}", "x", "===", "4", ";",
554 ]);
555 let mut s = JavascriptState::default();
556 assert_eq!(
557 JavascriptEvalPolicy::new(256)
558 .unwrap()
559 .eval_lowered(&e, &mut s)
560 .unwrap(),
561 Completion::Normal(JavascriptValue::Bool(true))
562 );
563 }
564 #[test]
565 fn destructuring_and_abrupt_completion_are_direct() {
566 let e = script(&[
567 "let", "[", "a", ",", "b", "]", "=", "[", "40", ",", "2", "]", ";", "return", "a", "+",
568 "b", ";",
569 ]);
570 let mut s = JavascriptState::default();
571 assert_eq!(
572 JavascriptEvalPolicy::new(128)
573 .unwrap()
574 .eval_lowered(&e, &mut s)
575 .unwrap(),
576 Completion::Return(JavascriptValue::Number(42.0))
577 );
578 }
579 #[test]
580 fn conditional_executes_only_the_selected_statement() {
581 let e = script(&[
582 "let", "x", "=", "0", ";", "if", "(", "false", ")", "{", "x", "=", "1", ";", "}",
583 "else", "{", "x", "=", "42", ";", "}", "x", ";",
584 ]);
585 let mut s = JavascriptState::default();
586 assert_eq!(
587 JavascriptEvalPolicy::new(128)
588 .unwrap()
589 .eval_lowered(&e, &mut s)
590 .unwrap(),
591 Completion::Normal(JavascriptValue::Number(42.0))
592 );
593 }
594 #[test]
595 fn rejects_non_codec_forms_and_bounds_steps() {
596 let mut s = JavascriptState::default();
597 assert!(
598 JavascriptEvalPolicy::new(1)
599 .unwrap()
600 .eval_lowered(&script(&["1", "+", "2"]), &mut s)
601 .is_err()
602 );
603 assert!(
604 JavascriptEvalPolicy::new(8)
605 .unwrap()
606 .eval_lowered(&Expr::Bool(true), &mut s)
607 .is_err()
608 );
609 }
610}