Skip to content
Merged
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
3 changes: 3 additions & 0 deletions changelog.d/9133-opencode-source-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Full-source OpenCode builds now preserve namespace and type-only import semantics, class-expression and closure initialization, iterator/class runtime behavior, and Windows archive linking across the complete TypeScript dependency graph instead of requiring a pre-bundled JavaScript input.
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/codegen/ctor_arity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub(super) fn synthesized_ctor_param_count(
while let Some(pname) = cur {
let imported_ctor_params = imported_classes
.iter()
.find(|i| i.local_alias.as_deref().unwrap_or(&i.name) == pname.as_str())
.find(|i| i.effective_name() == pname)
.map(|ic| ic.constructor_param_count)
.unwrap_or(0);
if let Some(pclass) = class_table.get(pname.as_str()) {
Expand Down
36 changes: 23 additions & 13 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,25 @@ pub(super) fn compile_module_entry(
(cn, len, prefix.clone())
})
.collect();
// `PERRY_DEBUG_INIT` is a startup-order diagnostic, so keep all of its
// emitted code in the entry object. The old implementation put a
// `puts("INIT: <prefix>")` in every non-entry module body, which made
// enabling the diagnostic invalidate every object in a large source
// graph. OpenCode's 7k-module graph consequently needed a full LLVM
// rebuild just to identify one failing initializer. Parallel to the
// eager call list below, these constants let the entry print the next
// initializer before dispatching it while every dependency object
// remains byte-for-byte reusable.
let debug_init_chain: Vec<String> = if std::env::var_os("PERRY_DEBUG_INIT").is_some() {
let constants = non_entry_module_prefixes
.iter()
.map(|prefix| llmod.add_string_constant(&format!("INIT: {}\0", prefix)).0)
.collect();
llmod.declare_function("puts", I32, &[PTR]);
constants
} else {
Vec::new()
};
let main = if is_dylib {
llmod.define_function("perry_module_init", VOID, vec![])
} else {
Expand Down Expand Up @@ -681,10 +700,13 @@ pub(super) fn compile_module_entry(
],
);
}
for prefix in non_entry_module_prefixes {
for (index, prefix) in non_entry_module_prefixes.iter().enumerate() {
if cross_module.deferred_module_prefixes.contains(prefix) {
continue;
}
if let Some(const_name) = debug_init_chain.get(index) {
blk.call_void("puts", &[(PTR, &format!("@{}", const_name))]);
}
blk.call_void(&format!("{}__init", prefix), &[]);
}
}
Expand Down Expand Up @@ -1372,15 +1394,6 @@ pub(super) fn compile_module_entry(
// only the wrapper above ever calls it, both within this module
// and across modules via the wrapper's external symbol.
let init_name = init_body_name;
// Debug: emit puts("INIT: <prefix>") at the top of each module init
let debug_init_const = if std::env::var("PERRY_DEBUG_INIT").is_ok() {
let debug_msg = format!("INIT: {}\0", module_prefix);
let (const_name, _) = llmod.add_string_constant(&debug_msg);
llmod.declare_function("puts", I32, &[PTR]);
Some(const_name)
} else {
None
};
let ic_base = llmod.ic_counter;
let buffer_alias_base = llmod.buffer_alias_counter;
let init_fn = llmod.define_function(&init_name, VOID, vec![]);
Expand All @@ -1400,9 +1413,6 @@ pub(super) fn compile_module_entry(
let _ = init_fn.create_block("entry");
{
let blk = init_fn.block_mut(0).unwrap();
if let Some(ref cname) = debug_init_const {
blk.call_void("puts", &[(PTR, &format!("@{}", cname))]);
}
if write_barriers_enabled() {
blk.call_void("js_gc_write_barriers_emitted", &[(I32, "1")]);
}
Expand Down
12 changes: 6 additions & 6 deletions crates/perry-codegen/src/codegen/method_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ pub(crate) fn build_method_names(
// registry and pre-declare them as extern LLVM functions so the
// linker can resolve cross-module method calls.
for ic in imported_classes {
let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name);
let effective_name = ic.effective_name();
// Skip if locally defined — local methods take precedence.
if hir.classes.iter().any(|c| c.name == *effective_name) {
if hir.classes.iter().any(|c| c.name == effective_name) {
continue;
}
let src = &ic.source_prefix;
Expand All @@ -192,7 +192,7 @@ pub(crate) fn build_method_names(
sanitize_member(method_name),
);
method_names
.entry((effective_name.to_string(), method_name.clone()))
.entry((effective_name.clone(), method_name.clone()))
.or_insert_with(|| llvm_fn.clone());

// Declare extern: `double method(double this, double arg0, …)`.
Expand Down Expand Up @@ -247,7 +247,7 @@ pub(crate) fn build_method_names(
&format!("__get_{}", inner_fn_name),
);
method_names
.entry((effective_name.to_string(), format!("__get_{}", prop)))
.entry((effective_name.clone(), format!("__get_{}", prop)))
.or_insert_with(|| llvm_fn.clone());
// Getters take only `this` (NaN-boxed double) and return double.
llmod.declare_function(&llvm_fn, DOUBLE, &[DOUBLE]);
Expand All @@ -263,7 +263,7 @@ pub(crate) fn build_method_names(
&format!("__set_{}", inner_fn_name),
);
method_names
.entry((effective_name.to_string(), format!("__set_{}", prop)))
.entry((effective_name.clone(), format!("__set_{}", prop)))
.or_insert_with(|| llvm_fn.clone());
// Setters take `this` plus the new value, both NaN-boxed
// doubles, and return double (the assigned value).
Expand Down Expand Up @@ -301,7 +301,7 @@ pub(crate) fn build_method_names(
)
};
method_names
.entry((effective_name.to_string(), static_method_registry_key(sm)))
.entry((effective_name.clone(), static_method_registry_key(sm)))
.or_insert_with(|| llvm_fn.clone());
// Declare conservatively with 6 double params; LLVM's direct-call
// resolution doesn't require an exact arity match for declarations.
Expand Down
85 changes: 55 additions & 30 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ pub(crate) use helpers::{
module_callable_count, set_full_outline_ic, write_barriers_enabled,
};
pub use opts::{
AppMetadata, CompileOptions, ExportedObjectLiteralCapability, FpContractMode, ImportedClass,
namespace_member_class_key, namespace_member_func_key, namespace_member_var_key, AppMetadata,
CompileOptions, ExportedObjectLiteralCapability, FpContractMode, ImportedClass,
ImportedObjectLiteral, ImportedObjectLiteralMethod, NamespaceEntry, NamespaceEntryKind,
ObjectLiteralMethodCandidate, ShortSpreadMethodCandidate,
};
Expand Down Expand Up @@ -597,10 +598,26 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
let class_id = ic
.source_class_id
.unwrap_or_else(|| next_class_id + (idx as u32));
let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name);
let effective_name = ic.effective_name();
let exported_name = ic.local_alias.as_deref().unwrap_or(&ic.name);

// Namespace member class identity must be scoped to the namespace.
// A flat `class_ids[member]` lookup lets a class exported by one
// namespace hijack an equal-named value/function in another (Effect's
// SchemaAST.Boolean class versus Schema.Boolean schema value). The
// driver already resolved every member to its origin prefix, and an
// ImportedClass carries that same prefix plus its consumer-visible
// alias, so record the exact `(namespace, member)` identity here.
for ((namespace, member), source_prefix) in &opts.namespace_member_prefixes {
if source_prefix == &ic.source_prefix && member == exported_name {
class_ids
.entry(namespace_member_class_key(namespace, member))
.or_insert(class_id);
}
}

// Skip if already defined locally (local definition takes precedence).
if class_table.contains_key(effective_name) {
if class_table.contains_key(&effective_name) {
// Issue #26 / #321: a locally-shadowed import is still needed for
// *parent resolution* of OTHER imported classes. Effect's
// ParseResult.ts declares its own local `class Type`
Expand All @@ -614,7 +631,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// can find it WITHOUT polluting the name-keyed dispatch maps.
if !ic.field_names.is_empty() || ic.parent_name.is_some() {
shadowed_parent_stubs.push((
effective_name.to_string(),
effective_name.clone(),
ic.source_prefix.clone(),
ic.parent_name.clone(),
ic.field_names
Expand Down Expand Up @@ -644,11 +661,11 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// must agree, otherwise the method registry builds symbols mixing
// the FIRST writer's methods with the LAST writer's prefix +
// canonical name, producing fnames the linker can't resolve.
class_ids
.entry(effective_name.to_string())
.or_insert(class_id);
// Also register the canonical name if aliased.
if ic.local_alias.is_some() && !class_ids.contains_key(&ic.name) {
class_ids.entry(effective_name.clone()).or_insert(class_id);
// A lexical alias also exposes the canonical binding for legacy
// source-name lookups. Namespace members do not: `ns.Service` must
// never claim the unrelated bare `Service` binding in this module.
if ic.namespace.is_none() && ic.local_alias.is_some() && !class_ids.contains_key(&ic.name) {
class_ids.insert(ic.name.clone(), class_id);
}

Expand Down Expand Up @@ -703,7 +720,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// their names here keeps dispatch and field inference conservative.
let stub = perry_hir::Class {
id: 0, // imported — no local ClassId
name: effective_name.to_string(),
name: effective_name.clone(),
// #6812: width hints don't cross module metadata; imported stubs
// fall back to runtime learned sizing.
alloc_width_hint: 0,
Expand All @@ -713,7 +730,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
specialized_from: None,
type_params: Vec::new(),
extends: None,
extends_name: ic.parent_name.clone(),
extends_name: ic.effective_parent_name(),
native_extends: None,
extends_expr: None,
heritage_lexically_shadowed: false,
Expand Down Expand Up @@ -979,8 +996,8 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// method-registry loop below recover the source name.
let mut imported_class_source_name: HashMap<String, String> = HashMap::new();
for ic in &opts.imported_classes {
let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name);
if hir.classes.iter().any(|c| c.name == *effective_name) {
let effective_name = ic.effective_name();
if hir.classes.iter().any(|c| c.name == effective_name) {
continue;
}
// Refs #665: first-writer-wins to match `class_table`'s
Expand All @@ -993,11 +1010,11 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// method symbols mangled under the wrong class — the linker can't
// resolve them and the build fails with "undefined value".
imported_class_prefix
.entry(effective_name.to_string())
.entry(effective_name.clone())
.or_insert_with(|| ic.source_prefix.clone());
if effective_name != ic.name {
imported_class_source_name
.entry(effective_name.to_string())
.entry(effective_name)
.or_insert_with(|| ic.name.clone());
}
}
Expand Down Expand Up @@ -1619,13 +1636,15 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
}
}
for ic in &opts.imported_classes {
let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name).to_string();
let effective_name = ic.effective_name();
for (i, mname) in ic.method_names.iter().enumerate() {
// Default to 0 if the source side hasn't populated method_param_counts
// yet (legacy ImportedClass with no parallel Vec). 0 means "no padding".
let count = ic.method_param_counts.get(i).copied().unwrap_or(0);
// Register under the canonical class name and the local alias if any.
method_param_counts.insert((ic.name.clone(), mname.clone()), count);
if ic.namespace.is_none() {
method_param_counts.insert((ic.name.clone(), mname.clone()), count);
}
if effective_name != ic.name {
method_param_counts.insert((effective_name.clone(), mname.clone()), count);
}
Expand All @@ -1635,7 +1654,9 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// args either dropped or silently spread into the next slot —
// `c.cmd("SET", "k", "v")` reached the callee as `args = "k"`.
if ic.method_has_rest.get(i).copied().unwrap_or(false) {
method_has_rest.insert((ic.name.clone(), mname.clone()), true);
if ic.namespace.is_none() {
method_has_rest.insert((ic.name.clone(), mname.clone()), true);
}
if effective_name != ic.name {
method_has_rest.insert((effective_name.clone(), mname.clone()), true);
}
Expand All @@ -1646,7 +1667,9 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
.copied()
.unwrap_or(false)
{
method_has_synthetic_arguments.insert((ic.name.clone(), mname.clone()), true);
if ic.namespace.is_none() {
method_has_synthetic_arguments.insert((ic.name.clone(), mname.clone()), true);
}
if effective_name != ic.name {
method_has_synthetic_arguments
.insert((effective_name.clone(), mname.clone()), true);
Expand All @@ -1668,12 +1691,16 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
for (i, method_name) in ic.static_method_names.iter().enumerate() {
let registry_name = static_method_registry_key(method_name);
let count = ic.static_method_param_counts.get(i).copied().unwrap_or(0);
method_param_counts.insert((ic.name.clone(), registry_name.clone()), count);
if ic.namespace.is_none() {
method_param_counts.insert((ic.name.clone(), registry_name.clone()), count);
}
if effective_name != ic.name {
method_param_counts.insert((effective_name.clone(), registry_name.clone()), count);
}
if ic.static_method_has_rest.get(i).copied().unwrap_or(false) {
method_has_rest.insert((ic.name.clone(), registry_name.clone()), true);
if ic.namespace.is_none() {
method_has_rest.insert((ic.name.clone(), registry_name.clone()), true);
}
if effective_name != ic.name {
method_has_rest.insert((effective_name.clone(), registry_name.clone()), true);
}
Expand All @@ -1684,8 +1711,10 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
.copied()
.unwrap_or(false)
{
method_has_synthetic_arguments
.insert((ic.name.clone(), registry_name.clone()), true);
if ic.namespace.is_none() {
method_has_synthetic_arguments
.insert((ic.name.clone(), registry_name.clone()), true);
}
if effective_name != ic.name {
method_has_synthetic_arguments
.insert((effective_name.clone(), registry_name), true);
Expand Down Expand Up @@ -2069,11 +2098,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// The tower subset is producer-authored because only the defining module
// can see enough of the body to price its additional keys-token check.
for imported in &opts.imported_classes {
let effective_name = imported
.local_alias
.as_deref()
.unwrap_or(&imported.name)
.to_string();
let effective_name = imported.effective_name();
if hir.classes.iter().any(|class| class.name == effective_name) {
continue;
}
Expand Down Expand Up @@ -2413,10 +2438,10 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
.imported_classes
.iter()
.map(|ic| {
let effective_name = ic.local_alias.as_deref().unwrap_or(&ic.name);
let effective_name = ic.effective_name();
let ctor_name = format!("{}__{}_constructor", ic.source_prefix, ic.name);
(
effective_name.to_string(),
effective_name,
ImportedCtor {
symbol: ctor_name,
param_count: ic.constructor_param_count,
Expand Down
Loading
Loading