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
150 changes: 76 additions & 74 deletions Cargo.lock

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions crates/perry-api-manifest/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1402,6 +1402,13 @@ pub static API_MANIFEST: &[ApiEntry] = &[
method("commander", "parse", true, None),
method("commander", "opts", true, None),
method("commander", "argument", true, None),
// `program.args` is a bare member read modeled as a property for the
// `.d.ts` surface (`export const args`), but the dispatch table lowers
// it to a 0-arg instance getter row (`commander::args`, has_receiver).
// The drift gate (every_dispatch_entry_has_manifest_counterpart) wants
// a Method counterpart for that row; keep both — the has_receiver
// method isn't emitted as a module export, so docs are unchanged (#5137).
method("commander", "args", true, None),
property("commander", "args"),
property("async_hooks", "default"),
property("async_hooks", "asyncWrapProviders"),
Expand Down Expand Up @@ -1473,6 +1480,44 @@ pub static API_MANIFEST: &[ApiEntry] = &[
method_sig("uuid", "v4", false, None, &[], TypeSpec::String),
method_sig("uuid", "v1", false, None, &[], TypeSpec::String),
method_sig("uuid", "v7", false, None, &[], TypeSpec::String),
method_sig(
"uuid",
"v5",
false,
None,
&[
ParamSpec::Named {
name: "name",
ty: TypeSpec::String,
optional: false,
},
ParamSpec::Named {
name: "namespace",
ty: TypeSpec::String,
optional: false,
},
],
TypeSpec::String,
),
method_sig(
"uuid",
"v3",
false,
None,
&[
ParamSpec::Named {
name: "name",
ty: TypeSpec::String,
optional: false,
},
ParamSpec::Named {
name: "namespace",
ty: TypeSpec::String,
optional: false,
},
],
TypeSpec::String,
),
method_sig(
"uuid",
"validate",
Expand All @@ -1485,6 +1530,18 @@ pub static API_MANIFEST: &[ApiEntry] = &[
}],
TypeSpec::Bool,
),
method_sig(
"uuid",
"version",
false,
None,
&[ParamSpec::Named {
name: "id",
ty: TypeSpec::String,
optional: false,
}],
TypeSpec::Number,
),
method_sig(
"jsonwebtoken",
"sign",
Expand Down
13 changes: 12 additions & 1 deletion crates/perry-codegen/src/lower_call/native_module_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ pub fn lower_native_module_dispatch(
| NativeRetKind::Str
| NativeRetKind::ObjFromJsonStr
| NativeRetKind::BigInt => I64,
NativeRetKind::F64 => DOUBLE,
NativeRetKind::F64 | NativeRetKind::Bool => DOUBLE,
NativeRetKind::I32Void => I32,
NativeRetKind::Void => crate::types::VOID,
};
Expand Down Expand Up @@ -272,6 +272,17 @@ pub fn lower_native_module_dispatch(
Ok(nanbox_bigint_inline(blk, &raw))
}
NativeRetKind::F64 => Ok(ctx.block().call(DOUBLE, sig.runtime, &arg_slices)),
NativeRetKind::Bool => {
// Runtime returns the FFI bool-as-f64 convention (1.0/0.0).
// Box it as a real JS boolean so `===`, `if`, and
// `console.log` see `true`/`false`, not the numbers 1/0.
let blk = ctx.block();
let raw = blk.call(DOUBLE, sig.runtime, &arg_slices);
let is_true = blk.fcmp("one", &raw, "0.0");
let true_val = double_literal(f64::from_bits(crate::nanbox::TAG_TRUE));
let false_val = double_literal(f64::from_bits(crate::nanbox::TAG_FALSE));
Ok(blk.select(crate::types::I1, &is_true, DOUBLE, &true_val, &false_val))
}
NativeRetKind::I32Void => {
let _discard = ctx.block().call(I32, sig.runtime, &arg_slices);
Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)))
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-codegen/src/lower_call/native_table/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ pub(super) enum NativeRetKind {
BigInt,
/// Returns f64 → pass through (NaN-boxed JSValue).
F64,
/// Returns f64 `1.0`/`0.0` → box as a real JS boolean
/// (`TAG_TRUE`/`TAG_FALSE`) so `console.log` prints `true`/`false`
/// rather than `1`/`0`. Use for predicates whose runtime signature
/// returns the FFI bool-as-f64 convention (e.g. `uuid.validate`).
Bool,
/// Returns i32 → ignored, return TAG_UNDEFINED.
I32Void,
/// Returns void → return TAG_UNDEFINED.
Expand Down Expand Up @@ -129,6 +134,7 @@ pub(super) const NR_STR: NativeRetKind = NativeRetKind::Str;
pub(super) const NR_OBJ_FROM_JSON_STR: NativeRetKind = NativeRetKind::ObjFromJsonStr;
pub(super) const NR_BIGINT: NativeRetKind = NativeRetKind::BigInt;
pub(super) const NR_F64: NativeRetKind = NativeRetKind::F64;
pub(super) const NR_BOOL: NativeRetKind = NativeRetKind::Bool;
pub(super) const NR_I32: NativeRetKind = NativeRetKind::I32Void;
pub(super) const NR_VOID: NativeRetKind = NativeRetKind::Void;

Expand Down Expand Up @@ -238,6 +244,7 @@ fn ret_kind_tag(r: &NativeRetKind) -> &'static str {
NativeRetKind::ObjFromJsonStr => "NR_OBJ_FROM_JSON_STR",
NativeRetKind::BigInt => "NR_BIGINT",
NativeRetKind::F64 => "NR_F64",
NativeRetKind::Bool => "NR_BOOL",
NativeRetKind::I32Void => "NR_I32",
NativeRetKind::Void => "NR_VOID",
}
Expand Down
45 changes: 41 additions & 4 deletions crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ use super::*;

