Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions crates/perry-codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,13 @@ serde_json.workspace = true
inkwell = { version = "0.9.0", default-features = false, features = ["llvm22-1", "target-x86", "target-aarch64"], optional = true }
llvm-sys = { version = "221", optional = true }

# LLVM's official Windows static archives use a private /MT CRT and bundle
# rpmalloc, while Rust's MSVC target uses /MD. Passing LLVM-owned allocations
# across that boundary crashes even when the static link succeeds (#7985).
# The same release ships a complete LLVM-C.dll/import-library pair; link that
# on Windows and let build.rs locate its import library under the pinned prefix.
# LLVM must use the same dynamic MSVC CRT (`/MD`) as Rust on Windows. The Perry
# toolchain archive is built that way, so embed LLVM in `perry.exe` instead of
# importing LLVM-C.dll. `force-static` makes this packaging contract explicit
# and prevents llvm-config from silently selecting a shared library.
[target.'cfg(windows)'.dependencies]
inkwell = { version = "0.9.0", default-features = false, features = ["llvm22-1-no-llvm-linking", "target-x86", "target-aarch64"], optional = true }
llvm-sys = { version = "221", features = ["no-llvm-linking"], optional = true }
inkwell = { version = "0.9.0", default-features = false, features = ["llvm22-1-force-static", "target-x86", "target-aarch64"], optional = true }
llvm-sys = { version = "221", features = ["force-static"], optional = true }

# Self dev-dependency (#7493). This is the whole mechanism by which the
# integration suites under `tests/` — which link this crate as an ordinary
Expand Down
15 changes: 0 additions & 15 deletions crates/perry-codegen/build.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
fn main() {
println!("cargo:rerun-if-env-changed=LLVM_SYS_221_PREFIX");

if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}

let prefix = std::env::var_os("LLVM_SYS_221_PREFIX").unwrap_or_else(|| {
panic!("LLVM_SYS_221_PREFIX must point to the LLVM 22 development archive on Windows")
});
let lib_dir = std::path::PathBuf::from(prefix).join("lib");
if !lib_dir.join("LLVM-C.lib").is_file() {
panic!("{} does not contain LLVM-C.lib", lib_dir.display());
}

println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=dylib=LLVM-C");
}
35 changes: 33 additions & 2 deletions crates/perry-codegen/src/codegen/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,10 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
let wf = llmod.define_function(&exported_wrap, DOUBLE, wrap_params);
let _ = wf.create_block("entry");
let blk = wf.block_mut(0).unwrap();
let target = scoped_fn_name(module_prefix, &f.name);
let target = func_names
.get(&f.id)
.cloned()
.unwrap_or_else(|| scoped_fn_name(module_prefix, &f.name));
let call_args: Vec<(LlvmType, String)> =
(0..arity).map(|i| (DOUBLE, format!("%a{}", i))).collect();
let call_args_ref: Vec<(LlvmType, &str)> =
Expand Down Expand Up @@ -1086,6 +1089,31 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
}
}

// Sub-bug C: the module namespace initializer reads a renamed
// export through its LOCAL getter name. Bundled code commonly
// hides generated locals behind public names, for example
// `var $SamplingFilter; export { $SamplingFilter as
// SamplingFilter }` or `var Tm; export { Tm as languages }`.
// The producer already has the public zero-argument getter (or
// undefined stub), but the initializer calls
// `perry_fn_<src>__<local>`. Emit a local-name forwarding getter
// for non-function aliases so that call has a definition.
if local != exported && !func_by_local_name.contains_key(local.as_str()) {
let local_target = format!("perry_fn_{}__{}", module_prefix, sanitize(local));
let exported_target = format!("perry_fn_{}__{}", module_prefix, sanitize(exported));
if local_target != exported_target
&& !llmod.has_function(&local_target)
&& llmod.has_function(&exported_target)
&& emitted_aliases.insert(local_target.clone())
{
let getter = llmod.define_function(&local_target, DOUBLE, vec![]);
let _ = getter.create_block("entry");
let blk = getter.block_mut(0).unwrap();
let value = blk.call(DOUBLE, &exported_target, &[]);
blk.ret(DOUBLE, &value);
}
}

