Skip to main content

meta_language/translation/
javascript.rs

1//! JavaScript frontend for the portable core.
2//!
3//! It reads the subset of modern JavaScript the core can represent
4//! faithfully: Number and `BigInt` arithmetic, strings and booleans; top-level functions
5//! and object-literal namespaces of methods whose types come from `JSDoc`
6//! (`@param`, `@returns`, and `@typedef` unions of `{ $: 'tag', … }` object
7//! types for data types) or, where `JSDoc` is silent, are inferred from how the
8//! program uses them; bodies made of `const`, `if`, `return`, `throw` and
9//! `switch` statements; and a top level of `console.log`, `const` and
10//! `node:assert` statements, which are the program's effects. A function
11//! whose leading statements throw on a negative argument takes a natural
12//! number. Everything else is rejected with a precise obligation.
13//!
14//! Mirrors `js/src/translation/javascript.js`.
15
16use 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];
49/// `x op= e` is `x = x op e`; `+=` adds numbers or concatenates strings like `+`.
50const 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];
86/// The own properties of `Object.prototype`. The JavaScript frontend looks
87/// assertion methods up in a plain object, so these names inherit a truthy
88/// entry and read as `notDeepStrictEqual`.
89const 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/// What an `assert.method(…)` call asserts.
105#[derive(Clone, Copy, PartialEq, Eq)]
106enum AssertionKind {
107    Eq,
108    Ne,
109    Deep,
110    NotDeep,
111}
112
113/// Reads a JavaScript ES module into the surface AST.
114///
115/// # Errors
116///
117/// On syntax errors, type errors in `JSDoc` annotations and constructs outside
118/// the portable core.
119pub 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/// Locals in scope, the ones `let` or a parameter makes assignable, and
125/// names of the current block still in their temporal dead zone.
126#[derive(Clone, Default)]
127struct Scope {
128    locals: HashSet<String>,
129    tdz: HashSet<String>,
130    mutable: HashSet<String>,
131}
132
133/// Enclosing loops and switches, for `break` and `continue`.
134#[derive(Clone, Copy, Default)]
135struct Jumps {
136    loops: usize,
137    switches: usize,
138}
139
140/// The local name of the imported `node:assert` module.
141struct Assertion {
142    name: String,
143    strict: bool,
144}
145
146/// A `JSDoc` block's `@param` types, in order, and its `@returns` type.
147struct JsDoc {
148    params: Vec<(String, String)>,
149    returns: Option<String>,
150    range: Span,
151}
152
153/// A `JSDoc` tag: `@tag {type} name`.
154struct JsDocTag {
155    tag: String,
156    ty: Option<String>,
157    name: Option<String>,
158}
159
160/// Function-body statements before lowering to one expression.
161#[derive(Clone)]
162enum Stmt {
163    Const {
164        name: String,
165        value: SExpr,
166        span: Span,
167    },
168    /// `let name = value`, an assignable local.
169    Let {
170        name: String,
171        value: SExpr,
172        span: Span,
173    },
174    /// `name = value`, and `x op= e`, `x++` and `x--` read as one.
175    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 (init; cond; update) body`, whose `init` are `let` declarations or assignments.
191    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    /// A braced block; `inline` for the bindings of a destructuring `const`.
213    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    /// `console.log(expr)`.
231    Print {
232        expr: SExpr,
233        style: ShowStyle,
234        span: Span,
235    },
236}
237
238#[derive(Clone)]
239struct Switch {
240    /// The switch subject: the discriminant, or the object of `x.$`.
241    scrutinee: SExpr,
242    /// The subject's name and data type in a tag switch.
243    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/// Where a block-declaration scan stops: the end of the file or the block's `}`.
276#[derive(Clone, Copy)]
277enum ScanEnd {
278    File,
279    Brace,
280}
281
282struct JavaScriptParser {
283    source: Source,
284    cursor: TokenCursor,
285    docs: Vec<Comment>,
286    /// Tags of every @typedef data type, for `switch (x.$)`.
287    data_types: Vec<SData>,
288    scope: Scope,
289    assertion: Option<Assertion>,
290    /// Async functions run sequentially: each call of one is awaited where it
291    /// is made, so nothing else runs until its result is back. `await` is
292    /// allowed at the top level of the module and in async functions.
293    async_names: HashSet<String>,
294    in_async: bool,
295    unawaited: Vec<(String, Span)>,
296    sequential_async: bool,
297    jumps: Jumps,
298    /// Whether a top-level statement is being read, where `return` is a syntax error.
299    top_level: bool,
300    /// Functions and data types that loops and joins of statements lower to, and their count.
301    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    /// The span from `from` to the next token.
338    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
354/// `JSDoc` tags of a comment: `@tag {type} name`. Leading `*` of each line is
355/// decoration.
356fn 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
420/// The next `@tag` at or after `from`: the positions of the `@` and after the letters.
421fn 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
434/// `^[ \t]*([A-Za-z_$][\w$]*)`.
435fn 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
452/// `/^[A-Za-z_$][\w$]*$/`.
453fn 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
461/// `String.prototype.trim`: JavaScript whitespace and line terminators.
462fn 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
466/// A `JSDoc` type, where an empty one counts as missing.
467fn non_empty(text: Option<String>) -> Option<String> {
468    text.filter(|text| !text.is_empty())
469}
470
471/// `5n` and `0x1fn` are `BigInt` literals, which the lexer keeps as decimal
472/// digits; `42`, `1.5` and `1e21` are Numbers, IEEE-754 doubles, which the
473/// lexer keeps as `String(value)`.
474fn 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}