1use std::collections::HashSet;
17
18use super::diagnostics::{Result, TranslationError, type_error, unsupported};
19use super::lexer::{
20 Comment, Source, Token, TokenCursor, TokenKind, describe, is_js_space, tokenize,
21};
22use super::surface::{
23 BinaryOp, Guard, SArrayItem, SComparison, SCtor, SData, SEffect, SExpr, SField, SFn, SItem,
24 SMain, SModule, SNode, SParam, SPattern, SPatternNode, SProgram, SProp, SPropNode, SRow,
25 STagTest, ShowStyle, UnaryOp,
26};
27use super::types::{BOOL, FLOAT, INT, NAT, STRING, Type, array};
28use super::{Language, Span};
29
30mod console;
31mod declarations;
32mod expressions;
33mod flow;
34mod imperative;
35mod infer;
36mod loops;
37mod lowering;
38mod math;
39mod statements;
40use self::flow::{imperative, statement_uses};
41use self::imperative::lower_imperative;
42use self::lowering::{assertion_kind, guarded_parameter, lower, prop_of};
43
44const ROOT: &str = "crate";
45const ASSIGNMENTS: [&str; 16] = [
46 "=", "+=", "-=", "*=", "/=", "%=", "**=", "<<=", ">>=", ">>>=", "&=", "|=", "^=", "&&=", "||=",
47 "??=",
48];
49const COMPOUND: [(&str, BinaryOp); 7] = [
51 ("+=", BinaryOp::Plus),
52 ("-=", BinaryOp::Sub),
53 ("*=", BinaryOp::Mul),
54 ("/=", BinaryOp::Div),
55 ("%=", BinaryOp::Rem),
56 ("&&=", BinaryOp::And),
57 ("||=", BinaryOp::Or),
58];
59const ERRORS: [&str; 3] = ["Error", "RangeError", "TypeError"];
60const GLOBALS: [&str; 24] = [
61 "Math",
62 "Number",
63 "BigInt",
64 "parseInt",
65 "parseFloat",
66 "JSON",
67 "Array",
68 "Object",
69 "Symbol",
70 "Date",
71 "Promise",
72 "Reflect",
73 "Proxy",
74 "Map",
75 "Set",
76 "WeakMap",
77 "WeakSet",
78 "globalThis",
79 "process",
80 "require",
81 "console",
82 "setTimeout",
83 "isNaN",
84 "isFinite",
85];
86const OBJECT_PROTOTYPE: [&str; 12] = [
90 "constructor",
91 "__defineGetter__",
92 "__defineSetter__",
93 "hasOwnProperty",
94 "__lookupGetter__",
95 "__lookupSetter__",
96 "isPrototypeOf",
97 "propertyIsEnumerable",
98 "toString",
99 "valueOf",
100 "__proto__",
101 "toLocaleString",
102];
103
104#[derive(Clone, Copy, PartialEq, Eq)]
106enum AssertionKind {
107 Eq,
108 Ne,
109 Deep,
110 NotDeep,
111}
112
113pub fn parse_javascript(source: &str) -> Result<SProgram> {
120 let tokens = tokenize(source, Language::JavaScript)?;
121 JavaScriptParser::new(source, tokens.tokens, &tokens.comments).file()
122}
123
124#[derive(Clone, Default)]
127struct Scope {
128 locals: HashSet<String>,
129 tdz: HashSet<String>,
130 mutable: HashSet<String>,
131}
132
133#[derive(Clone, Copy, Default)]
135struct Jumps {
136 loops: usize,
137 switches: usize,
138}
139
140struct Assertion {
142 name: String,
143 strict: bool,
144}
145
146struct JsDoc {
148 params: Vec<(String, String)>,
149 returns: Option<String>,
150 range: Span,
151}
152
153struct JsDocTag {
155 tag: String,
156 ty: Option<String>,
157 name: Option<String>,
158}
159
160#[derive(Clone)]
162enum Stmt {
163 Const {
164 name: String,
165 value: SExpr,
166 span: Span,
167 },
168 Let {
170 name: String,
171 value: SExpr,
172 span: Span,
173 },
174 Assign {
176 name: String,
177 value: SExpr,
178 span: Span,
179 },
180 While {
181 cond: SExpr,
182 body: Vec<Self>,
183 span: Span,
184 },
185 DoWhile {
186 body: Vec<Self>,
187 cond: SExpr,
188 span: Span,
189 },
190 For {
192 init: Vec<Self>,
193 cond: Option<SExpr>,
194 update: Vec<Self>,
195 body: Vec<Self>,
196 span: Span,
197 },
198 Break {
199 span: Span,
200 },
201 Continue {
202 span: Span,
203 },
204 Return {
205 expr: SExpr,
206 span: Span,
207 },
208 Throw {
209 message: String,
210 span: Span,
211 },
212 Block {
214 body: Vec<Self>,
215 inline: bool,
216 span: Span,
217 },
218 Empty,
219 If {
220 cond: SExpr,
221 then: Vec<Self>,
222 otherwise: Option<Vec<Self>>,
223 span: Span,
224 },
225 Switch(Switch),
226 Expr {
227 expr: SExpr,
228 span: Span,
229 },
230 Print {
232 expr: SExpr,
233 style: ShowStyle,
234 span: Span,
235 },
236}
237
238#[derive(Clone)]
239struct Switch {
240 scrutinee: SExpr,
242 tag: Option<(String, SData)>,
244 clauses: Vec<Clause>,
245 span: Span,
246}
247
248#[derive(Clone)]
249struct Clause {
250 tests: Vec<CaseTest>,
251 body: Vec<Stmt>,
252 span: Span,
253}
254
255#[derive(Clone)]
256enum CaseTest {
257 Default {
258 span: Span,
259 },
260 Tag {
261 tag: String,
262 span: Span,
263 },
264 NumLit {
265 value: String,
266 negative: bool,
267 span: Span,
268 },
269 BoolLit {
270 value: bool,
271 span: Span,
272 },
273}
274
275#[derive(Clone, Copy)]
277enum ScanEnd {
278 File,
279 Brace,
280}
281
282struct JavaScriptParser {
283 source: Source,
284 cursor: TokenCursor,
285 docs: Vec<Comment>,
286 data_types: Vec<SData>,
288 scope: Scope,
289 assertion: Option<Assertion>,
290 async_names: HashSet<String>,
294 in_async: bool,
295 unawaited: Vec<(String, Span)>,
296 sequential_async: bool,
297 jumps: Jumps,
298 top_level: bool,
300 generated: Vec<SItem>,
302 generated_count: usize,
303}
304
305impl JavaScriptParser {
306 fn new(source: &str, tokens: Vec<Token>, comments: &[Comment]) -> Self {
307 Self {
308 source: Source::new(source),
309 cursor: TokenCursor::new(tokens, Language::JavaScript),
310 docs: comments
311 .iter()
312 .filter(|comment| comment.text.starts_with("/**") && comment.text != "/**/")
313 .cloned()
314 .collect(),
315 data_types: Vec::new(),
316 scope: Scope::default(),
317 assertion: None,
318 async_names: HashSet::new(),
319 in_async: true,
320 unawaited: Vec::new(),
321 sequential_async: false,
322 jumps: Jumps::default(),
323 top_level: false,
324 generated: Vec::new(),
325 generated_count: 0,
326 }
327 }
328
329 fn peek(&self) -> Token {
330 self.cursor.peek().clone()
331 }
332
333 fn peek_at(&self, offset: usize) -> Token {
334 self.cursor.peek_at(offset).clone()
335 }
336
337 fn to_here(&self, from: &Token) -> Span {
339 span(from, self.cursor.peek())
340 }
341
342 fn fail(message: &str, token: &Token) -> TranslationError {
343 TranslationError::syntax(
344 format!("{message} but found {}", describe(token)),
345 Some(token.span()),
346 )
347 }
348
349 fn fail_here(&self, message: &str) -> TranslationError {
350 Self::fail(message, self.cursor.peek())
351 }
352}
353
354fn jsdoc_tags(comment: &Comment) -> Result<Vec<JsDocTag>> {
357 let units: Vec<u16> = comment.text.encode_utf16().collect();
358 let inner = String::from_utf16_lossy(
359 &units[3.min(units.len())..units.len().saturating_sub(2).max(3.min(units.len()))],
360 );
361 let text: Vec<char> = inner
362 .split('\n')
363 .map(|line| {
364 let line = line
365 .trim_start_matches(|ch: char| u16::try_from(u32::from(ch)).is_ok_and(is_js_space));
366 line.strip_prefix('*').unwrap_or(line)
367 })
368 .collect::<Vec<_>>()
369 .join("\n")
370 .chars()
371 .collect();
372 let mut tags = Vec::new();
373 let mut at = 0;
374 while let Some((tag_start, tag_end)) = next_tag(&text, at) {
375 let tag: String = text[tag_start + 1..tag_end].iter().collect();
376 let mut index = tag_end;
377 while matches!(text.get(index), Some(' ' | '\t')) {
378 index += 1;
379 }
380 let mut ty = None;
381 if text.get(index) == Some(&'{') {
382 let mut depth = 0;
383 let mut end = index;
384 while end < text.len() {
385 if text[end] == '{' {
386 depth += 1;
387 }
388 if text[end] == '}' {
389 depth -= 1;
390 if depth == 0 {
391 break;
392 }
393 }
394 end += 1;
395 }
396 if depth != 0 {
397 return Err(TranslationError::syntax(
398 format!("unbalanced braces in the JSDoc type of @{tag}"),
399 Some(Span::new(comment.start, comment.end)),
400 ));
401 }
402 ty = Some(
403 text[index + 1..end]
404 .iter()
405 .map(|&ch| if ch == '\n' { ' ' } else { ch })
406 .collect(),
407 );
408 index = end + 1;
409 }
410 tags.push(JsDocTag {
411 tag,
412 ty,
413 name: jsdoc_name(&text[index.min(text.len())..]),
414 });
415 at = index;
416 }
417 Ok(tags)
418}
419
420fn next_tag(text: &[char], from: usize) -> Option<(usize, usize)> {
422 (from..text.len()).find_map(|start| {
423 if text[start] != '@' {
424 return None;
425 }
426 let letters = text[start + 1..]
427 .iter()
428 .take_while(|ch| ch.is_ascii_alphabetic())
429 .count();
430 (letters > 0).then_some((start, start + 1 + letters))
431 })
432}
433
434fn jsdoc_name(text: &[char]) -> Option<String> {
436 let start = text
437 .iter()
438 .take_while(|&&ch| ch == ' ' || ch == '\t')
439 .count();
440 let first = *text.get(start)?;
441 if !(first.is_ascii_alphabetic() || first == '_' || first == '$') {
442 return None;
443 }
444 Some(
445 text[start..]
446 .iter()
447 .take_while(|&&ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
448 .collect(),
449 )
450}
451
452fn is_identifier_name(name: &str) -> bool {
454 let mut chars = name.chars();
455 chars
456 .next()
457 .is_some_and(|first| first.is_ascii_alphabetic() || first == '_' || first == '$')
458 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
459}
460
461fn js_trim(text: &str) -> &str {
463 text.trim_matches(|ch: char| u16::try_from(u32::from(ch)).is_ok_and(is_js_space))
464}
465
466fn non_empty(text: Option<String>) -> Option<String> {
468 text.filter(|text| !text.is_empty())
469}
470
471fn number_literal(token: &Token) -> SExpr {
475 node(
476 SNode::Num {
477 value: token.value.clone(),
478 ty: (token.suffix != "n").then_some(FLOAT),
479 negative: false,
480 unit: false,
481 },
482 span(token, token),
483 )
484}
485
486fn has_ctor(data: &SData, tag: &str) -> bool {
487 data.ctors.iter().any(|ctor| ctor.name == tag)
488}
489
490fn is_tag_field(expr: &SExpr) -> bool {
491 matches!(&expr.node, SNode::Field { field, .. } if field == "$")
492}
493
494const fn node(node: SNode, place: Span) -> SExpr {
495 SExpr::new(node, Some(place))
496}
497
498const fn wild(place: Span) -> SPattern {
499 SPattern {
500 node: SPatternNode::Wild,
501 span: Some(place),
502 }
503}
504
505fn binary(op: BinaryOp, left: SExpr, right: SExpr, token: &Token) -> SExpr {
506 let place = joined(&left, &right, token);
507 node(
508 SNode::Binary {
509 op,
510 left: Box::new(left),
511 right: Box::new(right),
512 rounding: None,
513 },
514 place,
515 )
516}
517
518fn joined(left: &SExpr, right: &SExpr, token: &Token) -> Span {
519 Span::new(
520 left.span.map_or(token.start, |place| place.start),
521 right.span.map_or(token.end, |place| place.end),
522 )
523}
524
525fn span(from: &Token, to: &Token) -> Span {
526 let end = if to.kind == TokenKind::Eof {
527 from.end
528 } else {
529 to.start
530 };
531 Span::new(from.start, from.end.max(end))
532}