You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Perry already has two strong halves of a native-value story, but they are not
yet one stable user-facing contract.
On the application side, Perry intentionally accepts ordinary TypeScript: number, objects, arrays, interfaces, classes, npm modules, and Node-compatible
APIs. That compatibility should remain the default.
On the compiler and native-library side, Perry already models explicit native
representations:
the native-representation pipeline has I32, I64, U32, U64, USize, F32, F64, BufferLen, native handles, POD records, and verified
materialization boundaries;
the native-library manifest accepts typed ABI descriptors and C-layout POD
records;
generated declarations currently expose internal-looking types such as PerryI32, PerryU64, PerryF32, and PerryPod<T>;
NativeArena, typed views, sizeof<T>(), alignof<T>(), and offsetof<T>() already exist; and
The missing piece is a documented source-level contract that an application or
library author can intentionally use.
Today, a developer can see that Perry internally understands exact widths and
POD layouts, but there is no stable answer to questions such as:
How do I declare that a value is a u16, i32, u64, or f32, rather
than an ordinary JavaScript number?
Which conversions are checked, lossy, wrapping, or rejected?
Can an exact-width value remain native across local variables, calls,
arrays, records, WebAssembly, and native-library boundaries?
How do I define one C-layout record in TypeScript and have Perry verify the
same layout against a native-library manifest?
What happens when i64 or u64 crosses a JavaScript-compatible boundary
where a number cannot represent it exactly?
Which parts are stable language/API guarantees and which parts are compiler
optimizations that may safely fall back to boxed JavaScript values?
Without that contract, native packages either duplicate representation
information in a manifest, pass data through generic number/object values, or
drop down to typed arrays and manual offsets. The compiler can optimize some of
those cases, but the author cannot state and rely on an exact native contract.
A concrete example I would like Perry to support is:
The goal is not to make ordinary Perry code look like systems code. The goal is
to let TypeScript code become explicit at the small number of boundaries where
width, range, layout, ABI, or measured performance is part of correctness.
Proposed solution
Stabilize an opt-in Perry native-value profile exposed through a normal,
valid TypeScript module such as perry/native.
This should build on the native representation and ABI work Perry already has,
not introduce a separate parser, file extension, or second compiler pipeline.
Ordinary .ts/.tsx code and ordinary number semantics remain unchanged.
1. Public native scalar types
Expose stable public names for:
Type
Contract
i8, i16, i32, i64
exact-width signed integers
u8, u16, u32, u64
exact-width unsigned integers
isize, usize
target pointer-sized signed/unsigned integers
f32, f64
IEEE-754 32-bit/64-bit floating-point values
byte
alias for u8
number
unchanged JavaScript-compatible numeric type
The existing PerryI32, PerryU64, PerryF32, etc. could remain as
compatibility aliases or internal generated names, while perry/native
provides the supported author-facing surface.
Numeric literals may use contextual typing when they are compile-time
representable:
The important part is that the conversion behavior is specified. A checked
conversion should not silently wrap, truncate, or lose precision. Explicit
wrapping/truncating/saturating operations can be separate APIs if Perry wants
to support them.
2. A stable POD/native-record contract
Promote the existing PerryPod<T> concept to a documented public form such as pod<T>.
For the first stable version, keep POD deliberately narrow and verifier-friendly:
ordered fields;
fixed-width scalar fields;
nested POD records;
fixed-size byte/numeric storage if and when Perry has a stable spelling for
it;
pointer-free handle IDs where explicitly supported; and
no managed string, normal Array<T>, class instance, closure, promise,
map, set, or arbitrary object fields.
sizeof<T>(), alignof<T>(), and offsetof<T>() should be compile-time
intrinsics for valid POD types. Field order, alignment, padding, and target
dependence must be documented.
POD layout should not silently imply endianness. Binary readers/writers should
continue to state byte order explicitly.
3. One representation contract across source, manifests, and codegen
A TypeScript POD declaration and a perry.nativeLibrary descriptor should be
cross-checked rather than allowed to drift independently.
For example, either:
the manifest references an exported TypeScript POD type; or
Perry generates a canonical layout descriptor from the TypeScript type and
compares it with the manifest/native declaration during perry native validate.
A disagreement in width, signedness, field order, offset, size, alignment,
ownership, or return type should be a compile-time error with both sides shown.
This would also provide a common type vocabulary for:
Native values should remain in their exact representation while Perry can prove
that representation, then materialize only at a real dynamic boundary.
The materialization rules should be part of the contract:
i8 through u32, f32, and f64 can convert to a JavaScript number
without changing their mathematical value (subject to the normal f32
rounding already having occurred);
i64 and u64 must not silently become an imprecise number;
crossing an untyped/dynamic boundary with i64/u64 should either use bigint, require an explicit checked conversion, or produce a compile-time
diagnostic; and
a POD record should materialize to an ordinary object only through an
explicit or compiler-recorded boundary, preserving Perry's existing
verifier/artifact model.
This is where the public feature should reuse #1849 rather than bypass it:
native representations, guards, fallback paths, and materialization reasons
remain auditable.
5. Preserve Perry's TypeScript compatibility by default
This proposal is opt-in.
constvalue=10;// still an ordinary TypeScript number
It does not reinterpret every integer-looking literal as an integer, does not
change operators for ordinary number, and does not require systems-level
annotations in normal application code.
The module spelling is also intentional: perry/native makes the non-portable
contract visible while keeping the source valid TypeScript and friendly to
existing editors, formatters, and TypeScript tooling.
Alternatives considered
Keep native representations entirely inferred
Perry should continue to infer and specialize ordinary TypeScript. That is a
major strength, and #1849 is the right foundation for it.
Inference alone is not enough when representation is externally observable:
C ABI calls, binary layouts, persistent formats, WebAssembly signatures,
hardware-facing buffers, and 64-bit integer precision need an author-declared
contract. An optimization may safely fall back; an ABI cannot silently do so.
Use typed arrays and DataView for everything
Typed arrays and DataView are good byte-storage APIs and should remain
supported. They become cumbersome for nested records and require authors to
repeat offsets, sizes, signedness, and alignment manually. They also do not
provide a function/manifest ABI contract by themselves.
Put all type information only in package.json
The native-library manifest already carries useful ABI information, but keeping
it only there duplicates types and prevents normal TypeScript code from
expressing the same contract. The source and manifest should be linked and
validated, not maintained as unrelated schemas.
Add new keywords such as struct, unsafe, or a new source extension now
That would make Perry-specific code clearer in some cases, but it would also
require parser, formatter, editor, language-server, and syntax-highlighting
work before the semantic contract is settled.
The existing branded types, POD analysis, native ABI descriptors, and arena
intrinsics show that Perry can establish the contract using valid TypeScript
first. New syntax can be reconsidered later if the library/type form proves
insufficient.
Treat TypeScript annotations as unconditional runtime truth
This would conflict with the safety model described in #1849. Normal TypeScript
annotations should remain erasable hints. Native types become runtime-relevant
only because the author explicitly opts into the perry/native contract, and
the compiler must still verify conversions, layouts, lifetimes, and boundary
transitions.
Scope
Small — CLI flag, new stdlib function, doc page
Medium — new widget, new code-gen pass, new perry.toml field
Large — new platform target, new backend, cross-cutting compiler change
This is large as a complete feature, but it can land in independently useful
slices:
stabilize names, documentation, conversions, and diagnostics for the native
scalar types Perry already models;
stabilize and document pod<T>, layout intrinsics, and NativeArena;
validate TypeScript POD/scalar declarations against native-library
manifests;
extend exact-width coverage (i8/i16/u8/u16/isize) and cross-target
behavior; and
broaden typed function/collection paths only where verifier-backed
correctness and measurable benefit justify them.
I would be happy for maintainers to split this RFC into a tracker plus
implementer-sized issues.
Additional context
This proposal is intended as a public-contract layer over work Perry already
contains:
I do not intend this to duplicate either tracker. #1849 is primarily the
compiler's specialization, proof, fallback, and materialization foundation. #6562 is runtime dynamic C-ABI loading and callbacks. This RFC asks for the
stable TypeScript-facing type, conversion, layout, and boundary contract that
can sit above both.
Keep ordinary TypeScript ordinary, but provide one stable way to state exact native width, layout, and boundary behavior when those properties are part of correctness.
The problem
Perry already has two strong halves of a native-value story, but they are not
yet one stable user-facing contract.
On the application side, Perry intentionally accepts ordinary TypeScript:
number, objects, arrays, interfaces, classes, npm modules, and Node-compatibleAPIs. That compatibility should remain the default.
On the compiler and native-library side, Perry already models explicit native
representations:
I32,I64,U32,U64,USize,F32,F64,BufferLen, native handles, POD records, and verifiedmaterialization boundaries;
records;
PerryI32,PerryU64,PerryF32, andPerryPod<T>;NativeArena, typed views,sizeof<T>(),alignof<T>(), andoffsetof<T>()already exist; andrepresentations and verifier-backed native regions.
The missing piece is a documented source-level contract that an application or
library author can intentionally use.
Today, a developer can see that Perry internally understands exact widths and
POD layouts, but there is no stable answer to questions such as:
u16,i32,u64, orf32, ratherthan an ordinary JavaScript
number?arrays, records, WebAssembly, and native-library boundaries?
same layout against a native-library manifest?
i64oru64crosses a JavaScript-compatible boundarywhere a
numbercannot represent it exactly?optimizations that may safely fall back to boxed JavaScript values?
Without that contract, native packages either duplicate representation
information in a manifest, pass data through generic
number/object values, ordrop down to typed arrays and manual offsets. The compiler can optimize some of
those cases, but the author cannot state and rely on an exact native contract.
A concrete example I would like Perry to support is:
The goal is not to make ordinary Perry code look like systems code. The goal is
to let TypeScript code become explicit at the small number of boundaries where
width, range, layout, ABI, or measured performance is part of correctness.
Proposed solution
Stabilize an opt-in Perry native-value profile exposed through a normal,
valid TypeScript module such as
perry/native.This should build on the native representation and ABI work Perry already has,
not introduce a separate parser, file extension, or second compiler pipeline.
Ordinary
.ts/.tsxcode and ordinarynumbersemantics remain unchanged.1. Public native scalar types
Expose stable public names for:
i8,i16,i32,i64u8,u16,u32,u64isize,usizef32,f64byteu8numberThe existing
PerryI32,PerryU64,PerryF32, etc. could remain ascompatibility aliases or internal generated names, while
perry/nativeprovides the supported author-facing surface.
Numeric literals may use contextual typing when they are compile-time
representable:
Conversions from a dynamic or wider value should be explicit and checked:
The important part is that the conversion behavior is specified. A checked
conversion should not silently wrap, truncate, or lose precision. Explicit
wrapping/truncating/saturating operations can be separate APIs if Perry wants
to support them.
2. A stable POD/native-record contract
Promote the existing
PerryPod<T>concept to a documented public form such aspod<T>.For the first stable version, keep POD deliberately narrow and verifier-friendly:
it;
string, normalArray<T>, class instance, closure, promise,map, set, or arbitrary object fields.
sizeof<T>(),alignof<T>(), andoffsetof<T>()should be compile-timeintrinsics for valid POD types. Field order, alignment, padding, and target
dependence must be documented.
POD layout should not silently imply endianness. Binary readers/writers should
continue to state byte order explicitly.
3. One representation contract across source, manifests, and codegen
A TypeScript POD declaration and a
perry.nativeLibrarydescriptor should becross-checked rather than allowed to drift independently.
For example, either:
compares it with the manifest/native declaration during
perry native validate.A disagreement in width, signedness, field order, offset, size, alignment,
ownership, or return type should be a compile-time error with both sides shown.
This would also provide a common type vocabulary for:
perry.nativeLibrarypackages;4. Explicit dynamic-boundary materialization
Native values should remain in their exact representation while Perry can prove
that representation, then materialize only at a real dynamic boundary.
The materialization rules should be part of the contract:
i8throughu32,f32, andf64can convert to a JavaScriptnumberwithout changing their mathematical value (subject to the normal
f32rounding already having occurred);
i64andu64must not silently become an imprecisenumber;i64/u64should either usebigint, require an explicit checked conversion, or produce a compile-timediagnostic; and
explicit or compiler-recorded boundary, preserving Perry's existing
verifier/artifact model.
This is where the public feature should reuse #1849 rather than bypass it:
native representations, guards, fallback paths, and materialization reasons
remain auditable.
5. Preserve Perry's TypeScript compatibility by default
This proposal is opt-in.
It does not reinterpret every integer-looking literal as an integer, does not
change operators for ordinary
number, and does not require systems-levelannotations in normal application code.
The module spelling is also intentional:
perry/nativemakes the non-portablecontract visible while keeping the source valid TypeScript and friendly to
existing editors, formatters, and TypeScript tooling.
Alternatives considered
Keep native representations entirely inferred
Perry should continue to infer and specialize ordinary TypeScript. That is a
major strength, and #1849 is the right foundation for it.
Inference alone is not enough when representation is externally observable:
C ABI calls, binary layouts, persistent formats, WebAssembly signatures,
hardware-facing buffers, and 64-bit integer precision need an author-declared
contract. An optimization may safely fall back; an ABI cannot silently do so.
Use typed arrays and
DataViewfor everythingTyped arrays and
DataVieware good byte-storage APIs and should remainsupported. They become cumbersome for nested records and require authors to
repeat offsets, sizes, signedness, and alignment manually. They also do not
provide a function/manifest ABI contract by themselves.
Put all type information only in
package.jsonThe native-library manifest already carries useful ABI information, but keeping
it only there duplicates types and prevents normal TypeScript code from
expressing the same contract. The source and manifest should be linked and
validated, not maintained as unrelated schemas.
Add new keywords such as
struct,unsafe, or a new source extension nowThat would make Perry-specific code clearer in some cases, but it would also
require parser, formatter, editor, language-server, and syntax-highlighting
work before the semantic contract is settled.
The existing branded types, POD analysis, native ABI descriptors, and arena
intrinsics show that Perry can establish the contract using valid TypeScript
first. New syntax can be reconsidered later if the library/type form proves
insufficient.
Treat TypeScript annotations as unconditional runtime truth
This would conflict with the safety model described in #1849. Normal TypeScript
annotations should remain erasable hints. Native types become runtime-relevant
only because the author explicitly opts into the
perry/nativecontract, andthe compiler must still verify conversions, layouts, lifetimes, and boundary
transitions.
Scope
This is large as a complete feature, but it can land in independently useful
slices:
scalar types Perry already models;
pod<T>, layout intrinsics, andNativeArena;manifests;
i8/i16/u8/u16/isize) and cross-targetbehavior; and
correctness and measurable benefit justify them.
I would be happy for maintainers to split this RFC into a tracker plus
implementer-sized issues.
Additional context
This proposal is intended as a public-contract layer over work Perry already
contains:
PerryI32/PerryPod<T>/NativeArenadeclarationsI do not intend this to duplicate either tracker. #1849 is primarily the
compiler's specialization, proof, fallback, and materialization foundation.
#6562 is runtime dynamic C-ABI loading and callbacks. This RFC asks for the
stable TypeScript-facing type, conversion, layout, and boundary contract that
can sit above both.
Relevant prior art:
https://bun.sh/docs/api/ffi
i32,i64,f32, andf64value types:https://webassembly.github.io/spec/core/syntax/types.html
than an inference:
https://doc.rust-lang.org/reference/type-layout.html#the-c-representation
The requested Perry-specific outcome is: