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
7 changes: 7 additions & 0 deletions changelog.d/8032-native-value-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Native values: expose the first stable `perry/native` profile (#6827)

Applications can now import fixed-width scalar markers, verified `pod<T>`
records, compile-time layout intrinsics, `PodView<T>`, and `NativeArena` from
`perry/native`. Named import aliases reuse Perry's existing verifier-backed
native layout pipeline, generated project type stubs include the module, and
`PodView.length` reports the checked record count at runtime.
7 changes: 7 additions & 0 deletions crates/perry-api-manifest/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub const NATIVE_MODULES: &[&str] = &[
"perry/i18n", // internationalization runtime
"worker_threads", // (Node builtin) OS-thread workers
"perry/thread", // perry-native threading (parallelMap/spawn)
"perry/native", // exact native layouts and arena-backed POD values
// `perry/gc` — explicit GC control (collect / minor / idleHint).
// Served entirely by perry-runtime; a no-op-style Perry-native
// surface like `perry/thread` (doesn't resolve under Node/Bun).
Expand Down Expand Up @@ -254,6 +255,7 @@ pub const RUNTIME_ONLY_MODULES: &[&str] = &[
"perry/widget",
"perry/i18n",
"perry/thread",
"perry/native",
"perry/gc",
"perry/media",
"perry/audio",
Expand Down Expand Up @@ -381,6 +383,11 @@ const fn method_sig_entry(
}
}

const fn intrinsic(mut entry: ApiEntry) -> ApiEntry {
entry.source = ApiSource::Intrinsic;
entry
}

const fn property(module: &'static str, name: &'static str) -> ApiEntry {
ApiEntry {
module,
Expand Down
28 changes: 28 additions & 0 deletions crates/perry-api-manifest/src/entries/part_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1488,6 +1488,34 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[
&[p_any("p0")],
TypeSpec::Promise,
),
// #6827 — public native layout profile. The three layout helpers are
// compiler intrinsics (their generic POD type is erased before runtime);
// NativeArena is a runtime-backed constructor value.
intrinsic(method_sig(
"perry/native",
"sizeof",
false,
None,
&[],
TypeSpec::Number,
)),
intrinsic(method_sig(
"perry/native",
"alignof",
false,
None,
&[],
TypeSpec::Number,
)),
intrinsic(method_sig(
"perry/native",
"offsetof",
false,
None,
&[p_str("field")],
TypeSpec::Number,
)),
property("perry/native", "NativeArena"),
// `perry/gc` — explicit GC control. `collect()` runs a full collection
// (same as the global `gc()`), `minor()` runs a nursery-only collection
// and returns the freed byte count, `idleHint()` runs a threshold-due
Expand Down
22 changes: 22 additions & 0 deletions crates/perry-api-manifest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,28 @@ mod tests {
assert_eq!(bare.is_some(), prefixed.is_some());
}

#[test]
fn perry_native_public_value_surface_is_manifested() {
for name in ["sizeof", "alignof", "offsetof", "NativeArena"] {
assert!(
module_has_public_named_export("perry/native", name),
"perry/native missing public value export {name}"
);
}

for name in ["sizeof", "alignof", "offsetof"] {
let entry = module_has_symbol("perry/native", name)
.unwrap_or_else(|| panic!("perry/native missing intrinsic {name}"));
assert!(matches!(entry.kind, ApiKind::Method { .. }));
assert_eq!(entry.source, ApiSource::Intrinsic, "{name}");
assert_eq!(entry.returns, TypeSpec::Number, "{name}");
}

let arena = module_has_symbol("perry/native", "NativeArena")
.expect("perry/native missing NativeArena");
assert!(matches!(arena.kind, ApiKind::Property));
}

/// `dotenv.parse` regression guard.
///
/// `js_dotenv_parse` has always been implemented and declared to codegen,
Expand Down
15 changes: 15 additions & 0 deletions crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}

match expr {
Expr::PropertyGet {
object, property, ..
} if property == "length"
&& matches!(object.as_ref(), Expr::LocalGet(id) if ctx.pod_views.contains_key(id)) =>
{
// A NativePodView is a verifier-owned GC object, not a normal JS
// object with a property table. Read its record count through the
// validating runtime helper so the public `PodView.length`
// declaration has the promised observable behavior and disposed
// owners are still rejected.
let view = lower_expr(ctx, object)?;
Ok(ctx
.block()
.call(DOUBLE, "js_native_pod_view_length", &[(DOUBLE, &view)]))
}
Expr::PropertyGet {
object, property, ..
} if matches!(object.as_ref(), Expr::LocalGet(id)
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/strings_part2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
module.declare_function("js_native_arena_alloc", I64, &[I64]);
module.declare_function("js_native_arena_view", I64, &[I64, I32, I64, I64]);
module.declare_function("js_native_pod_view", I64, &[I64, I64, I64, I64, I64, I64]);
module.declare_function("js_native_pod_view_length", DOUBLE, &[DOUBLE]);
module.declare_function("js_native_abi_check_pod_view_data_ptr", PTR, &[DOUBLE, I64]);
module.declare_function(
"js_native_abi_check_pod_view_record_count",
Expand Down
14 changes: 14 additions & 0 deletions crates/perry-codegen/src/stmt/let_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,20 @@ pub(crate) fn lower_let(
count_source: pod_view_count_source(ctx, count),
},
);
} else if let perry_hir::Expr::LocalGet(source_id) = init_expr {
// An immutable local-to-local assignment preserves the
// exact NativePodView value. Carry its provenance to the
// alias so `.length` and native pod+count boundaries keep
// using validating helpers instead of the object PIC.
if let Some(source_view) = ctx.pod_views.get(source_id).cloned() {
ctx.pod_views.insert(
id,
crate::native_value::PodViewLocal {
view_slot: slot.clone(),
..source_view
},
);
}
}
}
v
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,44 @@ fn native_pod_view_explicit_public_type_lowers_without_left_hand_annotation() {
);
}

#[test]
fn native_pod_view_length_survives_immutable_local_alias() {
let packet_ty = pod_type(&[
("tag", Type::Named("PerryU32".to_string())),
("gain", Type::Named("PerryF32".to_string())),
]);
let view_ty = pod_view_type(packet_ty);
let module = module(
"native_public_pod_view_length_alias.ts",
vec![
native_arena_owner_let(1, "owner", int(4096), false),
native_pod_view_let(2, "direct", view_ty.clone(), 1, int(0), int(8)),
Stmt::Let {
id: 3,
name: "alias".to_string(),
ty: view_ty,
mutable: false,
init: Some(local(2)),
},
Stmt::Return(Some(Expr::PropertyGet {
object: Box::new(local(3)),
property: "length".to_string(),
byte_offset: 0,
})),
],
);

let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap();
assert!(
ir.contains("call double @js_native_pod_view_length"),
"an immutable PodView alias must use the validating length helper:\n{ir}"
);
assert!(
!ir.contains("call double @js_object_get_field_ic_miss"),
"a PodView alias must not enter the ordinary object-property PIC:\n{ir}"
);
}

#[test]
fn native_pod_view_embedded_type_survives_any_expected_type() {
let packet_ty = pod_type(&[
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-hir/src/destructuring/var_decl/type_infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::types::Type;
use swc_ecma_ast as ast;

use crate::lower::LoweringContext;
use crate::lower_types::{extract_ts_type, infer_type_from_expr};
use crate::lower_types::{extract_ts_type, extract_ts_type_with_ctx, infer_type_from_expr};

/// #7547: the type of a declaration in a **`for` initializer**
/// (`for (let j = 0; …)`).
Expand Down Expand Up @@ -84,7 +84,7 @@ pub(crate) fn infer_decl_type(
let mut ty = ident
.type_ann
.as_ref()
.map(|ann| extract_ts_type(&ann.type_ann))
.map(|ann| extract_ts_type_with_ctx(&ann.type_ann, Some(ctx)))
.unwrap_or_else(|| {
// No type annotation: try local inference from initializer
if let Some(init_expr) = &decl.init {
Expand Down
29 changes: 29 additions & 0 deletions crates/perry-hir/src/lower/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ impl LoweringContext {
pending_body_enums: Vec::new(),
interfaces: Vec::new(),
type_aliases: Vec::new(),
native_profile_type_aliases: HashMap::new(),
immutable_locals: HashSet::new(),
interface_source_keys: std::collections::HashMap::new(),
interface_object_types: std::collections::HashMap::new(),
Expand Down Expand Up @@ -1288,6 +1289,34 @@ impl LoweringContext {
})
}

pub(crate) fn register_native_profile_type_alias(
&mut self,
local_name: String,
imported_name: &str,
) {
let canonical = match imported_name {
"u32" => "PerryU32",
"u64" => "PerryU64",
"usize" => "PerryUSize",
"i32" => "PerryI32",
"i64" => "PerryI64",
"f32" => "PerryF32",
"f64" => "PerryF64",
"pod" => "PerryPod",
"PodView" => "PerryPodView",
"NativeArena" => "NativeArena",
_ => return,
};
self.native_profile_type_aliases
.insert(local_name, canonical.to_string());
}

pub(crate) fn resolve_native_profile_type_alias(&self, name: &str) -> Option<&str> {
self.native_profile_type_aliases
.get(name)
.map(String::as_str)
}

/// #wall5: shadow a native-module name for the current scope IF it is a
/// registered module (so a local/param of that name resolves as a value, not
/// the module). No-op for non-module names. Restore with
Expand Down
49 changes: 36 additions & 13 deletions crates/perry-hir/src/lower/expr_call/intrinsics/native_arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,26 @@ use crate::lower_types::extract_ts_type_with_ctx;
use super::super::super::{lower_expr, LoweringContext};

fn pod_layout_intrinsic_is_shadowed(ctx: &LoweringContext, name: &str) -> bool {
ctx.lookup_local(name).is_some()
|| ctx.lookup_func(name).is_some()
|| ctx.lookup_imported_func(name).is_some()
ctx.shadows_unqualified_global(name)
}

fn pod_layout_intrinsic_name(ctx: &LoweringContext, local_name: &str) -> Option<&'static str> {
if matches!(local_name, "sizeof" | "alignof" | "offsetof")
&& !pod_layout_intrinsic_is_shadowed(ctx, local_name)
{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return match local_name {
"sizeof" => Some("sizeof"),
"alignof" => Some("alignof"),
"offsetof" => Some("offsetof"),
_ => None,
};
}
match ctx.lookup_native_module(local_name) {
Some(("perry/native", Some("sizeof"))) => Some("sizeof"),
Some(("perry/native", Some("alignof"))) => Some("alignof"),
Some(("perry/native", Some("offsetof"))) => Some("offsetof"),
_ => None,
}
}

fn explicit_single_type_arg(
Expand Down Expand Up @@ -75,13 +92,10 @@ pub(crate) fn try_pod_layout_constants(
let ast::Expr::Ident(ident) = callee_expr.as_ref() else {
return Ok(None);
};
let name = ident.sym.as_ref();
if !matches!(name, "sizeof" | "alignof" | "offsetof") {
let local_name = ident.sym.as_ref();
let Some(name) = pod_layout_intrinsic_name(ctx, local_name) else {
return Ok(None);
}
if pod_layout_intrinsic_is_shadowed(ctx, name) {
return Ok(None);
}
};
if has_spread {
crate::lower_bail!(call.span, "{}(...) does not accept spread arguments", name);
}
Expand Down Expand Up @@ -167,6 +181,15 @@ fn native_arena_global_is_shadowed(ctx: &LoweringContext) -> bool {
|| ctx.lookup_class("NativeArena").is_some()
}

fn is_native_arena_constructor_ident(ctx: &LoweringContext, ident: &ast::Ident) -> bool {
let name = ident.sym.as_ref();
(name == "NativeArena" && !native_arena_global_is_shadowed(ctx))
|| matches!(
ctx.lookup_native_module(name),
Some(("perry/native", Some("NativeArena")))
)
}

fn native_memory_global_is_shadowed(ctx: &LoweringContext) -> bool {
ctx.lookup_local("NativeMemory").is_some()
|| ctx.lookup_func("NativeMemory").is_some()
Expand Down Expand Up @@ -242,9 +265,8 @@ fn is_native_arena_alloc_call(ctx: &LoweringContext, call: &ast::CallExpr) -> bo
let ast::Expr::Member(member) = callee_expr.as_ref() else {
return false;
};
matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if obj.sym.as_ref() == "NativeArena")
matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if is_native_arena_constructor_ident(ctx, obj))
&& matches!(&member.prop, ast::MemberProp::Ident(prop) if prop.sym.as_ref() == "alloc")
&& !native_arena_global_is_shadowed(ctx)
}

fn native_arena_owner_type(ty: &crate::types::Type) -> bool {
Expand Down Expand Up @@ -286,8 +308,9 @@ pub(crate) fn try_native_arena_public_api(
};
let method = prop.sym.as_ref();

if matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if obj.sym.as_ref() == "NativeArena") {
if method != "alloc" || native_arena_global_is_shadowed(ctx) {
if matches!(member.obj.as_ref(), ast::Expr::Ident(obj) if is_native_arena_constructor_ident(ctx, obj))
{
if method != "alloc" {
return Ok(None);
}
if has_spread {
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-hir/src/lower/lower_module_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,10 @@ pub fn lower_module_full(
// same `__perry_cap_*` symbols.
let mut ctx =
LoweringContext::with_class_id_start_salted(source_file_path, name, start_class_id);
// Static imports are hoisted. Register `perry/native` type and value
// aliases before any pre-pass extracts annotations or lowers expressions,
// including when the declaration appears after its first source use.
module_decl::native_profile_import::pre_register_native_profile_imports(&mut ctx, ast_module);
// #6812 (w16): scan the module lowering actually consumes (post-fold) for
// constant-bounded dynamic-key builder widths; `lower_object` attaches
// them to the per-site empty-literal classes as alloc_width_hint.
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-hir/src/lower/lowering_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ pub struct LoweringContext {
pub(crate) interfaces: Vec<(String, InterfaceId)>,
/// Type aliases: name -> (id, type_params, aliased_type)
pub(crate) type_aliases: Vec<(String, TypeAliasId, Vec<TypeParam>, Type)>,
/// Type names imported from `perry/native`: local name -> the canonical
/// compiler marker (`u32` -> `PerryU32`, `pod` -> `PerryPod`, ...).
///
/// Native modules do not have source HIR declarations, so type-only
/// imports are otherwise erased before type extraction. Keeping this
/// small alias table lets the public module spellings reuse the existing
/// native-representation and POD pipeline without teaching every
/// downstream pass a second vocabulary.
pub(crate) native_profile_type_aliases: HashMap<String, String>,
/// Issue #179 typed-parse: interface name → field names in AST
/// source order. Populated alongside `interfaces` during
/// `lower_interface_decl`. `ObjectType::properties` is a HashMap
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-hir/src/lower/module_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::ir::*;
// Topical sub-modules extracted from this file (issue #1435 — pure code move).
mod namespace;
mod native_default_import;
pub(super) mod native_profile_import;

// Re-export moved items so existing `crate::...` / `super::*` call paths keep
// resolving. `lower_namespace_as_class` is also called from `lower/stmt.rs`.
Expand Down Expand Up @@ -125,6 +126,7 @@ pub(crate) fn lower_module_decl(
// method registry — `obj.method()` worked only via the
// CLASS_VTABLE_REGISTRY runtime fallback (#392 followup) and
// `typeof obj.method` returned `"undefined"`. Issue #446.
native_profile_import::register_native_profile_type_imports(ctx, &source, import_decl);
if import_decl.type_only && is_native {
return Ok(());
}
Expand Down
Loading
Loading