Skip to main content

sim_lib_lang_javascript/
runtime.rs

1use sim_kernel::{Error, Expr, Result};
2use sim_lib_standard_core::LanguageProfile;
3use std::collections::BTreeMap;
4
5/// Values in the phase's scalar JavaScript core.
6#[derive(Clone, Debug, PartialEq)]
7pub enum JavascriptValue {
8    /// `undefined`.
9    Undefined,
10    /// `null`.
11    Null,
12    /// Boolean.
13    Bool(bool),
14    /// Installed Number-domain value.
15    Number(f64),
16    /// Installed BigInt-domain value, bounded by its installed representation.
17    BigInt(i64),
18    /// String scalar.
19    String(String),
20    /// Cyclic identity from the managed arena.
21    Managed(sim_lib_mutation::ManagedHandle),
22}
23/// Abrupt or normal completion produced directly by statements.
24#[derive(Clone, Debug, PartialEq)]
25pub enum Completion {
26    /// Normal completion.
27    Normal(JavascriptValue),
28    /// `break`.
29    Break,
30    /// `continue`.
31    Continue,
32    /// `return`.
33    Return(JavascriptValue),
34    /// `throw`.
35    Throw(JavascriptValue),
36}
37/// Only the lexical and variable bindings required by checked single-agent clauses.
38#[derive(Clone, Debug, Default)]
39pub struct JavascriptState {
40    bindings: BTreeMap<String, JavascriptValue>,
41}
42impl JavascriptState {
43    /// Read a binding.
44    pub fn get(&self, name: &str) -> Option<&JavascriptValue> {
45        self.bindings.get(name)
46    }
47    /// Define or assign a binding.
48    pub fn set(&mut self, name: impl Into<String>, value: JavascriptValue) {
49        self.bindings.insert(name.into(), value);
50    }
51}
52/// Bounded direct evaluator over stable `javascript/*` lowering.
53#[derive(Clone, Debug)]
54pub struct JavascriptEvalPolicy {
55    profile: LanguageProfile,
56    max_steps: usize,
57}
58impl JavascriptEvalPolicy {
59    /// Create the direct evaluator.
60    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    /// Selected profile.
72    pub fn profile(&self) -> &LanguageProfile {
73        &self.profile
74    }
75    /// Evaluate a codec-produced Script or Module lowering without an intermediate plan.
76    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}