1use 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#[derive(Clone, Debug, Default)]
18pub struct JavascriptBuilder;
19
20impl JavascriptBuilder {
21 #[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 #[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 #[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 #[must_use]
60 pub fn node(&self, node: &Node) -> Expr {
61 lower_node(self, node)
62 }
63}
64
65#[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
145pub 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
161pub 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
178pub 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
238pub 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}