1use sim_codec::{DecodeBudget, Input, Output, ReadCx};
4use sim_codec_javascript::{JavascriptBuilder, Origin, Span, Token, TokenKind};
5use sim_kernel::{
6 Error, Expr, LocatedExpr, LocatedExprTree, Origin as KernelOrigin, Result, SourceId,
7 Span as KernelSpan,
8};
9
10use crate::{
11 Language, Limits, SyntaxKind, SyntaxNode, SyntaxTree, TYPESCRIPT_CODEC_ID,
12 parse_module_with_limits,
13};
14
15#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct AnnotationReference {
18 pub span: Span,
20 pub context: Vec<String>,
22 pub origin: Origin,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct EvaluationGap {
29 pub span: Span,
31 pub construct: String,
33 pub reason: String,
35}
36
37impl std::fmt::Display for EvaluationGap {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(
40 f,
41 "TypeScript evaluation gap at {}..{}: {} requires {}",
42 self.span.start, self.span.end, self.construct, self.reason
43 )
44 }
45}
46impl std::error::Error for EvaluationGap {}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct LoweredTypeScript {
51 pub javascript: Expr,
53 pub annotations: Vec<AnnotationReference>,
55 pub source_tree: SyntaxTree,
57}
58
59pub fn lower_typescript(
61 tree: &SyntaxTree,
62) -> std::result::Result<LoweredTypeScript, EvaluationGap> {
63 if let Some(gap) = first_gap(tree) {
64 return Err(gap);
65 }
66 let erased = erased_tokens(tree);
67 let builder = JavascriptBuilder;
68 let javascript = builder.form(
69 "module",
70 erased
71 .iter()
72 .map(|token| {
73 builder.token(
74 token_name(&token.kind),
75 &tree.source()[token.span.start..token.span.end],
76 executable(&token.kind),
77 )
78 })
79 .collect(),
80 );
81 let annotations = tree
82 .nodes
83 .iter()
84 .filter_map(|node| match node {
85 SyntaxNode::TypeScript {
86 kind: SyntaxKind::Annotation,
87 span,
88 context,
89 } => Some(AnnotationReference {
90 span: *span,
91 context: context.clone(),
92 origin: builder.derived_origin(
93 "typescript",
94 *span,
95 Some(builder.derived_origin("javascript", *span, None)),
96 ),
97 }),
98 _ => None,
99 })
100 .collect();
101 Ok(LoweredTypeScript {
102 javascript,
103 annotations,
104 source_tree: tree.clone(),
105 })
106}
107
108fn first_gap(tree: &SyntaxTree) -> Option<EvaluationGap> {
109 let source = tree.source();
110 for token in significant(&tree.tokens) {
111 let text = token_text(source, token);
112 let reason = match text {
113 "enum" | "namespace" | "module" => {
114 Some("an emitter transform and runtime code generation")
115 }
116 "satisfies" | "asserts" => Some("a type-checker acceptance decision"),
117 "as" if !is_module_alias(source, token.span.start) => {
118 Some("a type-checker acceptance decision")
119 }
120 "@" => Some("a decorator transform and target selection"),
121 _ => None,
122 };
123 if let Some(reason) = reason {
124 return Some(gap(token.span, text, reason));
125 }
126 }
127 if tree.language == Language::Tsx
128 && let Some(span) = tree.nodes.iter().find_map(|node| match node {
129 SyntaxNode::TypeScript {
130 kind: SyntaxKind::Jsx,
131 span,
132 ..
133 } => Some(*span),
134 _ => None,
135 })
136 {
137 return Some(gap(
138 span,
139 "JSX/TSX",
140 "a JSX emitter transform and target selection",
141 ));
142 }
143 for window in significant(&tree.tokens).windows(2) {
145 let first = token_text(source, window[0]);
146 if matches!(first, "public" | "private" | "protected")
147 && source[..window[0].span.start]
148 .rsplit_once("constructor(")
149 .is_some_and(|(_, tail)| !tail.contains(')'))
150 {
151 return Some(gap(
152 window[0].span,
153 "parameter property",
154 "an emitter transform and runtime assignment",
155 ));
156 }
157 }
158 None
159}
160
161fn erased_tokens(tree: &SyntaxTree) -> Vec<&Token> {
162 let tokens = significant(&tree.tokens);
163 let source = tree.source();
164 let mut erase = vec![false; tokens.len()];
165 let mut index = 0;
166 while index < tokens.len() {
167 let text = token_text(source, tokens[index]);
168 if matches!(text, "interface" | "type")
169 || (text == "declare"
170 && tokens.get(index + 1).is_some_and(|t| {
171 matches!(
172 token_text(source, t),
173 "interface" | "class" | "function" | "const" | "let" | "var"
174 )
175 }))
176 {
177 let end = declaration_end(&tokens, source, index);
178 erase[index..end].fill(true);
179 index = end;
180 continue;
181 }
182 if matches!(text, "readonly" | "abstract" | "override" | "declare") {
183 erase[index] = true;
184 }
185 if text == ":" {
186 let end = type_end(&tokens, source, index + 1);
187 erase[index..end].fill(true);
188 index = end;
189 continue;
190 }
191 if text == "<" && looks_like_generic(&tokens, source, index) {
192 let end = matching_angle(&tokens, source, index).map_or(index + 1, |x| x + 1);
193 erase[index..end].fill(true);
194 index = end;
195 continue;
196 }
197 index += 1;
198 }
199 tree.tokens
200 .iter()
201 .filter(|token| {
202 token.kind == TokenKind::Trivia
203 || tokens
204 .iter()
205 .position(|candidate| std::ptr::eq(*candidate, *token))
206 .is_none_or(|i| !erase[i])
207 })
208 .collect()
209}
210
211fn is_module_alias(source: &str, at: usize) -> bool {
212 let statement = source[..at]
213 .rsplit_once([';', '\n'])
214 .map_or(&source[..at], |(_, tail)| tail);
215 statement
216 .split_whitespace()
217 .any(|word| matches!(word, "import" | "export"))
218}
219
220fn declaration_end(tokens: &[&Token], source: &str, start: usize) -> usize {
221 let mut braces = 0usize;
222 for (i, token) in tokens.iter().enumerate().skip(start) {
223 match token_text(source, token) {
224 "{" => braces += 1,
225 "}" if braces > 0 => {
226 braces -= 1;
227 if braces == 0 {
228 return i + 1;
229 }
230 }
231 ";" if braces == 0 => return i + 1,
232 _ => {}
233 }
234 }
235 tokens.len()
236}
237fn type_end(tokens: &[&Token], source: &str, start: usize) -> usize {
238 let mut depth = 0usize;
239 for (i, token) in tokens.iter().enumerate().skip(start) {
240 match token_text(source, token) {
241 "{" if depth == 0 && i > start => return i,
242 "<" | "[" | "{" => depth += 1,
243 ">" | "]" | "}" if depth > 0 => depth -= 1,
244 "," | ")" | "=" | ";" if depth == 0 => return i,
245 _ => {}
246 }
247 }
248 tokens.len()
249}
250fn looks_like_generic(tokens: &[&Token], source: &str, at: usize) -> bool {
251 at > 0
252 && matches!(
253 tokens[at - 1].kind,
254 TokenKind::Identifier | TokenKind::Keyword
255 )
256 && matching_angle(tokens, source, at).is_some_and(|end| {
257 tokens
258 .get(end + 1)
259 .is_some_and(|t| matches!(token_text(source, t), "(" | "."))
260 })
261}
262fn matching_angle(tokens: &[&Token], source: &str, at: usize) -> Option<usize> {
263 let mut depth = 0usize;
264 for (i, token) in tokens.iter().enumerate().skip(at) {
265 match token_text(source, token) {
266 "<" => depth += 1,
267 ">" => {
268 depth -= 1;
269 if depth == 0 {
270 return Some(i);
271 }
272 }
273 _ => {}
274 }
275 }
276 None
277}
278fn significant(tokens: &[Token]) -> Vec<&Token> {
279 tokens
280 .iter()
281 .filter(|t| !matches!(t.kind, TokenKind::Trivia | TokenKind::End))
282 .collect()
283}
284fn token_text<'a>(source: &'a str, token: &Token) -> &'a str {
285 &source[token.span.start..token.span.end]
286}
287fn gap(span: Span, construct: &str, reason: &str) -> EvaluationGap {
288 EvaluationGap {
289 span,
290 construct: construct.into(),
291 reason: reason.into(),
292 }
293}
294fn token_name(kind: &TokenKind) -> &'static str {
295 match kind {
296 TokenKind::Identifier => "identifier",
297 TokenKind::Keyword => "keyword",
298 TokenKind::Number => "number",
299 TokenKind::String => "string",
300 TokenKind::RegExp => "regexp",
301 TokenKind::Template => "template",
302 TokenKind::Punctuator => "punctuator",
303 TokenKind::Trivia => "trivia",
304 TokenKind::End => "end",
305 }
306}
307fn executable(kind: &TokenKind) -> bool {
308 matches!(
309 kind,
310 TokenKind::Identifier
311 | TokenKind::Number
312 | TokenKind::String
313 | TokenKind::RegExp
314 | TokenKind::Template
315 | TokenKind::Punctuator
316 )
317}
318
319pub fn decode_typescript(
321 cx: &mut ReadCx<'_>,
322 source: &str,
323 budget: &mut DecodeBudget,
324) -> Result<Expr> {
325 budget.check_input_bytes(cx.codec, source.len())?;
326 if source.starts_with("__sim_expr__(") {
327 return sim_codec_javascript::decode_javascript(cx, source, budget);
328 }
329 let tree = parse_module_with_limits(source, parser_limits(budget))
330 .map_err(|e| codec_error(e.to_string()))?;
331 budget.check_tokens(cx.codec, tree.tokens.len())?;
332 lower_typescript(&tree)
333 .map(|x| x.javascript)
334 .map_err(|e| codec_error(e.to_string()))
335}
336pub fn decode_typescript_located(
338 cx: &mut ReadCx<'_>,
339 source_id: impl Into<String>,
340 input: Input,
341) -> Result<LocatedExpr> {
342 let source = input_text(input)?;
343 let source_id = SourceId(source_id.into());
344 cx.cx.sources_mut().intern_text(source_id.clone(), &source);
345 let mut budget = DecodeBudget::new(cx.limits);
346 let expr = decode_typescript(cx, &source, &mut budget)?;
347 Ok(LocatedExpr {
348 expr,
349 origin: Some(origin(cx.codec, source_id, 0, source.len())),
350 })
351}
352pub fn decode_typescript_tree(
354 cx: &mut ReadCx<'_>,
355 source_id: impl Into<String>,
356 input: Input,
357) -> Result<LocatedExprTree> {
358 let located = decode_typescript_located(cx, source_id, input)?;
359 let mut tree = LocatedExprTree::from_expr_recursive(located.expr);
360 tree.origin = located.origin;
361 Ok(tree)
362}
363pub fn encode_typescript(expr: &Expr) -> Result<Output> {
365 if is_javascript_form(expr) {
366 let mut source = String::new();
367 encode_direct(expr, &mut source)?;
368 return Ok(Output::Text(source));
369 }
370 sim_codec_javascript::encode_javascript(expr)
371}
372fn is_javascript_form(expr: &Expr) -> bool {
373 matches!(expr, Expr::Call { operator, .. } if matches!(operator.as_ref(), Expr::Symbol(symbol) if symbol.namespace.as_deref().map(AsRef::as_ref) == Some("javascript")))
374}
375fn encode_direct(expr: &Expr, source: &mut String) -> Result<()> {
376 let Expr::Call { operator, args } = expr else {
377 return Err(codec_error("malformed JavaScript form in TypeScript graph"));
378 };
379 let Expr::Symbol(symbol) = operator.as_ref() else {
380 return Err(codec_error(
381 "malformed JavaScript operator in TypeScript graph",
382 ));
383 };
384 if symbol.namespace.as_deref().map(AsRef::as_ref) != Some("javascript") {
385 return Err(codec_error("non-JavaScript child in TypeScript graph"));
386 }
387 if symbol.name.as_ref() == "token" {
388 if let [Expr::Symbol(_), Expr::String(text), Expr::Bool(_)] = args.as_slice() {
389 source.push_str(text);
390 return Ok(());
391 }
392 return Err(codec_error(
393 "javascript/token expects kind, text, executable",
394 ));
395 }
396 for arg in args {
397 encode_direct(arg, source)?;
398 }
399 Ok(())
400}
401fn parser_limits(b: &DecodeBudget) -> Limits {
402 let l = b.limits();
403 Limits {
404 max_bytes: l.max_input_bytes,
405 max_nodes: l.max_tokens,
406 max_nesting: l.max_depth,
407 }
408}
409fn input_text(input: Input) -> Result<String> {
410 match input {
411 Input::Text(x) => Ok(x),
412 Input::Bytes(x) => String::from_utf8(x)
413 .map_err(|e| codec_error(format!("codec input is not valid UTF-8: {e}"))),
414 }
415}
416fn origin(codec: sim_kernel::CodecId, source: SourceId, start: usize, end: usize) -> KernelOrigin {
417 KernelOrigin {
418 codec,
419 source,
420 span: KernelSpan { start, end },
421 trivia: Vec::new(),
422 }
423}
424fn codec_error(message: impl Into<String>) -> Error {
425 Error::CodecError {
426 codec: TYPESCRIPT_CODEC_ID,
427 message: message.into(),
428 }
429}