// Sub-bug B: emit no-op wrapper for `local==exported` named
// exports where local isn't a HIR function and no wrapper
// is yet defined. Catches `import * as z; export { z };`
Expand Down Expand Up @@ -1136,7 +1164,10 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
let wf = llmod.define_function(&exported_wrap, DOUBLE, wrap_params);
let _ = wf.create_block("entry");
let blk = wf.block_mut(0).unwrap();
let target = scoped_fn_name(module_prefix, &f.name);
let target = func_names
.get(&f.id)
.cloned()
.unwrap_or_else(|| scoped_fn_name(module_prefix, &f.name));
let call_args: Vec<(LlvmType, String)> =
(0..arity).map(|i| (DOUBLE, format!("%a{}", i))).collect();
let call_args_ref: Vec<(LlvmType, &str)> =
Expand Down
39 changes: 38 additions & 1 deletion crates/perry-codegen/src/codegen/emission_order_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@

use crate::{compile_module, AppMetadata, CompileOptions};
use perry_hir::types::Type;
use perry_hir::{Class, Expr, Function, Module, ModuleInitKind, Param, Stmt};
use perry_hir::{Class, Export, Expr, Function, Module, ModuleInitKind, Param, Stmt};

/// Enough entries that an accidentally-sorted hash order is not a plausible
/// explanation for a green run.
Expand Down Expand Up @@ -161,6 +161,43 @@ fn ir(module: &Module) -> String {
.expect("LLVM IR should be UTF-8")
}

#[test]
fn renamed_non_function_exports_define_their_local_namespace_getters() {
let mut module = empty_module("renamed_export.ts");
module.exports.extend([
Export::Named {
local: "$SamplingFilter".to_string(),
exported: "SamplingFilter".to_string(),
},
Export::Named {
local: "Tm".to_string(),
exported: "languages".to_string(),
},
]);

let text = ir(&module);
for (local_symbol, public_symbol) in [
(
"perry_fn_renamed_export_ts___SamplingFilter",
"perry_fn_renamed_export_ts__SamplingFilter",
),
(
"perry_fn_renamed_export_ts__Tm",
"perry_fn_renamed_export_ts__languages",
),
] {
let marker = format!("define double @{local_symbol}()");
let start = text
.find(&marker)
.unwrap_or_else(|| panic!("missing local getter {local_symbol}\n{text}"));
let body = &text[start..text[start..].find("\n}").map_or(text.len(), |n| start + n)];
assert!(
body.contains(&format!("call double @{public_symbol}()")),
"local getter {local_symbol} must forward to {public_symbol}:\n{body}"
);
}
}

// ---------------------------------------------------------------------------
// Shape 1: `js_register_function_name` / `@.str.N`
// ---------------------------------------------------------------------------
Expand Down
30 changes: 24 additions & 6 deletions crates/perry-codegen/src/codegen/func_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use std::collections::HashMap;

use perry_hir::Module as HirModule;
use perry_hir::{Export, Module as HirModule};

// Collector and boxing-analysis walkers live in dedicated modules.

