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.
This commit is contained in:
parent
0d120019df
commit
4a7dc45b3f
12 changed files with 496 additions and 87 deletions
|
|
@ -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<RefCell<ast::ScopeData>>,
|
||||
) -> 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<RefCell<ast::ScopeData>>,
|
||||
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<RefCell<ast::ScopeData>>,
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<u8>(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<ParsedProgram*>(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<Result<ScriptResult, Vector<ParserError>>> compile_parsed_script(ParsedProgram* parsed, NonnullRefPtr<SourceCode const> source_code, Realm& realm)
|
||||
{
|
||||
if (!parsed)
|
||||
|
|
@ -395,6 +405,23 @@ Optional<Result<ScriptResult, Vector<ParserError>>> compile_parsed_script(Parsed
|
|||
return builder.result;
|
||||
}
|
||||
|
||||
Optional<Result<ScriptResult, Vector<ParserError>>> materialize_compiled_script(CompiledProgram* compiled, NonnullRefPtr<SourceCode const> 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<ParserError> {};
|
||||
|
||||
builder.result.executable = static_cast<Bytecode::Executable*>(exec_ptr);
|
||||
return builder.result;
|
||||
}
|
||||
|
||||
Optional<Result<ScriptResult, Vector<ParserError>>> compile_script(StringView source_text, Realm& realm, StringView filename, size_t line_number_offset)
|
||||
{
|
||||
auto source_code = SourceCode::create(
|
||||
|
|
@ -502,6 +529,55 @@ Optional<Result<ModuleResult, Vector<ParserError>>> compile_parsed_module(Parsed
|
|||
return builder.result;
|
||||
}
|
||||
|
||||
Optional<Result<ModuleResult, Vector<ParserError>>> materialize_compiled_module(CompiledProgram* compiled, NonnullRefPtr<SourceCode const> 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<ParserError> {};
|
||||
|
||||
if (tla_executable) {
|
||||
auto& vm = realm.vm();
|
||||
auto* tla_exec = static_cast<Bytecode::Executable*>(tla_executable);
|
||||
|
||||
builder.result.tla_shared_data = vm.heap().allocate<SharedFunctionInstanceData>(
|
||||
vm, FunctionKind::Async,
|
||||
"module code with top-level await"_utf16_fly_string,
|
||||
0, 0, true, false, true,
|
||||
Vector<Utf16FlyString> {}, 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<Bytecode::Executable*>(exec_ptr);
|
||||
}
|
||||
|
||||
return builder.result;
|
||||
}
|
||||
|
||||
Optional<Result<ModuleResult, Vector<ParserError>>> 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));
|
||||
|
|
|
|||
|
|
@ -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<Result<ScriptResult, Vector<ParserError>>> compile_parsed_script(FFI::ParsedProgram* parsed, NonnullRefPtr<SourceCode const> source_code, Realm& realm);
|
||||
|
||||
// Materialize a previously compiled script. Must be called on the main thread.
|
||||
// Consumes and frees the Rust CompiledProgram.
|
||||
Optional<Result<ScriptResult, Vector<ParserError>>> materialize_compiled_script(FFI::CompiledProgram* compiled, NonnullRefPtr<SourceCode const> source_code, Realm& realm);
|
||||
|
||||
// Compile a script. Returns nullopt if Rust is not available.
|
||||
Optional<Result<ScriptResult, Vector<ParserError>>> compile_script(StringView source_text, Realm& realm, StringView filename, size_t line_number_offset);
|
||||
|
||||
|
|
@ -115,6 +126,10 @@ Optional<Result<EvalResult, String>> compile_eval(
|
|||
// Returns nullopt if Rust is not available.
|
||||
Optional<Result<ModuleResult, Vector<ParserError>>> compile_parsed_module(FFI::ParsedProgram* parsed, NonnullRefPtr<SourceCode const> source_code, Realm& realm);
|
||||
|
||||
// Materialize a previously compiled module. Must be called on the main thread.
|
||||
// Consumes and frees the Rust CompiledProgram.
|
||||
Optional<Result<ModuleResult, Vector<ParserError>>> materialize_compiled_module(FFI::CompiledProgram* compiled, NonnullRefPtr<SourceCode const> source_code, Realm& realm);
|
||||
|
||||
// Compile a module. Returns nullopt if Rust is not available.
|
||||
Optional<Result<ModuleResult, Vector<ParserError>>> compile_module(StringView source_text, Realm& realm, StringView filename);
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,17 @@ Result<GC::Ref<Script>, Vector<ParserError>> Script::create_from_parsed(FFI::Par
|
|||
return realm.heap().allocate<Script>(realm, filename, move(rust_compilation->value()), host_defined);
|
||||
}
|
||||
|
||||
Result<GC::Ref<Script>, Vector<ParserError>> Script::create_from_compiled(FFI::CompiledProgram* compiled, NonnullRefPtr<SourceCode const> source_code, Realm& realm, HostDefined* host_defined)
|
||||
{
|
||||
auto filename = source_code->filename();
|
||||
auto rust_compilation = RustIntegration::materialize_compiled_script(compiled, move(source_code), realm);
|
||||
if (!rust_compilation.has_value())
|
||||
return Vector<ParserError> {};
|
||||
if (rust_compilation->is_error())
|
||||
return rust_compilation->release_error();
|
||||
return realm.heap().allocate<Script>(realm, filename, move(rust_compilation->value()), host_defined);
|
||||
}
|
||||
|
||||
Script::Script(Realm& realm, StringView filename, RustIntegration::ScriptResult&& result, HostDefined* host_defined)
|
||||
: m_realm(realm)
|
||||
, m_executable(result.executable)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ JS_API extern bool g_dump_ast_use_color;
|
|||
namespace FFI {
|
||||
|
||||
struct ParsedProgram;
|
||||
struct CompiledProgram;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ public:
|
|||
virtual ~Script() override;
|
||||
static Result<GC::Ref<Script>, Vector<ParserError>> parse(StringView source_text, Realm&, StringView filename = {}, HostDefined* = nullptr, size_t line_number_offset = 1);
|
||||
static Result<GC::Ref<Script>, Vector<ParserError>> create_from_parsed(FFI::ParsedProgram* parsed, NonnullRefPtr<SourceCode const> source_code, Realm&, HostDefined* = nullptr);
|
||||
static Result<GC::Ref<Script>, Vector<ParserError>> create_from_compiled(FFI::CompiledProgram* compiled, NonnullRefPtr<SourceCode const> source_code, Realm&, HostDefined* = nullptr);
|
||||
|
||||
Realm& realm() { return *m_realm; }
|
||||
Vector<LoadedModuleRequest>& loaded_modules() { return m_loaded_modules; }
|
||||
|
|
|
|||
|
|
@ -83,6 +83,29 @@ Result<GC::Ref<SourceTextModule>, Vector<ParserError>> SourceTextModule::parse_f
|
|||
module_result.executable.ptr(), module_result.tla_shared_data.ptr());
|
||||
}
|
||||
|
||||
Result<GC::Ref<SourceTextModule>, Vector<ParserError>> SourceTextModule::parse_from_pre_compiled(FFI::CompiledProgram* compiled, NonnullRefPtr<SourceCode const> source_code, Realm& realm, Script::HostDefined* host_defined)
|
||||
{
|
||||
auto filename = source_code->filename();
|
||||
auto rust_result = RustIntegration::materialize_compiled_module(compiled, move(source_code), realm);
|
||||
// Always from the Rust pipeline, so the Optional must have a value.
|
||||
VERIFY(rust_result.has_value());
|
||||
if (rust_result->is_error())
|
||||
return rust_result->release_error();
|
||||
auto& module_result = rust_result->value();
|
||||
Vector<FunctionToInitialize> functions_to_initialize;
|
||||
functions_to_initialize.ensure_capacity(module_result.functions_to_initialize.size());
|
||||
for (auto& f : module_result.functions_to_initialize)
|
||||
functions_to_initialize.append({ *f.shared_data, move(f.name) });
|
||||
return realm.heap().allocate<SourceTextModule>(
|
||||
realm, filename, host_defined, module_result.has_top_level_await,
|
||||
move(module_result.requested_modules), move(module_result.import_entries),
|
||||
move(module_result.local_export_entries), move(module_result.indirect_export_entries),
|
||||
move(module_result.star_export_entries), move(module_result.default_export_binding_name),
|
||||
move(module_result.var_declared_names), move(module_result.lexical_bindings),
|
||||
move(functions_to_initialize),
|
||||
module_result.executable.ptr(), module_result.tla_shared_data.ptr());
|
||||
}
|
||||
|
||||
// 16.2.1.7.1 ParseModule ( sourceText, realm, hostDefined ), https://tc39.es/ecma262/#sec-parsemodule
|
||||
Result<GC::Ref<SourceTextModule>, Vector<ParserError>> SourceTextModule::parse(StringView source_text, Realm& realm, StringView filename, Script::HostDefined* host_defined)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ namespace JS {
|
|||
namespace FFI {
|
||||
|
||||
struct ParsedProgram;
|
||||
struct CompiledProgram;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ public:
|
|||
|
||||
static Result<GC::Ref<SourceTextModule>, Vector<ParserError>> parse(StringView source_text, Realm&, StringView filename = {}, Script::HostDefined* host_defined = nullptr);
|
||||
static Result<GC::Ref<SourceTextModule>, Vector<ParserError>> parse_from_pre_parsed(FFI::ParsedProgram* parsed, NonnullRefPtr<SourceCode const> source_code, Realm&, Script::HostDefined* host_defined = nullptr);
|
||||
static Result<GC::Ref<SourceTextModule>, Vector<ParserError>> parse_from_pre_compiled(FFI::CompiledProgram* compiled, NonnullRefPtr<SourceCode const> source_code, Realm&, Script::HostDefined* host_defined = nullptr);
|
||||
|
||||
virtual Vector<Utf16FlyString> get_exported_names(VM& vm, HashTable<Module const*>& export_star_set) override;
|
||||
virtual ResolvedBinding resolve_export(VM& vm, Utf16FlyString const& export_name, Vector<ResolvedBinding> resolve_set = {}) override;
|
||||
|
|
|
|||
|
|
@ -105,6 +105,39 @@ GC::Ref<ClassicScript> ClassicScript::create_from_pre_parsed(ByteString filename
|
|||
return script;
|
||||
}
|
||||
|
||||
GC::Ref<ClassicScript> ClassicScript::create_from_pre_compiled(ByteString filename, NonnullRefPtr<JS::SourceCode const> source_code, EnvironmentSettingsObject& settings, URL::URL base_url, JS::FFI::CompiledProgram* compiled, MutedErrors muted_errors)
|
||||
{
|
||||
auto& realm = settings.realm();
|
||||
auto& vm = realm.vm();
|
||||
|
||||
if (muted_errors == MutedErrors::Yes)
|
||||
base_url = URL::about_blank();
|
||||
|
||||
auto script = vm.heap().allocate<ClassicScript>(move(base_url), move(filename), settings);
|
||||
|
||||
script->m_muted_errors = muted_errors;
|
||||
script->set_parse_error(JS::js_null());
|
||||
script->set_error_to_rethrow(JS::js_null());
|
||||
|
||||
auto parse_timer = Core::ElapsedTimer::start_new();
|
||||
auto result = JS::Script::create_from_compiled(compiled, move(source_code), realm, script);
|
||||
dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Materialized pre-compiled {} in {}ms", script->filename(), parse_timer.elapsed_milliseconds());
|
||||
|
||||
if (result.is_error()) {
|
||||
auto& parse_error = result.error().first();
|
||||
dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Failed to materialize: {}", parse_error.to_string());
|
||||
|
||||
script->set_parse_error(JS::SyntaxError::create(realm, parse_error.to_string()));
|
||||
script->set_error_to_rethrow(script->parse_error());
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
script->m_script_record = *result.release_value();
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#run-a-classic-script
|
||||
JS::Completion ClassicScript::run(RethrowErrors rethrow_errors, GC::Ptr<JS::Environment> lexical_environment_override)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public:
|
|||
};
|
||||
static GC::Ref<ClassicScript> create(ByteString filename, StringView source, EnvironmentSettingsObject&, URL::URL base_url, size_t source_line_number = 1, MutedErrors = MutedErrors::No);
|
||||
static GC::Ref<ClassicScript> create_from_pre_parsed(ByteString filename, NonnullRefPtr<JS::SourceCode const> source_code, EnvironmentSettingsObject&, URL::URL base_url, JS::FFI::ParsedProgram* parsed, MutedErrors = MutedErrors::No);
|
||||
static GC::Ref<ClassicScript> create_from_pre_compiled(ByteString filename, NonnullRefPtr<JS::SourceCode const> source_code, EnvironmentSettingsObject&, URL::URL base_url, JS::FFI::CompiledProgram* compiled, MutedErrors = MutedErrors::No);
|
||||
|
||||
JS::Script* script_record() { return m_script_record; }
|
||||
JS::Script const* script_record() const { return m_script_record; }
|
||||
|
|
|
|||
|
|
@ -39,26 +39,27 @@
|
|||
|
||||
namespace Web::HTML {
|
||||
|
||||
// Submit a parse_program() call to the thread pool, then bounce back to
|
||||
// the main thread via deferred_invoke once parsing completes.
|
||||
// `on_parsed` is called on the main thread with the Rust ParsedProgram*
|
||||
// and the SourceCode.
|
||||
// NB: The SourceCode stays on the main thread (inside the heap-allocated
|
||||
// callback). The worker thread only receives raw UTF-16 data pointers.
|
||||
// The callback is heap-allocated so that if the event loop is
|
||||
// destroyed during parsing, we leak it (and any GC::Root objects it
|
||||
// captures) rather than destroying them on the worker thread.
|
||||
static void parse_off_thread(NonnullRefPtr<JS::SourceCode const> source_code, JS::RustIntegration::ProgramType type, size_t line_number_offset, Function<void(JS::FFI::ParsedProgram*, NonnullRefPtr<JS::SourceCode const>)> on_parsed)
|
||||
struct OffThreadCompiledProgram {
|
||||
JS::FFI::ParsedProgram* parsed { nullptr };
|
||||
JS::FFI::CompiledProgram* compiled { nullptr };
|
||||
};
|
||||
|
||||
// Submit parsing and top-level bytecode generation to the thread pool, then bounce back to the main thread via
|
||||
// deferred_invoke once the worker is done. Syntax errors still come back as a ParsedProgram so the main thread can
|
||||
// report them through the same Script/ModuleScript construction paths; successful programs come back as CompiledProgram
|
||||
// artifacts whose GC-backed Executable materialization must still happen on the main thread.
|
||||
// NB: The SourceCode stays on the main thread inside the heap-allocated callback. The worker thread only receives raw
|
||||
// UTF-16 data pointers, and the callback intentionally leaks if the event loop is destroyed during compilation.
|
||||
static void compile_off_thread(NonnullRefPtr<JS::SourceCode const> source_code, JS::RustIntegration::ProgramType type, size_t line_number_offset, Function<void(OffThreadCompiledProgram, NonnullRefPtr<JS::SourceCode const>)> on_compiled)
|
||||
{
|
||||
// Extract the raw data the parser needs while still on the main thread.
|
||||
auto const* utf16_data = source_code->utf16_data();
|
||||
auto length = source_code->length_in_code_units();
|
||||
|
||||
// Capture source_code in the callback so it stays alive (on the main
|
||||
// thread) for the duration of parsing and is available when we compile.
|
||||
auto* callback = new Function<void(JS::FFI::ParsedProgram*)>(
|
||||
[on_parsed = move(on_parsed), source_code = move(source_code)](JS::FFI::ParsedProgram* parsed) mutable {
|
||||
on_parsed(parsed, move(source_code));
|
||||
// Capture source_code in the callback so it stays alive on the main thread and is available for materialization.
|
||||
auto* callback = new Function<void(OffThreadCompiledProgram)>(
|
||||
[on_compiled = move(on_compiled), source_code = move(source_code)](OffThreadCompiledProgram result) mutable {
|
||||
on_compiled(result, move(source_code));
|
||||
});
|
||||
|
||||
auto event_loop_weak = Core::EventLoop::current_weak();
|
||||
|
|
@ -67,12 +68,17 @@ static void parse_off_thread(NonnullRefPtr<JS::SourceCode const> source_code, JS
|
|||
callback,
|
||||
event_loop_weak = move(event_loop_weak)]() {
|
||||
auto* parsed = JS::RustIntegration::parse_program(utf16_data, length, type, line_number_offset);
|
||||
OffThreadCompiledProgram result { .parsed = parsed };
|
||||
if (parsed && !JS::RustIntegration::parsed_program_has_errors(parsed)) {
|
||||
result.compiled = JS::RustIntegration::compile_parsed_program_off_thread(parsed, length);
|
||||
result.parsed = nullptr;
|
||||
}
|
||||
|
||||
auto origin = event_loop_weak->take();
|
||||
if (!origin)
|
||||
return;
|
||||
origin->deferred_invoke([parsed, callback]() {
|
||||
(*callback)(parsed);
|
||||
origin->deferred_invoke([result, callback]() {
|
||||
(*callback)(result);
|
||||
delete callback;
|
||||
// AD-HOC: Perform a microtask checkpoint so that any microtasks queued by the callback (e.g. promise
|
||||
// reactions from react_to_promise during module linking) are drained. Without this, module worker
|
||||
|
|
@ -438,11 +444,13 @@ void fetch_classic_script(GC::Ref<HTMLScriptElement> element, URL::URL const& ur
|
|||
String::from_utf8(response_url_string.view()).release_value_but_fixme_should_propagate_errors(),
|
||||
Utf16String::from_utf8(source_text));
|
||||
|
||||
parse_off_thread(move(source_code), JS::RustIntegration::ProgramType::Script, 1,
|
||||
compile_off_thread(move(source_code), JS::RustIntegration::ProgramType::Script, 1,
|
||||
[response_url = move(response_url), response_url_string = move(response_url_string),
|
||||
muted_errors, on_complete_root = move(on_complete_root),
|
||||
settings_root = move(settings_root)](auto* parsed, auto source_code) mutable {
|
||||
auto script = ClassicScript::create_from_pre_parsed(move(response_url_string), move(source_code), *settings_root, move(response_url), parsed, muted_errors);
|
||||
settings_root = move(settings_root)](auto result, auto source_code) mutable {
|
||||
auto script = result.compiled
|
||||
? ClassicScript::create_from_pre_compiled(move(response_url_string), move(source_code), *settings_root, move(response_url), result.compiled, muted_errors)
|
||||
: ClassicScript::create_from_pre_parsed(move(response_url_string), move(source_code), *settings_root, move(response_url), result.parsed, muted_errors);
|
||||
on_complete_root->function()(script);
|
||||
});
|
||||
} else {
|
||||
|
|
@ -798,12 +806,14 @@ void fetch_single_module_script(JS::Realm& realm,
|
|||
String::from_utf8(url_string.view()).release_value_but_fixme_should_propagate_errors(),
|
||||
Utf16String::from_utf8(source_text));
|
||||
|
||||
parse_off_thread(move(source_code), JS::RustIntegration::ProgramType::Module, 0,
|
||||
compile_off_thread(move(source_code), JS::RustIntegration::ProgramType::Module, 0,
|
||||
[url = move(url), url_string = move(url_string), response_url = move(response_url),
|
||||
module_type_string = move(module_type_string),
|
||||
on_complete_root = move(on_complete_root),
|
||||
settings_root = move(settings_root)](auto* parsed, auto source_code) mutable {
|
||||
auto module_script = ModuleScript::create_from_pre_parsed(url_string, move(source_code), *settings_root, move(response_url), parsed).release_value_but_fixme_should_propagate_errors();
|
||||
settings_root = move(settings_root)](auto result, auto source_code) mutable {
|
||||
auto module_script = result.compiled
|
||||
? ModuleScript::create_from_pre_compiled(url_string, move(source_code), *settings_root, move(response_url), result.compiled).release_value_but_fixme_should_propagate_errors()
|
||||
: ModuleScript::create_from_pre_parsed(url_string, move(source_code), *settings_root, move(response_url), result.parsed).release_value_but_fixme_should_propagate_errors();
|
||||
settings_root->module_map().set(url, module_type_string, { ModuleMap::EntryType::ModuleScript, module_script });
|
||||
on_complete_root->function()(module_script);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -90,6 +90,27 @@ WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> ModuleScript::create_from_pre_parsed(
|
|||
return script;
|
||||
}
|
||||
|
||||
WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> ModuleScript::create_from_pre_compiled(ByteString const& filename, NonnullRefPtr<JS::SourceCode const> source_code, EnvironmentSettingsObject& settings, URL::URL base_url, JS::FFI::CompiledProgram* compiled)
|
||||
{
|
||||
auto& realm = settings.realm();
|
||||
auto script = realm.create<ModuleScript>(move(base_url), filename, settings);
|
||||
|
||||
script->set_parse_error(JS::js_null());
|
||||
script->set_error_to_rethrow(JS::js_null());
|
||||
|
||||
auto result = JS::SourceTextModule::parse_from_pre_compiled(compiled, move(source_code), realm, script);
|
||||
|
||||
if (result.is_error()) {
|
||||
auto& parse_error = result.error().first();
|
||||
dbgln("JavaScriptModuleScript: Failed to materialize: {}", parse_error.to_string());
|
||||
script->set_parse_error(JS::SyntaxError::create(realm, parse_error.to_string()));
|
||||
return script;
|
||||
}
|
||||
|
||||
script->m_record = result.value();
|
||||
return script;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-css-module-script
|
||||
WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> ModuleScript::create_a_css_module_script(ByteString const& filename, StringView source, EnvironmentSettingsObject& settings)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
namespace JS::FFI {
|
||||
|
||||
struct ParsedProgram;
|
||||
struct CompiledProgram;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ public:
|
|||
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create(ByteString const& filename, StringView source, EnvironmentSettingsObject&, URL::URL base_url);
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_from_pre_parsed(ByteString const& filename, NonnullRefPtr<JS::SourceCode const> source_code, EnvironmentSettingsObject&, URL::URL base_url, JS::FFI::ParsedProgram* parsed);
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_from_pre_compiled(ByteString const& filename, NonnullRefPtr<JS::SourceCode const> source_code, EnvironmentSettingsObject&, URL::URL base_url, JS::FFI::CompiledProgram* compiled);
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_a_javascript_module_script(ByteString const& filename, StringView source, EnvironmentSettingsObject&, URL::URL base_url);
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_a_css_module_script(ByteString const& filename, StringView source, EnvironmentSettingsObject&);
|
||||
static WebIDL::ExceptionOr<GC::Ptr<ModuleScript>> create_a_json_module_script(ByteString const& filename, StringView source, EnvironmentSettingsObject&);
|
||||
|
|
|
|||
Loading…
Reference in a new issue