pub(super) const UTILS_CRYPTO_ROWS: &[NativeModSig] = &[
// ========== uuid ==========
// All generators return `*mut StringHeader`, so they must box as
// NR_STR (STRING_TAG) — NR_PTR boxed them as a generic native handle
// and `v4()` read back as `[object Object]` (#5197).
NativeModSig {
module: "uuid",
has_receiver: false,
method: "v4",
class_filter: None,
runtime: "js_uuid_v4",
args: &[],
ret: NR_PTR,
ret: NR_STR,
},
NativeModSig {
module: "uuid",
Expand All @@ -18,7 +21,7 @@ pub(super) const UTILS_CRYPTO_ROWS: &[NativeModSig] = &[
class_filter: None,
runtime: "js_uuid_v1",
args: &[],
ret: NR_PTR,
ret: NR_STR,
},
NativeModSig {
module: "uuid",
Expand All @@ -27,15 +30,49 @@ pub(super) const UTILS_CRYPTO_ROWS: &[NativeModSig] = &[
class_filter: None,
runtime: "js_uuid_v7",
args: &[],
ret: NR_PTR,
ret: NR_STR,
},
// v5 (SHA-1) / v3 (MD5) name-based: `vN(name, namespace)`. The shim
// supports the string-UUID namespace form; the array-namespace form
// is only reachable via `perry.compilePackages`.
NativeModSig {
module: "uuid",
has_receiver: false,
method: "v5",
class_filter: None,
runtime: "js_uuid_v5",
args: &[NA_STR, NA_STR],
ret: NR_STR,
},
NativeModSig {
module: "uuid",
has_receiver: false,
method: "v3",
class_filter: None,
runtime: "js_uuid_v3",
args: &[NA_STR, NA_STR],
ret: NR_STR,
},
NativeModSig {
module: "uuid",
has_receiver: false,
method: "validate",
class_filter: None,
runtime: "js_uuid_validate",
args: &[NA_F64],
// Runtime sig is `*const StringHeader` → coerce the arg to a
// string pointer (NA_F64 passed raw NaN-box bits, so validate
// always read 0 — #5197). NR_BOOL boxes the 1.0/0.0 result as a
// real JS boolean so it prints `true`/`false`, not `1`/`0`.
args: &[NA_STR],
ret: NR_BOOL,
},
NativeModSig {
module: "uuid",
has_receiver: false,
method: "version",
class_filter: None,
runtime: "js_uuid_version",
args: &[NA_STR],
ret: NR_F64,
},
// ========== jsonwebtoken ==========
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-ext-uuid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ crate-type = ["staticlib", "rlib"]

[dependencies]
perry-ffi.workspace = true
uuid = { version = "1.23", features = ["v1", "v4", "v7"] }
uuid = { version = "1.23", features = ["v1", "v3", "v4", "v5", "v7"] }

[dev-dependencies]
perry-ffi = { workspace = true, features = ["runtime-link"] }
78 changes: 78 additions & 0 deletions crates/perry-ext-uuid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,53 @@ pub extern "C" fn js_uuid_v7() -> *mut StringHeader {
alloc_string(&uuid.to_string()).as_raw()
}

/// Parse a NaN-boxed namespace argument into a `Uuid`. The shim only
/// supports the string-UUID namespace form (`v5(name, '6ba7…')`), which
/// covers the `uuid.v5.DNS`/`uuid.v5.URL` constants and the overwhelming
/// majority of real usage. A non-string / unparseable namespace falls
/// back to the nil UUID rather than crashing — the array-namespace form
/// is only reachable via `perry.compilePackages` (real source).
unsafe fn parse_namespace(ns_ptr: *const StringHeader) -> Uuid {
let handle = JsString::from_raw(ns_ptr as *mut StringHeader);
read_string(handle)
.and_then(|s| Uuid::parse_str(s).ok())
.unwrap_or_else(Uuid::nil)
}

/// `uuid.v5(name, namespace)` — SHA-1 name-based UUID.
///
/// # Safety
///
/// `name_ptr` / `ns_ptr` must be null or Perry-runtime `StringHeader`
/// pointers.
#[no_mangle]
pub unsafe extern "C" fn js_uuid_v5(
name_ptr: *const StringHeader,
ns_ptr: *const StringHeader,
) -> *mut StringHeader {
let name = read_string(JsString::from_raw(name_ptr as *mut StringHeader)).unwrap_or("");
let namespace = parse_namespace(ns_ptr);
let uuid = Uuid::new_v5(&namespace, name.as_bytes());
alloc_string(&uuid.to_string()).as_raw()
}

/// `uuid.v3(name, namespace)` — MD5 name-based UUID.
///
/// # Safety
///
/// `name_ptr` / `ns_ptr` must be null or Perry-runtime `StringHeader`
/// pointers.
#[no_mangle]
pub unsafe extern "C" fn js_uuid_v3(
name_ptr: *const StringHeader,
ns_ptr: *const StringHeader,
) -> *mut StringHeader {
let name = read_string(JsString::from_raw(name_ptr as *mut StringHeader)).unwrap_or("");
let namespace = parse_namespace(ns_ptr);
let uuid = Uuid::new_v3(&namespace, name.as_bytes());
alloc_string(&uuid.to_string()).as_raw()
}

/// `uuid.validate(str) -> boolean` — encoded as `1.0` / `0.0`
/// because the Perry FFI ABI carries booleans as f64.
///
Expand Down Expand Up @@ -125,4 +172,35 @@ mod tests {
let s = read_handle(js_uuid_nil());
assert_eq!(s, "00000000-0000-0000-0000-000000000000");
}

#[test]
fn v5_matches_the_reference_vector() {
// `v5('perry', '6ba7b810-9dad-11d1-80b4-00c04fd430c8')` — the
// exact value Node's `uuid` produces (issue #5197).
let name = alloc_string("perry");
let ns = alloc_string("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
let id =
read_handle(unsafe { js_uuid_v5(name.as_raw() as *const _, ns.as_raw() as *const _) });
assert_eq!(id, "6cb3836f-339d-52d8-acc6-8751229b61cf");
let id_handle = alloc_string(&id);
assert_eq!(
unsafe { js_uuid_version(id_handle.as_raw() as *const _) },
5.0
);
}

#[test]
fn v3_matches_the_reference_vector() {
// `v3('perry', '6ba7b810-9dad-11d1-80b4-00c04fd430c8')`.
let name = alloc_string("perry");
let ns = alloc_string("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
let id =
read_handle(unsafe { js_uuid_v3(name.as_raw() as *const _, ns.as_raw() as *const _) });
assert_eq!(id, "3533df6e-72b1-3859-a772-7410b3d2f9c2");
let id_handle = alloc_string(&id);
assert_eq!(
unsafe { js_uuid_version(id_handle.as_raw() as *const _) },
3.0
);
}
}
2 changes: 1 addition & 1 deletion crates/perry-stdlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ validator = { version = "0.20", features = ["derive"], optional = true }
regex = { version = "1.12", optional = true }

# IDs
uuid = { version = "1.23", features = ["v4", "v1", "v7"], optional = true }
uuid = { version = "1.23", features = ["v4", "v1", "v3", "v5", "v7"], optional = true }
nanoid = { version = "0.5", optional = true }

# LRU Cache — optional from v0.5.539 so the well-known flip can
Expand Down
44 changes: 44 additions & 0 deletions crates/perry-stdlib/src/uuid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,50 @@ pub extern "C" fn js_uuid_v7() -> *mut StringHeader {
js_string_from_bytes(uuid_str.as_ptr(), uuid_str.len() as u32)
}

/// Read a `*const StringHeader` into a `&str`, or `""` if null / invalid UTF-8.
unsafe fn read_str<'a>(str_ptr: *const StringHeader) -> &'a str {
if str_ptr.is_null() {
return "";
}
let len = (*str_ptr).byte_len as usize;
let data_ptr = (str_ptr as *const u8).add(std::mem::size_of::<StringHeader>());
let bytes = std::slice::from_raw_parts(data_ptr, len);
std::str::from_utf8(bytes).unwrap_or("")
}

/// Parse a namespace argument (string-UUID form) into a `Uuid`, falling
/// back to the nil UUID when the argument isn't a parseable UUID string.
/// The array-namespace form is only reachable via `perry.compilePackages`.
unsafe fn parse_namespace(ns_ptr: *const StringHeader) -> Uuid {
Uuid::parse_str(read_str(ns_ptr)).unwrap_or_else(|_| Uuid::nil())
}

/// Generate a v5 (SHA-1 name-based) UUID and return it as a string
/// uuid.v5(name, namespace) -> string
#[no_mangle]
pub unsafe extern "C" fn js_uuid_v5(
name_ptr: *const StringHeader,
ns_ptr: *const StringHeader,
) -> *mut StringHeader {
let namespace = parse_namespace(ns_ptr);
let uuid = Uuid::new_v5(&namespace, read_str(name_ptr).as_bytes());
let uuid_str = uuid.to_string();
js_string_from_bytes(uuid_str.as_ptr(), uuid_str.len() as u32)
}

/// Generate a v3 (MD5 name-based) UUID and return it as a string
/// uuid.v3(name, namespace) -> string
#[no_mangle]
pub unsafe extern "C" fn js_uuid_v3(
name_ptr: *const StringHeader,
ns_ptr: *const StringHeader,
) -> *mut StringHeader {
let namespace = parse_namespace(ns_ptr);
let uuid = Uuid::new_v3(&namespace, read_str(name_ptr).as_bytes());
let uuid_str = uuid.to_string();
js_string_from_bytes(uuid_str.as_ptr(), uuid_str.len() as u32)
}

/// Validate if a string is a valid UUID
/// uuid.validate(str) -> boolean
#[no_mangle]
Expand Down
8 changes: 7 additions & 1 deletion docs/api/perry.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Auto-generated from Perry's API manifest (#465). Do not edit by hand.
// Source: perry-api-manifest::API_MANIFEST
// Coverage: 1940 entries across 113 modules
// Coverage: 1943 entries across 113 modules

type PerryU32 = number & { readonly __perryU32?: never };
type PerryU64 = number & { readonly __perryU64?: never };
Expand Down Expand Up @@ -3985,11 +3985,17 @@ declare module "uuid" {
/** stdlib */
export function v1(): string;
/** stdlib */
export function v3(name: string, namespace: string): string;
/** stdlib */
export function v4(): string;
/** stdlib */
export function v5(name: string, namespace: string): string;
/** stdlib */
export function v7(): string;
/** stdlib */
export function validate(id: string): boolean;
/** stdlib */
export function version(id: string): number;
}

declare module "v8" {
Expand Down
Loading