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
6 changes: 6 additions & 0 deletions changelog.d/8330-native-u8-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Native values: expose `u8` and `byte` in `perry/native` (#6827)

Applications can now use checked `u8(value)` conversions and exact one-byte
`u8` or `byte` fields in verifier-backed `pod<T>` records. Out-of-range,
fractional, negative, and non-number conversions fail instead of truncating or
wrapping.
4 changes: 4 additions & 0 deletions crates/perry-api-manifest/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ fn entry_visible_in_dts(entry: &ApiEntry) -> bool {
}

fn emit_native_memory_globals(out: &mut String) {
let _ = writeln!(
out,
"type PerryU8 = number & {{ readonly __perryU8?: never }};"
);
let _ = writeln!(
out,
"type PerryU32 = number & {{ readonly __perryU32?: never }};"
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-api-manifest/src/entries/part_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,14 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[
&[p_str("field")],
TypeSpec::Number,
)),
method_sig(
"perry/native",
"u8",
false,
None,
&[p_num("value")],
TypeSpec::Number,
),
method_sig(
"perry/native",
"i32",
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-api-manifest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ mod tests {
"sizeof",
"alignof",
"offsetof",
"u8",
"i32",
"i64",
"u32",
Expand All @@ -577,7 +578,7 @@ mod tests {
assert_eq!(entry.returns, TypeSpec::Number, "{name}");
}

for name in ["i32", "i64", "u32", "u64", "usize", "f32", "f64"] {
for name in ["u8", "i32", "i64", "u32", "u64", "usize", "f32", "f64"] {
let entry = module_has_symbol("perry/native", name)
.unwrap_or_else(|| panic!("perry/native missing conversion {name}"));
assert!(matches!(entry.kind, ApiKind::Method { .. }));
Expand Down
29 changes: 29 additions & 0 deletions crates/perry-codegen/src/expr/i32_fast_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,7 @@ pub(crate) fn lower_expr_native(
ExpectedNativeRep::JsValueBits => lower_expr_native_js_value_bits(ctx, e),
ExpectedNativeRep::I32 => lower_expr_native_i32(ctx, e),
ExpectedNativeRep::I64 => lower_expr_native_i64(ctx, e),
ExpectedNativeRep::U8 => lower_expr_native_u8(ctx, e),
ExpectedNativeRep::U32 => lower_expr_native_u32(ctx, e),
ExpectedNativeRep::U64 => lower_expr_native_u64(ctx, e),
ExpectedNativeRep::USize => lower_expr_native_usize(ctx, e),
Expand Down Expand Up @@ -952,6 +953,10 @@ fn u32_lowered(value: String) -> LoweredValue {
LoweredValue::u32(value)
}

fn u8_lowered(value: String) -> LoweredValue {
LoweredValue::u8(value)
}

fn u64_lowered(value: String) -> LoweredValue {
LoweredValue::u64(value)
}
Expand Down Expand Up @@ -1676,6 +1681,30 @@ fn lower_expr_native_u32(ctx: &mut FnCtx<'_>, e: &Expr) -> Result<LoweredValue>
Ok(lowered)
}

fn lower_expr_native_u8(ctx: &mut FnCtx<'_>, e: &Expr) -> Result<LoweredValue> {
let value = match e {
Expr::Integer(n) if u8::try_from(*n).is_ok() => (*n as u8).to_string(),
_ => {
let value = lower_expr(ctx, e)?;
ctx.block().fptoui(DOUBLE, &value, I8)
}
};
let lowered = u8_lowered(value);
ctx.record_lowered_value(
native_expr_kind(e),
None,
"lower_expr_native_u8",
&lowered,
None,
None,
None,
false,
false,
Vec::new(),
);
Ok(lowered)
}

fn lower_expr_native_i64(ctx: &mut FnCtx<'_>, e: &Expr) -> Result<LoweredValue> {
let value = match e {
Expr::Integer(n) => n.to_string(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/expr/pod_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ fn coerce_js_double_to_native(
field: &PodLayoutField,
) -> LoweredValue {
let value = match field.native_rep {
NativeRep::U8 => ctx.block().fptoui(DOUBLE, value_js, I8),
NativeRep::I32 => ctx.block().fptosi(DOUBLE, value_js, I32),
NativeRep::I64 => ctx.block().fptosi(DOUBLE, value_js, I64),
NativeRep::U32 | NativeRep::BufferLen => ctx.block().toint32(value_js),
Expand Down Expand Up @@ -483,6 +484,7 @@ fn pod_field_write_compatibility_guard(

fn pod_scalar_guard_rep_id(rep: &NativeRep) -> i32 {
match rep {
NativeRep::U8 => 10,
NativeRep::I32 => 1,
NativeRep::I64 => 2,
NativeRep::U32 => 3,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
use super::*;

pub(super) const NATIVE_PROFILE_ROWS: &[NativeModSig] = &[
NativeModSig {
module: "perry/native",
has_receiver: false,
method: "u8",
class_filter: None,
runtime: "js_perry_native_u8",
args: &[NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "perry/native",
has_receiver: false,
Expand Down
11 changes: 10 additions & 1 deletion crates/perry-codegen/src/native_value/pod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ fn pod_init_value_roundtrips_exact(rep: &NativeRep, value: &Expr) -> bool {
}

match rep {
NativeRep::U8 => {
literal_i64(value).is_some_and(|n| u8::try_from(n).is_ok())
|| literal_f64(value).is_some_and(|n| uint_roundtrips_exact(n, 256.0))
}
NativeRep::I32 => {
literal_i64(value).is_some_and(|n| i32::try_from(n).is_ok())
|| literal_f64(value).is_some_and(|n| {
Expand Down Expand Up @@ -225,7 +229,8 @@ fn checked_native_scalar_conversion_matches(rep: &NativeRep, value: &Expr) -> bo

matches!(
(rep, method.as_str()),
(NativeRep::I32, "i32")
(NativeRep::U8, "u8")
| (NativeRep::I32, "i32")
| (NativeRep::I64, "i64")
| (NativeRep::U32, "u32")
| (NativeRep::U64, "u64")
Expand Down Expand Up @@ -278,6 +283,7 @@ pub(crate) fn llvm_type_for_native_rep(rep: &NativeRep) -> Option<&'static str>
Some(match rep {
NativeRep::JsValue | NativeRep::F64 => DOUBLE,
NativeRep::F32 => F32,
NativeRep::U8 => crate::types::I8,
NativeRep::I64 | NativeRep::U64 | NativeRep::USize | NativeRep::HandleId => I64,
NativeRep::I32 | NativeRep::U32 | NativeRep::BufferLen => I32,
_ => return None,
Expand All @@ -286,6 +292,7 @@ pub(crate) fn llvm_type_for_native_rep(rep: &NativeRep) -> Option<&'static str>

pub(crate) fn expected_rep_for_native_rep(rep: &NativeRep) -> Option<ExpectedNativeRep> {
Some(match rep {
NativeRep::U8 => ExpectedNativeRep::U8,
NativeRep::I32 => ExpectedNativeRep::I32,
NativeRep::I64 => ExpectedNativeRep::I64,
NativeRep::U32 => ExpectedNativeRep::U32,
Expand Down Expand Up @@ -366,6 +373,7 @@ fn layout_for_manifest_pod_with_prefix(

pub(crate) fn scalar_size_align(rep: &NativeRep) -> Option<(u32, u32)> {
Some(match rep {
NativeRep::U8 => (1, 1),
NativeRep::I32 | NativeRep::U32 | NativeRep::F32 | NativeRep::BufferLen => (4, 4),
NativeRep::I64
| NativeRep::U64
Expand Down Expand Up @@ -690,6 +698,7 @@ fn field_native_rep(ctx: &FnCtx<'_>, ty: &Type, depth: u8) -> Result<NativeRep,
}
match ty {
Type::Named(name) => match name.as_str() {
"PerryU8" => Ok(NativeRep::U8),
"PerryU32" => Ok(NativeRep::U32),
"PerryU64" => Ok(NativeRep::U64),
"PerryUSize" => Ok(NativeRep::USize),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/native_value/rep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ pub(crate) enum ExpectedNativeRep {
JsValueBits,
I32,
I64,
U8,
U32,
U64,
USize,
Expand Down Expand Up @@ -275,6 +276,7 @@ impl LoweredValue {
(ExpectedNativeRep::JsValueBits, NativeRep::JsValueBits)
| (ExpectedNativeRep::I32, NativeRep::I32)
| (ExpectedNativeRep::I64, NativeRep::I64)
| (ExpectedNativeRep::U8, NativeRep::U8)
| (ExpectedNativeRep::U32, NativeRep::U32)
| (ExpectedNativeRep::U64, NativeRep::U64)
| (ExpectedNativeRep::USize, NativeRep::USize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub(crate) fn declare_third_party(module: &mut LlModule) {
module.declare_function("js_thread_spawn", DOUBLE, &[DOUBLE]);
// `perry/native` checked scalar conversions. Results remain ordinary
// JavaScript-compatible numbers at the public boundary.
module.declare_function("js_perry_native_u8", DOUBLE, &[DOUBLE]);
module.declare_function("js_perry_native_i32", DOUBLE, &[DOUBLE]);
module.declare_function("js_perry_native_i64", DOUBLE, &[DOUBLE]);
module.declare_function("js_perry_native_u32", DOUBLE, &[DOUBLE]);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/lower/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,7 @@ impl LoweringContext {
imported_name: &str,
) {
let canonical = match imported_name {
"u8" | "byte" => "PerryU8",
"u32" => "PerryU32",
"u64" => "PerryU64",
"usize" => "PerryUSize",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn native_scalar_conversion_name<'a>(
(module == "perry/native"
&& matches!(
method,
"i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64"
"u8" | "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64"
))
.then_some(method)
}
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-hir/src/lower_types/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ pub(crate) fn extract_ts_type_with_ctx(

if matches!(
name.as_str(),
"PerryU32"
"PerryU8"
| "PerryU32"
| "PerryU64"
| "PerryUSize"
| "PerryF32"
Expand Down
12 changes: 10 additions & 2 deletions crates/perry-hir/tests/native_arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ fn perry_native_imports_reuse_canonical_pod_lowering() {
let module = lower_src(
r#"
import {
type u8 as Octet,
type byte as Byte,
type u32 as Word,
type f32,
type pod as NativeRecord,
Expand All @@ -326,7 +328,7 @@ fn perry_native_imports_reuse_canonical_pod_lowering() {
offsetof as offsetOf,
} from "perry/native";

type Packet = NativeRecord<{ tag: Word; gain: f32; }>;
type Packet = NativeRecord<{ kind: Octet; marker: Byte; tag: Word; gain: f32; }>;
const packetSize = sizeOf<Packet>();
const packetAlign = alignOf<Packet>();
const gainOffset = offsetOf<Packet>("gain");
Expand Down Expand Up @@ -657,13 +659,19 @@ fn native_arena_public_view_rejects_dynamic_kind() {
fn native_scalar_conversion_imports_lower_as_native_module_calls() {
let module = lower_src(
r#"
import { u32 as word, f32 } from "perry/native";
import { u8 as octet, u32 as word, f32 } from "perry/native";
const kind = octet(255);
const count = word(42);
const ratio = f32(0.1);
"#,
)
.expect("native scalar conversions should lower");

assert!(module_any(&module, |expr| matches!(
expr,
Expr::NativeMethodCall { module, method, args, .. }
if module == "perry/native" && method == "u8" && args.len() == 1
)));
assert!(module_any(&module, |expr| matches!(
expr,
Expr::NativeMethodCall { module, method, args, .. }
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-runtime/src/native_value_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;

#[derive(Clone, Copy, Debug)]
enum ScalarConversion {
U8,
I32,
I64,
U32,
Expand All @@ -24,6 +25,7 @@ enum ScalarConversion {
impl ScalarConversion {
fn name(self) -> &'static str {
match self {
Self::U8 => "u8",
Self::I32 => "i32",
Self::I64 => "i64",
Self::U32 => "u32",
Expand All @@ -43,6 +45,7 @@ fn checked_number(value: f64, conversion: ScalarConversion) -> Result<f64, &'sta

let number = js_value.as_number();
let valid = match conversion {
ScalarConversion::U8 => integer_in_range(number, 0.0, u8::MAX as f64),
ScalarConversion::I32 => integer_in_range(number, i32::MIN as f64, i32::MAX as f64),
ScalarConversion::I64 => integer_in_range(number, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER),
ScalarConversion::U32 => integer_in_range(number, 0.0, u32::MAX as f64),
Expand Down Expand Up @@ -100,6 +103,7 @@ macro_rules! scalar_conversion {
}

scalar_conversion!(js_perry_native_i32, I32);
scalar_conversion!(js_perry_native_u8, U8);
scalar_conversion!(js_perry_native_i64, I64);
scalar_conversion!(js_perry_native_u32, U32);
scalar_conversion!(js_perry_native_u64, U64);
Expand All @@ -113,6 +117,8 @@ mod tests {

#[test]
fn integer_conversions_reject_fractional_out_of_range_and_imprecise_numbers() {
assert_eq!(checked_number(0.0, ScalarConversion::U8), Ok(0.0));
assert_eq!(checked_number(255.0, ScalarConversion::U8), Ok(255.0));
assert_eq!(
checked_number(-2_147_483_648.0, ScalarConversion::I32),
Ok(-2_147_483_648.0)
Expand All @@ -122,6 +128,8 @@ mod tests {
Ok(4_294_967_295.0)
);
assert!(checked_number(1.5, ScalarConversion::I32).is_err());
assert!(checked_number(-1.0, ScalarConversion::U8).is_err());
assert!(checked_number(256.0, ScalarConversion::U8).is_err());
assert!(checked_number(-1.0, ScalarConversion::U32).is_err());
assert!(checked_number(4_294_967_296.0, ScalarConversion::U32).is_err());
assert!(checked_number(9_007_199_254_740_992.0, ScalarConversion::U64).is_err());
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/value/nanbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const POD_REP_F64: i32 = 6;
const POD_REP_F32: i32 = 7;
const POD_REP_BUFFER_LEN: i32 = 8;
const POD_REP_HANDLE_ID: i32 = 9;
const POD_REP_U8: i32 = 10;

// FFI functions for creating NaN-boxed values from raw pointers

Expand All @@ -35,6 +36,7 @@ pub extern "C" fn js_pod_scalar_write_compatible(value: f64, native_rep: i32) ->

let number = js_value.as_number();
let compatible = match native_rep {
POD_REP_U8 => uint_roundtrips_exact(number, 256.0),
POD_REP_I32 => int_roundtrips_exact(number, i32::MIN as f64, (i32::MAX as f64) + 1.0),
POD_REP_I64 => int_roundtrips_exact(number, i64::MIN as f64, 9_223_372_036_854_775_808.0),
POD_REP_U32 | POD_REP_BUFFER_LEN => uint_roundtrips_exact(number, 4_294_967_296.0),
Expand Down
5 changes: 4 additions & 1 deletion docs/api/perry.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Auto-generated from Perry's API manifest (#465). Do not edit by hand.
// Source: perry-api-manifest::API_MANIFEST
// Coverage: 2023 entries across 123 modules
// Coverage: 2024 entries across 123 modules

type PerryU8 = number & { readonly __perryU8?: never };
type PerryU32 = number & { readonly __perryU32?: never };
type PerryU64 = number & { readonly __perryU64?: never };
type PerryUSize = number & { readonly __perryUSize?: never };
Expand Down Expand Up @@ -2691,6 +2692,8 @@ declare module "perry/native" {
/** stdlib */
export function u64(value: number): number;
/** stdlib */
export function u8(value: number): number;
/** stdlib */
export function usize(value: number): number;
}

Expand Down
3 changes: 2 additions & 1 deletion docs/src/api/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target.

Total: 2946 entries across 125 modules.
Total: 2947 entries across 125 modules.

## Modules

Expand Down Expand Up @@ -2538,6 +2538,7 @@ Total: 2946 entries across 125 modules.
- `sizeof` — module *(intrinsic)*
- `u32` — module
- `u64` — module
- `u8` — module
- `usize` — module

### Properties
Expand Down
Loading
Loading