Skip to main content

sim_codec_javascript/
lower.rs

1//! Stable `javascript/*` forms and public lowering builders.
2
3use sim_codec::{DecodeBudget, Input, Output, ReadCx};
4use sim_kernel::{
5    CodecId, Error, Expr, LocatedExpr, LocatedExprTree, Origin as KernelOrigin, Result, SourceId,
6    Span as KernelSpan, Symbol,
7};
8
9use crate::{
10    Goal, Limits, Node, NodeKind, Origin, SyntaxTree, Token, TokenKind, parse_module_with_limits,
11    parse_script_with_limits,
12};
13
14const FALLBACK_HEAD: &str = "__sim_expr__(";
15
16/// Public construction seam for JavaScript and syntax extensions.
17#[derive(Clone, Debug, Default)]
18pub struct JavascriptBuilder;
19
20impl JavascriptBuilder {
21    /// Construct a stable namespaced JavaScript form.
22    #[must_use]
23    pub fn form(&self, name: &str, args: Vec<Expr>) -> Expr {
24        Expr::Call {
25            operator: Box::new(Expr::Symbol(Symbol::qualified("javascript", name))),
26            args,
27        }
28    }
29
30    /// Construct a token form shared by the built-in lowering and extensions.
31    #[must_use]
32    pub fn token(&self, kind: &str, text: impl Into<String>, executable: bool) -> Expr {
33        self.form(
34            "token",
35            vec![
36                Expr::Symbol(Symbol::new(kind)),
37                Expr::String(text.into()),
38                Expr::Bool(executable),
39            ],
40        )
41    }
42
43    /// Attach a caller-owned origin whose parent retains the earlier transform.
44    #[must_use]
45    pub fn derived_origin(
46        &self,
47        source: impl Into<String>,
48        span: crate::Span,
49        parent: Option<Origin>,
50    ) -> Origin {
51        Origin {
52            source: source.into(),
53            span,
54            parent: parent.map(Box::new),
55        }
56    }
57
58    /// Lower one public node, enabling downstream node wrappers without parser copying.
59    #[must_use]
60    pub fn node(&self, node: &Node) -> Expr {
61        lower_node(self, node)
62    }
63}
64
65/// Lower a complete tree to stable `javascript/*` forms.
66#[must_use]
67pub fn lower_javascript(tree: &SyntaxTree) -> Expr {
68    let builder = JavascriptBuilder;
69    let tokens = tree
70        .tokens
71        .iter()
72        .map(|token| lower_token(&builder, tree, token));
73    builder.form(
74        goal_name(tree.goal),
75        tokens
76            .chain(tree.root.children.iter().map(|node| builder.node(node)))
77            .collect(),
78    )
79}
80
81fn lower_node(builder: &JavascriptBuilder, node: &Node) -> Expr {
82    builder.form(
83        node_name(&node.kind),
84        node.children
85            .iter()
86            .map(|child| lower_node(builder, child))
87            .collect(),
88    )
89}
90
91fn lower_token(builder: &JavascriptBuilder, tree: &SyntaxTree, token: &Token) -> Expr {
92    builder.token(
93        token_name(&token.kind),
94        &tree.source()[token.span.start..token.span.end],
95        executable_token(&token.kind),
96    )
97}
98
99fn goal_name(goal: Goal) -> &'static str {
100    match goal {
101        Goal::Script => "script",
102        Goal::Module => "module",
103    }
104}
105fn node_name(kind: &NodeKind) -> &'static str {
106    match kind {
107        NodeKind::Script => "script",
108        NodeKind::Module => "module",
109        NodeKind::StatementList => "statement-list",
110        NodeKind::Declaration => "declaration",
111        NodeKind::Statement => "statement",
112        NodeKind::Function => "function",
113        NodeKind::Class => "class",
114        NodeKind::Import => "import",
115        NodeKind::Export => "export",
116        NodeKind::Expression => "expression",
117        NodeKind::Group => "group",
118    }
119}
120fn token_name(kind: &TokenKind) -> &'static str {
121    match kind {
122        TokenKind::Identifier => "identifier",
123        TokenKind::Keyword => "keyword",
124        TokenKind::Number => "number",
125        TokenKind::String => "string",
126        TokenKind::RegExp => "regexp",
127        TokenKind::Template => "template",
128        TokenKind::Punctuator => "punctuator",
129        TokenKind::Trivia => "trivia",
130        TokenKind::End => "end",
131    }
132}
133fn executable_token(kind: &TokenKind) -> bool {
134    matches!(
135        kind,
136        TokenKind::Identifier
137            | TokenKind::Number
138            | TokenKind::String
139            | TokenKind::RegExp
140            | TokenKind::Template
141            | TokenKind::Punctuator
142    )
143}
144
145/// Decode Script source using the shared codec budget.
146pub fn decode_javascript(
147    cx: &mut ReadCx<'_>,
148    source: &str,
149    budget: &mut DecodeBudget,
150) -> Result<Expr> {
151    budget.check_input_bytes(cx.codec, source.len())?;
152    if source.starts_with(FALLBACK_HEAD) {
153        return decode_fallback(cx.codec, source, budget);
154    }
155    let tree = parse_script_with_limits(source, parser_limits(budget))
156        .map_err(|e| codec_error(cx.codec, e.to_string()))?;
157    budget.check_tokens(cx.codec, tree.tokens.len())?;
158    Ok(lower_javascript(&tree))
159}
160
161/// Decode into a root-located expression.
162pub fn decode_javascript_located(
163    cx: &mut ReadCx<'_>,
164    source_id: impl Into<String>,
165    input: Input,
166) -> Result<LocatedExpr> {
167    let source = input_text(cx.codec, input)?;
168    let source_id = SourceId(source_id.into());
169    cx.cx.sources_mut().intern_text(source_id.clone(), &source);
170    let mut budget = DecodeBudget::new(cx.limits);
171    let expr = decode_javascript(cx, &source, &mut budget)?;
172    Ok(LocatedExpr {
173        expr,
174        origin: Some(origin(cx.codec, source_id, 0, source.len())),
175    })
176}
177
178/// Decode into a recursively located expression tree.
179pub fn decode_javascript_tree(
180    cx: &mut ReadCx<'_>,
181    source_id: impl Into<String>,
182    input: Input,
183) -> Result<LocatedExprTree> {
184    let source = input_text(cx.codec, input)?;
185    let source_id = SourceId(source_id.into());
186    cx.cx.sources_mut().intern_text(source_id.clone(), &source);
187    let mut budget = DecodeBudget::new(cx.limits);
188    if source.starts_with(FALLBACK_HEAD) {
189        let mut out =
190            LocatedExprTree::from_expr_recursive(decode_javascript(cx, &source, &mut budget)?);
191        out.origin = Some(origin(cx.codec, source_id, 0, source.len()));
192        return Ok(out);
193    }
194    let parsed = parse_script_with_limits(&source, parser_limits(&budget))
195        .map_err(|e| codec_error(cx.codec, e.to_string()))?;
196    budget.check_tokens(cx.codec, parsed.tokens.len())?;
197    let mut out = LocatedExprTree::from_expr_recursive(lower_javascript(&parsed));
198    out.origin = Some(origin(cx.codec, source_id.clone(), 0, source.len()));
199    for (leaf, token) in out.children.iter_mut().skip(1).zip(&parsed.tokens) {
200        leaf.origin = Some(origin(
201            cx.codec,
202            source_id.clone(),
203            token.span.start,
204            token.span.end,
205        ));
206    }
207    for (child, node) in out
208        .children
209        .iter_mut()
210        .skip(1 + parsed.tokens.len())
211        .zip(&parsed.root.children)
212    {
213        locate_node(child, &parsed, node, cx.codec, &source_id);
214    }
215    Ok(out)
216}
217
218fn locate_node(
219    tree: &mut LocatedExprTree,
220    syntax: &SyntaxTree,
221    node: &Node,
222    codec: CodecId,
223    source: &SourceId,
224) {
225    if let Some((start, end)) = node_bytes(syntax, node) {
226        tree.origin = Some(origin(codec, source.clone(), start, end));
227    }
228    for (child, child_node) in tree.children.iter_mut().skip(1).zip(&node.children) {
229        locate_node(child, syntax, child_node, codec, source);
230    }
231}
232
233fn node_bytes(tree: &SyntaxTree, node: &Node) -> Option<(usize, usize)> {
234    let tokens = &tree.tokens[node.tokens.clone()];
235    Some((tokens.first()?.span.start, tokens.last()?.span.end))
236}
237
238/// Canonically encode lowered forms or the established tagged fallback.
239pub fn encode_javascript(expr: &Expr) -> Result<Output> {
240    if javascript_call(expr).is_some() {
241        let mut out = String::new();
242        encode_form(expr, &mut out)?;
243        let goal = javascript_call(expr).map(|x| x.0).unwrap_or_default();
244        let parsed = if goal == "module" {
245            parse_module_with_limits(&out, Limits::default())
246        } else {
247            parse_script_with_limits(&out, Limits::default())
248        }
249        .map_err(|e| codec_error(crate::JAVASCRIPT_CODEC_ID, e.to_string()))?;
250        if lower_javascript(&parsed) != *expr {
251            return Err(codec_error(
252                crate::JAVASCRIPT_CODEC_ID,
253                "javascript form is not the canonical lowering of its source",
254            ));
255        }
256        return Ok(Output::Text(out));
257    }
258    Ok(Output::Text(format!(
259        "{FALLBACK_HEAD}{})",
260        sim_codec_json::expr_to_json(expr)
261    )))
262}
263
264fn encode_form(expr: &Expr, out: &mut String) -> Result<()> {
265    let Some((head, args)) = javascript_call(expr) else {
266        return Err(codec_error(
267            crate::JAVASCRIPT_CODEC_ID,
268            "malformed javascript form",
269        ));
270    };
271    if head == "token" && args.len() == 3 {
272        if let (Expr::Symbol(_), Expr::String(text), Expr::Bool(_)) = (&args[0], &args[1], &args[2])
273        {
274            out.push_str(text);
275            return Ok(());
276        }
277        return Err(codec_error(
278            crate::JAVASCRIPT_CODEC_ID,
279            "javascript/token expects kind, text, executable",
280        ));
281    }
282    if matches!(
283        head,
284        "script"
285            | "module"
286            | "statement-list"
287            | "declaration"
288            | "statement"
289            | "function"
290            | "class"
291            | "import"
292            | "export"
293            | "expression"
294            | "group"
295    ) {
296        for arg in args {
297            encode_form(arg, out)?;
298        }
299        return Ok(());
300    }
301    Err(codec_error(
302        crate::JAVASCRIPT_CODEC_ID,
303        format!("unknown javascript form javascript/{head}"),
304    ))
305}
306
307fn decode_fallback(codec: CodecId, source: &str, budget: &mut DecodeBudget) -> Result<Expr> {
308    let json = source
309        .strip_prefix(FALLBACK_HEAD)
310        .and_then(|x| x.strip_suffix(')'))
311        .ok_or_else(|| codec_error(codec, "malformed __sim_expr__ fallback"))?;
312    let value = serde_json::from_str(json)
313        .map_err(|e| codec_error(codec, format!("malformed tagged fallback: {e}")))?;
314    sim_codec_json::json_to_expr(codec, &value, budget, 0)
315}
316fn parser_limits(b: &DecodeBudget) -> Limits {
317    let l = b.limits();
318    Limits {
319        max_bytes: l.max_input_bytes,
320        max_tokens: l.max_tokens,
321        max_nesting: l.max_depth,
322        max_lines: l.max_tokens,
323        max_nodes: l.max_tokens,
324    }
325}
326fn javascript_call(expr: &Expr) -> Option<(&str, &[Expr])> {
327    let Expr::Call { operator, args } = expr else {
328        return None;
329    };
330    let Expr::Symbol(s) = operator.as_ref() else {
331        return None;
332    };
333    (s.namespace.as_deref().map(AsRef::as_ref) == Some("javascript"))
334        .then_some((s.name.as_ref(), args))
335}
336fn input_text(codec: CodecId, input: Input) -> Result<String> {
337    match input {
338        Input::Text(x) => Ok(x),
339        Input::Bytes(x) => String::from_utf8(x)
340            .map_err(|e| codec_error(codec, format!("codec input is not valid UTF-8: {e}"))),
341    }
342}
343fn origin(codec: CodecId, source: SourceId, start: usize, end: usize) -> KernelOrigin {
344    KernelOrigin {
345        codec,
346        source,
347        span: KernelSpan { start, end },
348        trivia: Vec::new(),
349    }
350}
351fn codec_error(codec: CodecId, message: impl Into<String>) -> Error {
352    Error::CodecError {
353        codec,
354        message: message.into(),
355    }
356}