-
-
Notifications
You must be signed in to change notification settings - Fork 159
fix(wasm): six codegen divergences from the native backend, found porting a 3D game #6524
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
10b5146
12e3f5f
fd36c84
d80eec2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -688,6 +688,22 @@ impl WasmModuleEmitter { | |
| for &(fid, idx) in &per_module_async[mod_idx] { | ||
| module_fm.insert(fid, idx); | ||
| } | ||
| // Function names are NOT globally unique across modules (a | ||
| // serializer's local `function vec3(v): string` coexists with the | ||
| // math library's exported `vec3(x, y, z)`), but `func_name_map` — | ||
| // the ExternFuncRef cross-module resolution table — is keyed by | ||
| // bare name. Prefer EXPORTED functions (the only legitimate | ||
| // cross-module call targets); a module-local helper only claims a | ||
| // name nobody exported. | ||
| let exported_names: std::collections::HashSet<&str> = module | ||
| .exported_functions | ||
| .iter() | ||
| .map(|(n, _)| n.as_str()) | ||
| .chain(module.exports.iter().filter_map(|e| match e { | ||
| perry_hir::ir::Export::Named { local, .. } => Some(local.as_str()), | ||
| _ => None, | ||
| })) | ||
| .collect(); | ||
| for func in &module.functions { | ||
| if func.is_async { | ||
| continue; // already registered as bridge import | ||
|
|
@@ -707,8 +723,16 @@ impl WasmModuleEmitter { | |
| self.void_funcs.insert(user_func_idx); | ||
| } | ||
| self.func_param_counts.insert(user_func_idx, param_count); | ||
| // Build func_name_map for ExternFuncRef resolution (name is globally unique) | ||
| self.func_name_map.insert(func.name.clone(), user_func_idx); | ||
| // Build func_name_map for ExternFuncRef resolution. Exported | ||
| // functions win the name; module-local helpers only fill a | ||
| // vacant slot (see exported_names above). | ||
| if exported_names.contains(func.name.as_str()) { | ||
| self.func_name_map.insert(func.name.clone(), user_func_idx); | ||
| } else { | ||
| self.func_name_map | ||
| .entry(func.name.clone()) | ||
| .or_insert(user_func_idx); | ||
| } | ||
| user_func_idx += 1; | ||
| } | ||
| self.module_func_maps.push(module_fm); | ||
|
|
@@ -864,11 +888,13 @@ impl WasmModuleEmitter { | |
| // the driver) and `Module.name` is a relative-from-project-root path. | ||
| // We compare paths by file-stem match against `Module.name` (which is | ||
| // a leaf "name.ts" or "subdir/name.ts" string), falling back to a | ||
| // basename match. Re-exports (`Export::ReExport`) point at another | ||
| // module by `source`; we don't chase those here — a one-hop re-export | ||
| // is handled by the source's own exports list (the re-export pass | ||
| // typically flattens through), and complex chains can be added later | ||
| // with a visited-set on demand. | ||
| // basename match. Re-exports (`Export::ReExport`, `ExportAll`, and the | ||
| // import-then-`export { x }` shape) are chased by | ||
| // `resolve_export_to_let` with a depth cap — a library facade like | ||
| // bloom's `index.ts` re-exporting `Key` from `core/keys.ts` is two to | ||
| // three hops deep, and stopping at the first module made every | ||
| // re-exported const OBJECT read undefined (scalars sometimes survived | ||
| // via other paths, which made the failure look random). | ||
| { | ||
| // module.name → source module index | ||
| let name_to_idx: std::collections::HashMap<&str, usize> = modules | ||
|
|
@@ -904,32 +930,96 @@ impl WasmModuleEmitter { | |
| let src_lets = &src_let_names[src_idx]; | ||
| for spec in &import.specifiers { | ||
| if let perry_hir::ir::ImportSpecifier::Named { imported, local } = spec { | ||
| // Walk the source module's exports to map the | ||
| // public `imported` name back to a source-local | ||
| // identifier, then look up that identifier's let. | ||
| let src_module = &modules[src_idx].1; | ||
| let mut resolved_local: Option<&str> = None; | ||
| for export in &src_module.exports { | ||
| if let perry_hir::ir::Export::Named { | ||
| local: src_local, | ||
| exported, | ||
| } = export | ||
| { | ||
| if exported == imported { | ||
| resolved_local = Some(src_local.as_str()); | ||
| break; | ||
| } | ||
| } | ||
| // Resolve the public `imported` name to a let | ||
| // global, following re-export chains (see | ||
| // resolve_export_to_let). | ||
| let resolved = resolve_export_to_let( | ||
| modules, | ||
| &src_let_names, | ||
| &name_to_idx, | ||
| src_idx, | ||
| imported, | ||
| 8, | ||
| ); | ||
| if std::env::var("PERRY_WASM_DEBUG_IMPORTS").is_ok() { | ||
| eprintln!( | ||
| "[wasm-imports] {} imports {{ {} }} from {} -> module #{} ({}) => {:?}", | ||
| modules[consumer_idx].1.name, | ||
| imported, | ||
| import.source, | ||
| src_idx, | ||
| modules[src_idx].1.name, | ||
| resolved, | ||
| ); | ||
| } | ||
| // Direct fall-through: if no Export::Named matched | ||
| // but a Let with the imported name exists, use it. | ||
| // (Some HIR lowering shapes register exports out-of- | ||
| // band; this keeps `export const X = ...` robust.) | ||
| let key = resolved_local.unwrap_or(imported.as_str()); | ||
| if let Some(&gidx) = src_lets.get(key) { | ||
| if let Some(gidx) = resolved { | ||
| self.imported_var_globals | ||
| .insert((consumer_idx, local.clone()), gidx); | ||
| } | ||
| // Function imports resolve per-consumer too — the | ||
| // whole-program func_name_map's bare-name keys | ||
| // collide across modules. | ||
| if let Some(fidx) = resolve_export_to_func( | ||
| modules, | ||
| &self.module_func_maps, | ||
| &name_to_idx, | ||
| src_idx, | ||
| imported, | ||
| 8, | ||
| ) { | ||
| self.imported_func_indices | ||
| .insert((consumer_idx, local.clone()), fidx); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
| // Namespace import (`import * as W from "./mod"`): | ||
| // register every exported module-level let under a | ||
| // DOTTED key ("W.MESH_COUNT"), so PropertyGet on the | ||
| // namespace ident resolves to the source module's | ||
| // promoted-let global — the same mechanism the Named | ||
| // arm above uses. Without this, every `W.member` read | ||
| // emitted a class_get_field on an undefined receiver | ||
| // and produced undefined (functions kept working via | ||
| // the whole-program name map, which made the failure | ||
| // maddeningly partial). | ||
| if let perry_hir::ir::ImportSpecifier::Namespace { local } = spec { | ||
| // Register `W.<member>` for exactly the source | ||
| // module's PUBLIC surface — its named/re-exported/ | ||
| // function/object exports, plus everything reached | ||
| // through `export * from "..."` (recursively). This | ||
| // replaced a blanket "register every module-level | ||
| // let" loop, which both exposed PRIVATE locals as | ||
| // `W.private` (not valid JS namespace members) and | ||
| // missed `export *` re-exports entirely. | ||
| let mut public: std::collections::BTreeSet<String> = | ||
| std::collections::BTreeSet::new(); | ||
| collect_exported_names(modules, src_idx, 8, &mut public); | ||
| for name in &public { | ||
| if let Some(gidx) = resolve_export_to_let( | ||
| modules, | ||
| &src_let_names, | ||
| &name_to_idx, | ||
| src_idx, | ||
| name, | ||
| 8, | ||
| ) { | ||
| self.imported_var_globals.insert( | ||
| (consumer_idx, format!("{}.{}", local, name)), | ||
| gidx, | ||
| ); | ||
| } | ||
| if let Some(fidx) = resolve_export_to_func( | ||
| modules, | ||
| &self.module_func_maps, | ||
| &name_to_idx, | ||
| src_idx, | ||
| name, | ||
| 8, | ||
| ) { | ||
| self.imported_ns_funcs | ||
| .entry((consumer_idx, format!("{}.{}", local, name))) | ||
| .or_insert(fidx); | ||
| } | ||
|
Comment on lines
+974
to
+1021
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Populate namespaces from the transitive export surface.
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1311,6 +1401,13 @@ impl WasmModuleEmitter { | |
| // Initialize globals — swap in per-module func_map for correct FuncRef resolution | ||
| for (mod_idx, (_, module)) in modules.iter().enumerate() { | ||
| self.func_map = self.module_func_maps[mod_idx].clone(); | ||
| // Per-consumer import resolution (imported_var_globals / | ||
| // imported_func_indices / imported_ns_funcs) is keyed by | ||
| // current_mod_idx; a module-scope initializer that calls an | ||
| // imported symbol (e.g. `const P = vec3(...)`) resolves | ||
| // against a stale consumer without this and could bind | ||
| // another module's like-named export. | ||
| self.current_mod_idx = mod_idx; | ||
| for global in &module.globals { | ||
| if let Some(init) = &global.init { | ||
| let mut ctx = | ||
|
|
@@ -1331,6 +1428,7 @@ impl WasmModuleEmitter { | |
| // Register class methods with the bridge and set up inheritance | ||
| for (mod_idx, (_, module)) in modules.iter().enumerate() { | ||
| self.func_map = self.module_func_maps[mod_idx].clone(); | ||
| self.current_mod_idx = mod_idx; // see the globals loop above | ||
| for class in &module.classes { | ||
| let class_name_id = self | ||
| .string_map | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,37 @@ impl<'a> FuncEmitCtx<'a> { | |
| Expr::Call { callee, args, .. } => { | ||
| // Check for method call patterns: obj.method(args) | ||
| if let Expr::PropertyGet { object, property } = callee.as_ref() { | ||
| // Namespace-import member call (`import * as W from "./mod"; | ||
| // W.fn(args)`): resolve to a DIRECT wasm call of the source | ||
| // module's function — the same lowering `fn(args)` gets via | ||
| // a named import. Without this the callee fell through to | ||
| // the class-dispatch fallback with an undefined receiver | ||
| // and silently returned undefined (never executing fn). | ||
| if let Expr::ExternFuncRef { name, .. } = object.as_ref() { | ||
| let key = ( | ||
| self.emitter.current_mod_idx, | ||
| format!("{}.{}", name, property), | ||
| ); | ||
| if let Some(&idx) = self.emitter.imported_ns_funcs.get(&key).copied().as_ref() { | ||
| for arg in args { | ||
| self.emit_expr(func, arg); | ||
| } | ||
| // Pad-up / drop-excess — see the FuncRef arm below (#183). | ||
| if let Some(&expected) = self.emitter.func_param_counts.get(&idx) { | ||
| for _ in args.len()..expected { | ||
| func.instruction(&Instruction::I64Const(TAG_UNDEFINED as i64)); | ||
| } | ||
| for _ in expected..args.len() { | ||
| func.instruction(&Instruction::Drop); | ||
| } | ||
| } | ||
| func.instruction(&Instruction::Call(idx)); | ||
| if self.emitter.void_funcs.contains(&idx) { | ||
| func.instruction(&Instruction::I64Const(TAG_UNDEFINED as i64)); | ||
| } | ||
| return true; | ||
| } | ||
| } | ||
|
Comment on lines
+20
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Support direct calls through imported function-valued globals. Direct imported-call lowering only consults compiled-function maps, even though exported arrow/function-valued constants are represented by closures in
📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| // console.log/warn/error | ||
| if let Expr::GlobalGet(_) = object.as_ref() { | ||
| match property.as_str() { | ||
|
|
@@ -147,10 +178,20 @@ impl<'a> FuncEmitCtx<'a> { | |
| Expr::ExternFuncRef { | ||
| name, return_type, .. | ||
| } => { | ||
| // Cross-module or FFI function call — look up by name. | ||
| // See FuncRef arm above for why both pad-up and drop-excess | ||
| // are required (#183). | ||
| if let Some(&idx) = self.emitter.func_name_map.get(name) { | ||
| // Cross-module or FFI function call. The consumer's | ||
| // own import table wins (resolved through re-export | ||
| // chains); the whole-program name map is only a | ||
| // fallback, since its bare-name keys collide across | ||
| // modules. See FuncRef arm above for why both pad-up | ||
| // and drop-excess are required (#183). | ||
| let consumer_key = | ||
| (self.emitter.current_mod_idx, name.clone()); | ||
| if let Some(&idx) = self | ||
| .emitter | ||
| .imported_func_indices | ||
| .get(&consumer_key) | ||
| .or_else(|| self.emitter.func_name_map.get(name)) | ||
| { | ||
| if let Some(&expected) = self.emitter.func_param_counts.get(&idx) { | ||
| for _ in args.len()..expected { | ||
| func.instruction(&Instruction::I64Const(TAG_UNDEFINED as i64)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 38945
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 588
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 13577
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 27170
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 12665
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 49322
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 6882
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 11283
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 9017
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 206
Route bitwise ops through the runtime helpers
I64TruncSatF64S/I32WrapI64does not match JSToInt32/ToUint32for out-of-range numbers, and it skipsToNumeric/BigInt handling. Usejs_dynamic_bitand/js_dynamic_bitor/js_dynamic_bitxor/js_dynamic_shr/js_dynamic_ushrincrates/perry-codegen-wasm/src/emit/binary.rsandjs_dynamic_bitnotincrates/perry-codegen-wasm/src/emit/expr/literals_vars.rs.📍 Affects 2 files
crates/perry-codegen-wasm/src/emit/binary.rs#L42-L63(this comment)crates/perry-codegen-wasm/src/emit/expr/literals_vars.rs#L387-L388🤖 Prompt for AI Agents