From 4a7dc45b3f008018db791750b109e2634f36f598 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 26 Apr 2026 14:53:34 +0200 Subject: [PATCH] LibWeb+LibJS: Compile fetched top-level JS off-thread Split Rust program compilation so code generation and assembly finish before the main thread materializes GC-backed executable objects. The new CompiledProgram handle owns the parsed program, generator state, and bytecode until C++ consumes it on the main thread. Wire WebContent script fetching through that handle for classic scripts and modules. Syntax-error paths still return ParsedProgram, so existing error reporting stays in place. Successful fetches now do top-level codegen on the thread pool before deferred_invoke hands control back to the main thread. Executable creation, SharedFunctionInstanceData materialization, module metadata extraction, and declaration data extraction still run on the main thread where VM and GC access is valid. --- Libraries/LibJS/Rust/src/lib.rs | 341 ++++++++++++++---- Libraries/LibJS/RustIntegration.cpp | 76 ++++ Libraries/LibJS/RustIntegration.h | 15 + Libraries/LibJS/Script.cpp | 11 + Libraries/LibJS/Script.h | 2 + Libraries/LibJS/SourceTextModule.cpp | 23 ++ Libraries/LibJS/SourceTextModule.h | 2 + .../LibWeb/HTML/Scripting/ClassicScript.cpp | 33 ++ .../LibWeb/HTML/Scripting/ClassicScript.h | 1 + Libraries/LibWeb/HTML/Scripting/Fetching.cpp | 56 +-- .../LibWeb/HTML/Scripting/ModuleScript.cpp | 21 ++ .../LibWeb/HTML/Scripting/ModuleScript.h | 2 + 12 files changed, 496 insertions(+), 87 deletions(-) diff --git a/Libraries/LibJS/Rust/src/lib.rs b/Libraries/LibJS/Rust/src/lib.rs index c252d4f2ae..ca04d8c063 100644 --- a/Libraries/LibJS/Rust/src/lib.rs +++ b/Libraries/LibJS/Rust/src/lib.rs @@ -121,6 +121,26 @@ pub struct ParsedProgram { // SAFETY: Full ownership transfer between threads, never concurrent access. unsafe impl Send for ParsedProgram {} +pub struct CompiledProgram { + parsed: ParsedProgram, + bytecode: CompiledProgramBytecode, +} + +enum CompiledProgramBytecode { + Program(CompiledBytecode), + AsyncModule(CompiledBytecode), +} + +struct CompiledBytecode { + generator: bytecode::generator::Generator, + assembled: bytecode::generator::AssembledBytecode, +} + +// SAFETY: This handle owns its parser and bytecode-generator state, and C++ treats it as a move-only handoff object. +// The Rc/RefCell values inside are only ever touched by one thread at a time: the worker creates the handle, then the +// main thread consumes or frees it after the event-loop hop. +unsafe impl Send for CompiledProgram {} + // ============================================================================= // Internal helpers // ============================================================================= @@ -259,9 +279,49 @@ fn new_program_generator( generator } +/// Shared codegen pipeline: local variable setup → bytecode generation → assembly. +/// +/// This deliberately stops before `create_executable()`, because executable materialization creates GC-managed objects +/// and resolves VM-specific constants. Keeping that work separate lets WebContent perform the expensive AST-to-bytecode +/// pass on a worker thread while preserving all main-thread ownership rules for VM and heap data. +fn compile_program_body_to_bytecode( + generator: &mut bytecode::generator::Generator, + program: &ast::Statement, + scope_ref: &Rc>, +) -> bytecode::generator::AssembledBytecode { + generator.local_variables = convert_local_variables(&scope_ref.borrow()); + + let entry_block = generator.make_block(); + generator.switch_to_basic_block(entry_block); + generator.capture_saved_lexical_environment(); + + let result = bytecode::codegen::generate_statement(program, generator, None); + + if !generator.is_current_block_terminated() + && let Some(value) = result + { + generator.emit(bytecode::instruction::Instruction::End { value: value.operand() }); + } + // If result is None, the assembler will add End(undefined) as a fallthrough for unterminated blocks, matching C++. + + generator.assemble() +} + +unsafe fn create_executable_from_compiled_bytecode( + bytecode: &mut CompiledBytecode, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, +) -> *mut c_void { + unsafe { + bytecode.generator.vm_ptr = vm_ptr; + bytecode.generator.source_code_ptr = source_code_ptr; + bytecode::ffi::create_executable(&mut bytecode.generator, &bytecode.assembled, vm_ptr, source_code_ptr) + } +} + /// Shared compilation pipeline: local variable setup → codegen → assemble → create Executable. /// -/// Called by all three program-level entry points after parsing and scope analysis. +/// Called by program-level entry points that compile synchronously on the main thread. unsafe fn compile_program_body( generator: &mut bytecode::generator::Generator, program: &ast::Statement, @@ -269,26 +329,8 @@ unsafe fn compile_program_body( vm_ptr: *mut c_void, source_code_ptr: *const c_void, ) -> *mut c_void { - unsafe { - generator.local_variables = convert_local_variables(&scope_ref.borrow()); - - let entry_block = generator.make_block(); - generator.switch_to_basic_block(entry_block); - generator.capture_saved_lexical_environment(); - - let result = bytecode::codegen::generate_statement(program, generator, None); - - if !generator.is_current_block_terminated() - && let Some(value) = result - { - generator.emit(bytecode::instruction::Instruction::End { value: value.operand() }); - } - // If result is None, the assembler will add End(undefined) as a - // fallthrough for unterminated blocks, matching C++ compile(). - - let assembled = generator.assemble(); - bytecode::ffi::create_executable(generator, &assembled, vm_ptr, source_code_ptr) - } + let assembled = compile_program_body_to_bytecode(generator, program, scope_ref); + unsafe { bytecode::ffi::create_executable(generator, &assembled, vm_ptr, source_code_ptr) } } // ============================================================================= @@ -490,6 +532,60 @@ pub unsafe extern "C" fn rust_free_parsed_program(parsed: *mut ParsedProgram) { } } +/// Compile a parsed program to an off-thread bytecode artifact. +/// +/// Consumes and frees the ParsedProgram. The returned CompiledProgram still needs to be materialized on the main thread +/// before it becomes a GC-backed Executable. +/// +/// # Safety +/// - `parsed` must be a valid pointer from `rust_parse_program()` with no errors. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_compile_parsed_program_off_thread( + parsed: *mut ParsedProgram, + source_len: usize, +) -> *mut CompiledProgram { + unsafe { + abort_on_panic(|| { + if parsed.is_null() { + return std::ptr::null_mut(); + } + + let mut parsed = Box::from_raw(parsed); + let bytecode = if parsed.has_top_level_await { + let mut generator = new_module_async_generator(source_len, std::mem::take(&mut parsed.function_table)); + let assembled = compile_module_as_async_to_bytecode(&parsed.program, &parsed.scope_ref, &mut generator); + CompiledProgramBytecode::AsyncModule(CompiledBytecode { generator, assembled }) + } else { + let mut generator = new_program_generator( + parsed.is_strict_mode, + std::ptr::null_mut(), + std::ptr::null(), + source_len, + ); + generator.function_table = std::mem::take(&mut parsed.function_table); + let assembled = compile_program_body_to_bytecode(&mut generator, &parsed.program, &parsed.scope_ref); + CompiledProgramBytecode::Program(CompiledBytecode { generator, assembled }) + }; + + Box::into_raw(Box::new(CompiledProgram { + parsed: *parsed, + bytecode, + })) + }) + } +} + +/// Free a CompiledProgram without materializing it. +/// +/// # Safety +/// `compiled` must be a valid pointer from `rust_compile_parsed_program_off_thread()`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_free_compiled_program(compiled: *mut CompiledProgram) { + unsafe { + drop(Box::from_raw(compiled)); + } +} + /// Get the AST dump string from a ParsedProgram. /// /// Generates the dump on first call and caches it. Writes the pointer @@ -565,6 +661,50 @@ pub unsafe extern "C" fn rust_compile_parsed_script( } } +/// Materialize an off-thread-compiled script. Consumes and frees the CompiledProgram. +/// +/// # Safety +/// - `compiled` must be a valid pointer from `rust_compile_parsed_program_off_thread()`. +/// - `vm_ptr` must be a valid `JS::VM*`. +/// - `source_code_ptr` must be a valid `JS::SourceCode const*`. +/// - `gdi_context` must be a valid pointer to a C++ ScriptGdiBuilder. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_materialize_compiled_script( + compiled: *mut CompiledProgram, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + gdi_context: *mut c_void, +) -> *mut c_void { + unsafe { + abort_on_panic(|| { + if compiled.is_null() { + return std::ptr::null_mut(); + } + + let mut compiled = Box::from_raw(compiled); + let CompiledProgramBytecode::Program(ref mut bytecode) = compiled.bytecode else { + return std::ptr::null_mut(); + }; + + let exec_ptr = create_executable_from_compiled_bytecode(bytecode, vm_ptr, source_code_ptr); + if exec_ptr.is_null() { + return std::ptr::null_mut(); + } + + extract_script_gdi( + &compiled.parsed.scope_ref.borrow(), + compiled.parsed.is_strict_mode, + vm_ptr, + source_code_ptr, + gdi_context, + &mut bytecode.generator.function_table, + ); + + exec_ptr + }) + } +} + /// Compile an eval script and extract EDI (EvalDeclarationInstantiation) metadata. /// /// This is the path for eval(). It: @@ -1012,6 +1152,67 @@ pub unsafe extern "C" fn rust_compile_parsed_module( } } +/// Materialize an off-thread-compiled module. Consumes and frees the CompiledProgram. +/// +/// # Safety +/// - `compiled` must be a valid pointer from `rust_compile_parsed_program_off_thread()`. +/// - `vm_ptr` must be a valid `JS::VM*`. +/// - `source_code_ptr` must be a valid `JS::SourceCode const*`. +/// - `module_context` must be a valid `ModuleBuilder*`. +/// - `callbacks` must point to a valid `ModuleCallbacks`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_materialize_compiled_module( + compiled: *mut CompiledProgram, + vm_ptr: *mut c_void, + source_code_ptr: *const c_void, + module_context: *mut c_void, + callbacks: *const ModuleCallbacks, + tla_executable_out: *mut *mut c_void, +) -> *mut c_void { + unsafe { + abort_on_panic(|| { + if compiled.is_null() { + return std::ptr::null_mut(); + } + + let mut compiled = Box::from_raw(compiled); + let cb = &*callbacks; + + (cb.set_has_top_level_await)(module_context, compiled.parsed.has_top_level_await); + extract_module_metadata(&compiled.parsed.scope_ref.borrow(), module_context, cb); + + let bytecode = match &mut compiled.bytecode { + CompiledProgramBytecode::Program(bytecode) | CompiledProgramBytecode::AsyncModule(bytecode) => bytecode, + }; + extract_module_declarations( + &compiled.parsed.scope_ref.borrow(), + vm_ptr, + source_code_ptr, + module_context, + cb, + &mut bytecode.generator.function_table, + ); + extract_requested_modules(&compiled.parsed.scope_ref.borrow(), module_context, cb); + + match &mut compiled.bytecode { + CompiledProgramBytecode::AsyncModule(bytecode) => { + let exec_ptr = create_executable_from_compiled_bytecode(bytecode, vm_ptr, source_code_ptr); + if !tla_executable_out.is_null() { + *tla_executable_out = exec_ptr; + } + std::ptr::null_mut() + } + CompiledProgramBytecode::Program(bytecode) => { + if !tla_executable_out.is_null() { + *tla_executable_out = std::ptr::null_mut(); + } + create_executable_from_compiled_bytecode(bytecode, vm_ptr, source_code_ptr) + } + } + }) + } +} + // ============================================================================= // FFI entry point: module compilation // ============================================================================= @@ -1523,6 +1724,59 @@ unsafe fn extract_requested_modules(scope: &ast::ScopeData, ctx: *mut c_void, cb /// /// Emits async-function wrapping (initial Yield, final Yield) around the /// module body statements. +fn new_module_async_generator(source_len: usize, function_table: ast::FunctionTable) -> bytecode::generator::Generator { + let mut generator = bytecode::generator::Generator::new(); + generator.strict = true; + generator.function_table = function_table; + generator.source_len = source_len; + generator.enclosing_function_kind = ast::FunctionKind::Async; + generator +} + +fn compile_module_as_async_to_bytecode( + program: &ast::Statement, + scope_ref: &Rc>, + generator: &mut bytecode::generator::Generator, +) -> bytecode::generator::AssembledBytecode { + use bytecode::instruction::Instruction; + + let scope = scope_ref.borrow(); + + // Extract local variables from the program scope so the executable has the correct registers_and_locals_count. + // Without this, locals are not saved across await suspension points, causing them to become undefined. + generator.local_variables = convert_local_variables(&scope); + + let entry_block = generator.make_block(); + generator.switch_to_basic_block(entry_block); + + // Async function start: emit initial Yield before GetLexicalEnvironment. + let start_block = generator.make_block(); + let undef = generator.add_constant_undefined(); + generator.emit(Instruction::Yield { + continuation_label: Some(start_block), + value: undef.operand(), + }); + generator.switch_to_basic_block(start_block); + generator.capture_saved_lexical_environment(); + + // Generate module body statements. + let _result = bytecode::codegen::generate_statement(program, generator, None); + + // Async function end: emit final Yield (no continuation = done). + if !generator.is_current_block_terminated() { + let undef = generator.add_constant_undefined(); + generator.emit(Instruction::Yield { + continuation_label: None, + value: undef.operand(), + }); + } + + // Terminate all unterminated blocks with Yield. + generator.terminate_unterminated_blocks_with_yield(); + + generator.assemble() +} + unsafe fn compile_module_as_async( program: &ast::Statement, scope_ref: &Rc>, @@ -1533,52 +1787,11 @@ unsafe fn compile_module_as_async( function_table: ast::FunctionTable, ) -> *mut c_void { unsafe { - use bytecode::generator::Generator; - use bytecode::instruction::Instruction; - - let scope = scope_ref.borrow(); - let mut generator = Generator::new(); - generator.strict = true; - generator.function_table = function_table; + let mut generator = new_module_async_generator(source_len, function_table); generator.vm_ptr = vm_ptr; generator.source_code_ptr = source_code_ptr; - generator.source_len = source_len; - generator.enclosing_function_kind = ast::FunctionKind::Async; - // Extract local variables from the program scope so the executable has the - // correct registers_and_locals_count. Without this, locals are not saved - // across await suspension points, causing them to become undefined. - generator.local_variables = convert_local_variables(&scope); - - let entry_block = generator.make_block(); - generator.switch_to_basic_block(entry_block); - - // Async function start: emit initial Yield before GetLexicalEnvironment. - let start_block = generator.make_block(); - let undef = generator.add_constant_undefined(); - generator.emit(Instruction::Yield { - continuation_label: Some(start_block), - value: undef.operand(), - }); - generator.switch_to_basic_block(start_block); - generator.capture_saved_lexical_environment(); - - // Generate module body statements. - let _result = bytecode::codegen::generate_statement(program, &mut generator, None); - - // Async function end: emit final Yield (no continuation = done). - if !generator.is_current_block_terminated() { - let undef = generator.add_constant_undefined(); - generator.emit(Instruction::Yield { - continuation_label: None, - value: undef.operand(), - }); - } - - // Terminate all unterminated blocks with Yield. - generator.terminate_unterminated_blocks_with_yield(); - - let assembled = generator.assemble(); + let assembled = compile_module_as_async_to_bytecode(program, scope_ref, &mut generator); bytecode::ffi::create_executable(&mut generator, &assembled, vm_ptr, source_code_ptr) } } diff --git a/Libraries/LibJS/RustIntegration.cpp b/Libraries/LibJS/RustIntegration.cpp index b76e3f1073..05ac8521e4 100644 --- a/Libraries/LibJS/RustIntegration.cpp +++ b/Libraries/LibJS/RustIntegration.cpp @@ -359,6 +359,11 @@ ParsedProgram* parse_program(u16 const* utf16_data, size_t length_in_code_units, return rust_parse_program(utf16_data, length_in_code_units, static_cast(type), line_number_offset, g_dump_ast, g_dump_ast_use_color); } +CompiledProgram* compile_parsed_program_off_thread(ParsedProgram* parsed, size_t length_in_code_units) +{ + return rust_compile_parsed_program_off_thread(parsed, length_in_code_units); +} + bool parsed_program_has_errors(ParsedProgram const* parsed) { return rust_parsed_program_has_errors(const_cast(parsed)); @@ -369,6 +374,11 @@ void free_parsed_program(ParsedProgram* parsed) rust_free_parsed_program(parsed); } +void free_compiled_program(CompiledProgram* compiled) +{ + rust_free_compiled_program(compiled); +} + Optional>> compile_parsed_script(ParsedProgram* parsed, NonnullRefPtr source_code, Realm& realm) { if (!parsed) @@ -395,6 +405,23 @@ Optional>> compile_parsed_script(Parsed return builder.result; } +Optional>> materialize_compiled_script(CompiledProgram* compiled, NonnullRefPtr source_code, Realm& realm) +{ + if (!compiled) + return {}; + + GC::DeferGC defer_gc(realm.vm().heap()); + ScriptGdiBuilder builder; + + void* exec_ptr = rust_materialize_compiled_script(compiled, &realm.vm(), source_code.ptr(), &builder); + + if (!exec_ptr) + return Vector {}; + + builder.result.executable = static_cast(exec_ptr); + return builder.result; +} + Optional>> compile_script(StringView source_text, Realm& realm, StringView filename, size_t line_number_offset) { auto source_code = SourceCode::create( @@ -502,6 +529,55 @@ Optional>> compile_parsed_module(Parsed return builder.result; } +Optional>> materialize_compiled_module(CompiledProgram* compiled, NonnullRefPtr source_code, Realm& realm) +{ + if (!compiled) + return {}; + + GC::DeferGC defer_gc(realm.vm().heap()); + ModuleBuilder builder; + ModuleCallbacks callbacks { + .set_has_top_level_await = module_set_has_top_level_await, + .push_import_entry = module_push_import_entry, + .push_local_export = module_push_local_export, + .push_indirect_export = module_push_indirect_export, + .push_star_export = module_push_star_export, + .push_requested_module = module_push_requested_module, + .set_default_export_binding = module_set_default_export_binding, + .push_var_name = module_push_var_name, + .push_function = module_push_function, + .push_lexical_binding = module_push_lexical_binding, + }; + + void* tla_executable = nullptr; + + void* exec_ptr = rust_materialize_compiled_module(compiled, &realm.vm(), source_code.ptr(), + &builder, &callbacks, &tla_executable); + + if (!exec_ptr && !tla_executable) + return Vector {}; + + if (tla_executable) { + auto& vm = realm.vm(); + auto* tla_exec = static_cast(tla_executable); + + builder.result.tla_shared_data = vm.heap().allocate( + vm, FunctionKind::Async, + "module code with top-level await"_utf16_fly_string, + 0, 0, true, false, true, + Vector {}, nullptr); + builder.result.tla_shared_data->m_is_module_wrapper = true; + builder.result.tla_shared_data->m_uses_this = true; + builder.result.tla_shared_data->m_function_environment_needed = true; + builder.result.tla_shared_data->update_asm_call_metadata(); + builder.result.tla_shared_data->set_executable(tla_exec); + } else { + builder.result.executable = static_cast(exec_ptr); + } + + return builder.result; +} + Optional>> compile_module(StringView source_text, Realm& realm, StringView filename) { auto source_code = SourceCode::create(String::from_utf8(filename).release_value_but_fixme_should_propagate_errors(), Utf16String::from_utf8(source_text)); diff --git a/Libraries/LibJS/RustIntegration.h b/Libraries/LibJS/RustIntegration.h index 13e120b4fd..4174085b0d 100644 --- a/Libraries/LibJS/RustIntegration.h +++ b/Libraries/LibJS/RustIntegration.h @@ -25,6 +25,7 @@ namespace JS::FFI { struct ParsedProgram; +struct CompiledProgram; } @@ -89,17 +90,27 @@ JS_API bool rust_pipeline_available(); // Parse a program (script or module) without GC interaction. Thread-safe. JS_API FFI::ParsedProgram* parse_program(u16 const* utf16_data, size_t length_in_code_units, ProgramType type, size_t line_number_offset = 0); +// Compile a parsed program to bytecode without touching the VM or GC. Thread-safe. +JS_API FFI::CompiledProgram* compile_parsed_program_off_thread(FFI::ParsedProgram* parsed, size_t length_in_code_units); + // Check if a parsed program has errors. Does not consume the program. JS_API bool parsed_program_has_errors(FFI::ParsedProgram const*); // Free a parsed program without compiling it. JS_API void free_parsed_program(FFI::ParsedProgram*); +// Free a compiled program without materializing it. +JS_API void free_compiled_program(FFI::CompiledProgram*); + // Compile a previously parsed script. Must be called on the main thread. // Consumes and frees the Rust ParsedProgram. // Returns nullopt if Rust is not available. Optional>> compile_parsed_script(FFI::ParsedProgram* parsed, NonnullRefPtr source_code, Realm& realm); +// Materialize a previously compiled script. Must be called on the main thread. +// Consumes and frees the Rust CompiledProgram. +Optional>> materialize_compiled_script(FFI::CompiledProgram* compiled, NonnullRefPtr source_code, Realm& realm); + // Compile a script. Returns nullopt if Rust is not available. Optional>> compile_script(StringView source_text, Realm& realm, StringView filename, size_t line_number_offset); @@ -115,6 +126,10 @@ Optional> compile_eval( // Returns nullopt if Rust is not available. Optional>> compile_parsed_module(FFI::ParsedProgram* parsed, NonnullRefPtr source_code, Realm& realm); +// Materialize a previously compiled module. Must be called on the main thread. +// Consumes and frees the Rust CompiledProgram. +Optional>> materialize_compiled_module(FFI::CompiledProgram* compiled, NonnullRefPtr source_code, Realm& realm); + // Compile a module. Returns nullopt if Rust is not available. Optional>> compile_module(StringView source_text, Realm& realm, StringView filename); diff --git a/Libraries/LibJS/Script.cpp b/Libraries/LibJS/Script.cpp index 5bbb4b41bd..7fa0e1a8d2 100644 --- a/Libraries/LibJS/Script.cpp +++ b/Libraries/LibJS/Script.cpp @@ -42,6 +42,17 @@ Result, Vector> Script::create_from_parsed(FFI::Par return realm.heap().allocate