Expand Down Expand Up @@ -42,17 +42,35 @@ pub(crate) fn build_func_registry(hir: &HirModule, module_prefix: &str) -> FuncR
// referenced cross-module by their canonical `scoped_fn_name` and are unique
// per module, so they reserve that name first and never get suffixed.
let mut used_fn_symbols: HashMap<String, u32> = HashMap::new();
for f in &hir.functions {
if hir.exported_functions.iter().any(|(exp, _)| exp == &f.name) {
// Every public named export reserves its ABI symbol, including exported
// runtime values/closures that are served by a module-global getter.
// Otherwise an unrelated local function with the public name can claim
// the symbol before either a forwarding alias or value getter is emitted:
//
// function optionalKey(ast) { ... } // private AST helper
// const optionalKey2 = lambda(...); // public Schema wrapper
// export { optionalKey2 as optionalKey };
//
// Reserving only `exported_functions` covered direct FuncRef aliases but
// missed the closure-valued second shape (OpenCode/Effect Schema).
for export in &hir.exports {
if let Export::Named { exported, .. } = export {
used_fn_symbols
.entry(scoped_fn_name(module_prefix, &f.name))
.entry(scoped_fn_name(module_prefix, exported))
.or_insert(1);
}
}
for f in &hir.functions {
let base = scoped_fn_name(module_prefix, &f.name);
let is_exported = hir.exported_functions.iter().any(|(exp, _)| exp == &f.name);
let sym = if is_exported {
// A function owns its canonical local-name symbol only when that exact
// public name maps to this exact FuncId. Name-only matching is wrong
// for renamed exports and made a different local function overwrite
// the export target (OpenCode/Effect: `resolveAt2 as resolveAt`).
let owns_canonical_export = hir
.exported_functions
.iter()
.any(|(exp, func_id)| exp == &f.name && *func_id == f.id);
let sym = if owns_canonical_export {
base
} else {
let n = used_fn_symbols.entry(base.clone()).or_insert(0);
Expand Down
60 changes: 42 additions & 18 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,47 @@ pub(crate) fn static_method_registry_key(method_name: &str) -> String {
format!("__perry_static__{}", method_name)
}

/// Build the callable constructor table for names that are actually foreign
/// in this module. `CompileOptions::imported_classes` is intentionally wider
/// than the module's lexical imports: namespace imports and conservative
/// dispatch augmentation can add class metadata whose bare name is shadowed
/// by a local class. Letting that metadata enter `imported_class_ctors` makes
/// a synthesized `super()` on the local class call the unrelated foreign
/// constructor (for example, Effect's local `Node` calling tree-sitter's
/// `Node(internal, { ... })`). Keep constructor lookup consistent with
/// `class_table` / `class_ids`, where local declarations and their aliases
/// already take precedence.
fn build_imported_class_ctors(
hir: &HirModule,
imported_classes: &[ImportedClass],
) -> HashMap<String, ImportedCtor> {
let mut local_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
for class in &hir.classes {
local_names.insert(class.name.as_str());
local_names.extend(class.aliases.iter().map(String::as_str));
}

let mut ctors = HashMap::new();
for class in imported_classes {
let effective_name = class.local_alias.as_deref().unwrap_or(&class.name);
if local_names.contains(effective_name) {
continue;
}
// Match `class_table`'s first-writer-wins behavior when multiple
// imported classes contend for the same effective alias.
ctors
.entry(effective_name.to_string())
.or_insert_with(|| ImportedCtor {
symbol: format!("{}__{}_constructor", class.source_prefix, class.name),
param_count: class.constructor_param_count,
has_own_constructor: class.has_own_constructor,
has_instance_fields: class.has_instance_fields,
has_rest: class.constructor_has_rest,
});
}
ctors
}

/// Compile a Perry HIR module to an object file via LLVM IR.
///
/// CRITICAL (#686): `hir` MUST be `&HirModule` (shared reference), never
Expand Down Expand Up @@ -1812,24 +1853,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
class_keys_globals: class_keys_globals_map,
class_field_counts: class_field_counts_map,
class_init_chains: class_init_chains_map,
imported_class_ctors: opts
.imported_classes
.iter()
.map(|ic| {
let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name);
let ctor_name = format!("{}__{}_constructor", ic.source_prefix, ic.name);
(
effective_name.to_string(),
ImportedCtor {
symbol: ctor_name,
param_count: ic.constructor_param_count,
has_own_constructor: ic.has_own_constructor,
has_instance_fields: ic.has_instance_fields,
has_rest: ic.constructor_has_rest,
},
)
})
.collect(),
imported_class_ctors: build_imported_class_ctors(hir, &opts.imported_classes),
// Per-module i18n lowering context. Built from `opts.i18n_table`
// when i18n is configured; `None` otherwise. The
// `Expr::I18nString` lowering pulls the right translation row at
Expand Down
81 changes: 56 additions & 25 deletions crates/perry-codegen/src/codegen/module_globals_emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,34 @@ pub(crate) fn emit_module_globals(
// so method calls in other functions fall through to the generic
// dispatch instead of the class method registry.
let mut module_global_types: HashMap<u32, perry_hir::types::Type> = HashMap::new();
// Collect exported variable names so we can create external
// globals + getter functions for cross-module access.
let exported_var_names: std::collections::HashSet<String> =
hir.exported_objects.iter().cloned().collect();
// Collect the LOCAL bindings of exported variables so we can create
// external globals + getter functions for cross-module access.
//
// `exported_objects` intentionally contains both sides of a renamed
// value export: `export { Prototype2 as Prototype }` records
// `Prototype2` for storage and `Prototype` so the compile driver can
// classify the public import as a variable. Treating that flat list as
// local bindings is ambiguous when this module also declares an unrelated
// local named `Prototype`: codegen globalized that local and claimed the
// public `perry_fn_*__Prototype` getter before it reached `Prototype2`, so
// importers received the unrelated value. Derive storage ownership from
// `Export::Named.local`; the alias getter emitted below still uses
// `Export::Named.exported`, preserving the public ABI.
let exported_object_names: std::collections::HashSet<&str> =
hir.exported_objects.iter().map(String::as_str).collect();
let exported_var_names: std::collections::HashSet<String> = hir
.exports
.iter()
.filter_map(|export| match export {
perry_hir::Export::Named { local, exported }
if exported_object_names.contains(local.as_str())
|| exported_object_names.contains(exported.as_str()) =>
{
Some(local.clone())
}
_ => None,
})
.collect();
// #6649: module-level array-destructuring declarations (`var [Prime, Size]
// = [BigInt(...), BigInt(...)]` — TypeBox's FNV-1a table in the pi bundle)
// lower their leaf `Stmt::Let`s inside the iterator-protocol `Stmt::Try`
Expand Down Expand Up @@ -301,14 +325,25 @@ pub(crate) fn emit_module_globals(
// emitting a getter here on top would be a redef and is
// semantically wrong (it'd return the closure value instead
// of invoking it).
let is_function_alias = hir.exported_functions.iter().any(|(exp, _)| exp == name);
let is_function_alias = hir.exported_functions.iter().any(|(exp, _)| exp == name)
|| hir.exports.iter().any(|export| match export {
perry_hir::Export::Named { local, exported } if local == name => hir
.exported_functions
.iter()
.any(|(function_export, _)| function_export == exported),
_ => false,
});
if is_exported && !is_also_function && !is_function_alias {
let fn_name = format!("perry_fn_{}__{}", module_prefix, sanitize(name),);
let getter = llmod.define_function(&fn_name, DOUBLE, vec![]);
let _ = getter.create_block("entry");
let blk = getter.block_mut(0).unwrap();
let val = blk.load(DOUBLE, &format!("@{}", global_name));
blk.ret(DOUBLE, &val);
let public_names: std::collections::BTreeSet<&str> = hir
.exports
.iter()
.filter_map(|export| match export {
perry_hir::Export::Named { local, exported } if local == name => {
Some(exported.as_str())
}
_ => None,
})
.collect();

// #460: also emit a duplicate getter under any renamed
// export targeting this local. `export { _await as await }`
Expand All @@ -319,21 +354,17 @@ pub(crate) fn emit_module_globals(
// returns; callers that invoke it as a function get the
// closure handle (matching status quo for non-renamed
// `export const f = aFunctionRef` exports).
for export in &hir.exports {
if let perry_hir::Export::Named { local, exported } = export {
if local == name && exported != name {
let alias_fn =
format!("perry_fn_{}__{}", module_prefix, sanitize(exported));
if alias_fn == fn_name {
continue;
}
let g = llmod.define_function(&alias_fn, DOUBLE, vec![]);
let _ = g.create_block("entry");
let b = g.block_mut(0).unwrap();
let v = b.load(DOUBLE, &format!("@{}", global_name));
b.ret(DOUBLE, &v);
}
for public_name in public_names {
let getter_name =
format!("perry_fn_{}__{}", module_prefix, sanitize(public_name));
if llmod.has_function(&getter_name) {
continue;
}
let getter = llmod.define_function(&getter_name, DOUBLE, vec![]);
let _ = getter.create_block("entry");
let blk = getter.block_mut(0).unwrap();
let val = blk.load(DOUBLE, &format!("@{}", global_name));
blk.ret(DOUBLE, &val);
}
}
}
Expand Down
Loading