Skip to main content

submilli_engine/codegen/
init_guard.rs

1//! Module variables read or written before their declaration has run.
2//!
3//! A function can run during module initialization, before a later `let`,
4//! `const` or static field has been initialized. JavaScript throws a
5//! `ReferenceError` there. A Wasm global already holds a zero or null default,
6//! so a guarded global gets an `i32` flag that the start function sets once its
7//! binding is initialized; every other access checks the flag first.
8//!
9//! Each guarded global is initialized once its declaration stores its value.
10//! A static field is guarded field by field, so a function that runs during
11//! its class's static initialization reads an earlier static and throws on a
12//! later one. JavaScript would find `undefined` there, which a typed Wasm slot
13//! can't hold, and its default would be a wrong value or a null reference. The
14//! message names the class, as Node's does for a class not yet initialized.
15//!
16//! A class is guarded the same way: constructing it, calling one of its
17//! static methods or testing `instanceof` against it before its declaration
18//! has run throws. Its flag is set where the declaration stands, before its
19//! static fields initialize, so they can use the class.
20//!
21//! Only a binding declared after some top-level code that could call a
22//! function is guarded. Before that point nothing can reach a function body,
23//! so the binding is always initialized by the time any function uses it.
24
25use std::collections::BTreeSet;
26
27use wasm_encoder::{BlockType, ConstExpr, GlobalSection, GlobalType, Instruction, ValType};
28
29use crate::codegen::CodegenCtx;
30use crate::codegen::function_emitter::FunctionEmitter;
31use crate::codegen::symbol_table::SymbolTable;
32use crate::compiler_error::CompilerFailure;
33use crate::{MangledName, StmtId, TypedAst, TypedExprKind, TypedStmtKind};
34
35/// One guarded global and the top-level statement that declares it.
36#[derive(Clone, Debug, PartialEq)]
37pub struct InitGuard {
38    pub global: MangledName,
39    /// The name the `ReferenceError` reports: the variable, or a static
40    /// field's class.
41    pub binding: String,
42    pub declared_by: StmtId,
43}
44
45/// A class that can be used before its declaration has run.
46#[derive(Clone, Debug, PartialEq)]
47pub struct ClassGuard {
48    pub name: String,
49    /// What a use of the class goes through: the class itself (for
50    /// `instanceof`), its constructor and its static methods.
51    pub uses: Vec<MangledName>,
52    /// The index into the top-level statements before which the class is
53    /// declared.
54    pub declared_at: usize,
55}
56
57/// A guarded binding's flag, as codegen allocated it.
58#[derive(Clone, Debug, PartialEq)]
59pub struct InitFlag {
60    pub flag_idx: u32,
61    /// The statement that initializes a global; `None` for a class, which no
62    /// statement writes.
63    pub declared_by: Option<StmtId>,
64    pub message: String,
65}
66
67/// The module globals that need an initialization flag, in declaration order.
68pub fn guarded_globals(ta: &TypedAst) -> Result<Vec<InitGuard>, CompilerFailure> {
69    let declared: BTreeSet<&MangledName> = ta.globals.iter().map(|g| &g.mangled_name).collect();
70    let mut seen = BTreeSet::new();
71    let mut code_may_have_run = false;
72    let mut guards = Vec::new();
73    for &stmt_id in &ta.top_level_statements {
74        let stmt = ta
75            .try_stmt(stmt_id)
76            .map_err(crate::codegen::arena_failure)?;
77        // The initializer runs before its own binding is initialized.
78        code_may_have_run |= may_run_code(ta, stmt_id)?;
79        let TypedStmtKind::AssignGlobal { ident, mangled, .. } = &stmt.kind else {
80            continue;
81        };
82        // `seen` records every first declaration, guarded or not, so a later
83        // write is never taken for one.
84        let is_first_declaration = declared.contains(mangled) && seen.insert(mangled);
85        if !is_first_declaration || !code_may_have_run {
86            continue;
87        }
88        guards.push(InitGuard {
89            global: mangled.clone(),
90            binding: reported_binding(&ident.name).to_string(),
91            declared_by: stmt_id,
92        });
93    }
94    Ok(guards)
95}
96
97/// The top-level classes that need an initialization flag.
98pub fn guarded_classes(ta: &TypedAst) -> Result<Vec<ClassGuard>, CompilerFailure> {
99    let mut code_may_have_run = Vec::with_capacity(ta.top_level_statements.len() + 1);
100    code_may_have_run.push(false);
101    for &stmt_id in &ta.top_level_statements {
102        let before = code_may_have_run.last().copied().unwrap_or(false);
103        code_may_have_run.push(before || may_run_code(ta, stmt_id)?);
104    }
105    let mut guards = Vec::new();
106    for decl in &ta.types {
107        let crate::TypedTypeDecl::Class(class) = decl else {
108            continue;
109        };
110        let Some(&declared_at) = ta.class_declaration_points.get(&class.mangled_name) else {
111            continue;
112        };
113        if !code_may_have_run.get(declared_at).copied().unwrap_or(false) {
114            continue;
115        }
116        let mangled = &class.mangled_name;
117        let mut uses = vec![
118            mangled.clone(),
119            crate::mangle::extend(mangled, "constructor"),
120        ];
121        uses.extend(static_method_names(class));
122        guards.push(ClassGuard {
123            name: class.name.name.clone(),
124            uses,
125            declared_at,
126        });
127    }
128    Ok(guards)
129}
130
131/// Whether running a top-level statement can call a function.
132fn may_run_code(ta: &TypedAst, stmt_id: StmtId) -> Result<bool, CompilerFailure> {
133    let stmt = ta
134        .try_stmt(stmt_id)
135        .map_err(crate::codegen::arena_failure)?;
136    match &stmt.kind {
137        TypedStmtKind::AssignGlobal { value, .. } => Ok(!is_inert(ta, *value)?),
138        _ => Ok(true),
139    }
140}
141
142/// Static fields are lowered to globals named `Class.field`; the binding a
143/// read of one needs is its class.
144fn reported_binding(global_name: &str) -> &str {
145    global_name
146        .split_once('.')
147        .map_or(global_name, |(class, _)| class)
148}
149
150/// The `ReferenceError` message for a binding, worded as Node words it.
151pub fn before_initialization_message(binding: &str) -> String {
152    format!("Cannot access '{binding}' before initialization")
153}
154
155/// Allocates one flag per guarded global and class, starting unset. Returns
156/// how many globals were added.
157pub fn allocate_flags(
158    ta: &TypedAst,
159    globals: &mut GlobalSection,
160    symbols: &mut SymbolTable,
161    next_global_idx: &mut u32,
162) -> Result<u32, CompilerFailure> {
163    let guards = guarded_globals(ta)?;
164    for guard in &guards {
165        let flag_idx = allocate_flag(globals, next_global_idx)?;
166        symbols.record_init_guard(
167            guard.global.clone(),
168            InitFlag {
169                flag_idx,
170                declared_by: Some(guard.declared_by),
171                message: before_initialization_message(&guard.binding),
172            },
173        );
174    }
175    let classes = guarded_classes(ta)?;
176    for class in &classes {
177        let flag_idx = allocate_flag(globals, next_global_idx)?;
178        for used in &class.uses {
179            symbols.record_init_guard(
180                used.clone(),
181                InitFlag {
182                    flag_idx,
183                    declared_by: None,
184                    message: before_initialization_message(&class.name),
185                },
186            );
187        }
188        symbols.record_class_declaration_flag(class.declared_at, flag_idx);
189    }
190    crate::codegen::wasm_u32(guards.len() + classes.len())
191}
192
193fn allocate_flag(
194    globals: &mut GlobalSection,
195    next_global_idx: &mut u32,
196) -> Result<u32, CompilerFailure> {
197    globals.global(
198        GlobalType {
199            val_type: ValType::I32,
200            mutable: true,
201            shared: false,
202        },
203        &ConstExpr::i32_const(0),
204    );
205    let flag_idx = *next_global_idx;
206    crate::codegen::next_index(next_global_idx)?;
207    Ok(flag_idx)
208}
209
210/// Before an access to a guarded global, throw unless its binding is
211/// initialized.
212pub(crate) fn emit_check(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, global: &MangledName) {
213    let Some(flag) = ctx.symbols.init_guard(global) else {
214        return;
215    };
216    emitter.instruction(Instruction::GlobalGet(flag.flag_idx));
217    emitter.instruction(Instruction::I32Eqz);
218    emitter.emit_if(BlockType::Empty);
219    crate::codegen::throw::emit_reference_error_throw(emitter, ctx, &flag.message);
220    emitter.emit_end();
221}
222
223/// Whether `stmt`, a write to `global`, is its declaration, which marks it
224/// initialized; any other write is checked.
225pub(crate) fn is_declaration(ctx: &CodegenCtx, global: &MangledName, stmt: StmtId) -> bool {
226    ctx.symbols
227        .init_guard(global)
228        .is_some_and(|flag| flag.declared_by == Some(stmt))
229}
230
231/// Mark the classes declared before top-level statement `index` initialized.
232pub(crate) fn emit_mark_classes_declared(
233    emitter: &mut FunctionEmitter,
234    ctx: &CodegenCtx,
235    index: usize,
236) {
237    for &flag_idx in ctx.symbols.class_declaration_flags(index) {
238        emitter.instruction(Instruction::I32Const(1));
239        emitter.instruction(Instruction::GlobalSet(flag_idx));
240    }
241}
242
243pub(crate) fn emit_mark_initialized(
244    emitter: &mut FunctionEmitter,
245    ctx: &CodegenCtx,
246    global: &MangledName,
247) {
248    let Some(flag) = ctx.symbols.init_guard(global) else {
249        return;
250    };
251    emitter.instruction(Instruction::I32Const(1));
252    emitter.instruction(Instruction::GlobalSet(flag.flag_idx));
253}
254
255/// Whether evaluating `expr` can never run a function body or read a class
256/// binding that may still be uninitialized.
257fn is_inert(ta: &TypedAst, expr: crate::ExprId) -> Result<bool, CompilerFailure> {
258    let expr = ta.try_expr(expr).map_err(crate::codegen::arena_failure)?;
259    Ok(match &expr.kind {
260        TypedExprKind::Number(_)
261        | TypedExprKind::BigInt(_)
262        | TypedExprKind::String(_)
263        | TypedExprKind::Boolean(_)
264        | TypedExprKind::Null => true,
265        // Reading a static method reads its class binding.
266        TypedExprKind::FunctionRef { mangled, .. } => !is_class_static_method(ta, mangled),
267        // An operator on an object can call its `toString` or `valueOf`.
268        TypedExprKind::Unary { operand, .. } => is_primitive_literal(ta, *operand)?,
269        TypedExprKind::Binary { lhs, rhs, .. } => {
270            is_primitive_literal(ta, *lhs)? && is_primitive_literal(ta, *rhs)?
271        }
272        TypedExprKind::TupleLiteral { elements, .. } => all_inert(ta, elements.iter().copied())?,
273        TypedExprKind::ArrayLiteral { elements, .. } => {
274            let mut values = Vec::with_capacity(elements.len());
275            for element in elements {
276                let crate::TypedArrayElement::Value(id) = element else {
277                    return Ok(false);
278                };
279                values.push(*id);
280            }
281            all_inert(ta, values.into_iter())?
282        }
283        _ => false,
284    })
285}
286
287fn is_class_static_method(ta: &TypedAst, mangled: &MangledName) -> bool {
288    ta.types.iter().any(|decl| {
289        let crate::TypedTypeDecl::Class(class) = decl else {
290            return false;
291        };
292        static_method_names(class).any(|method| method == *mangled)
293    })
294}
295
296fn static_method_names(class: &crate::TypedClassDecl) -> impl Iterator<Item = MangledName> + '_ {
297    class
298        .static_methods
299        .keys()
300        .map(|method| crate::mangle::static_member(&class.mangled_name, method))
301}
302
303fn is_primitive_literal(ta: &TypedAst, expr: crate::ExprId) -> Result<bool, CompilerFailure> {
304    let expr = ta.try_expr(expr).map_err(crate::codegen::arena_failure)?;
305    Ok(matches!(
306        expr.kind,
307        TypedExprKind::Number(_)
308            | TypedExprKind::BigInt(_)
309            | TypedExprKind::String(_)
310            | TypedExprKind::Boolean(_)
311    ))
312}
313
314fn all_inert(
315    ta: &TypedAst,
316    mut exprs: impl Iterator<Item = crate::ExprId>,
317) -> Result<bool, CompilerFailure> {
318    exprs.try_fold(true, |inert, id| Ok(inert && is_inert(ta, id)?))
319}