From 470bc06fd828e166102dfab9cfdaf62e083f1056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 14:08:51 +0200 Subject: [PATCH 1/4] perf(codegen): inline strict `===` against a string literal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `n.kind === "num"` compiled to a `js_eq` -> `js_jsvalue_equals` call pair; that pair plus its memcmp was ~21% of gc-handoff/apps/interp.ts. When one operand is a string literal both of a string's runtime representations are known at compile time — the pooled StringHeader pointer and, for <= 5 bytes, the canonical SSO immediate — so identity settles the true case in one icmp, a non-STRING_TAG operand is decided false by tag alone, and a heap string is filtered by byte_len plus its first and last byte before any call. The no-literal string arms gain the two shortcuts that need no compile-time facts (identical bits, SSO x SSO), keeping their existing fallbacks. Claude-Session: https://claude.ai/code/session_015JgLM9UWGa6WAMix7CvhQJ --- .../7768-inline-strict-string-equality.md | 48 +++ crates/perry-codegen/src/expr/compare.rs | 366 +++++++++++++++++- .../perry-codegen/src/expr/compare_tests.rs | 183 +++++++++ crates/perry-codegen/src/expr/mod.rs | 2 + .../test_strict_eq_string_literal_inline.ts | 126 ++++++ 5 files changed, 708 insertions(+), 17 deletions(-) create mode 100644 changelog.d/7768-inline-strict-string-equality.md create mode 100644 crates/perry-codegen/src/expr/compare_tests.rs create mode 100644 test-files/test_strict_eq_string_literal_inline.ts diff --git a/changelog.d/7768-inline-strict-string-equality.md b/changelog.d/7768-inline-strict-string-equality.md new file mode 100644 index 0000000000..771ae4d6e3 --- /dev/null +++ b/changelog.d/7768-inline-strict-string-equality.md @@ -0,0 +1,48 @@ +### Strict `===` against a string literal is no longer a runtime call + +`n.kind === "num"` — the shape every tree-walking interpreter, reducer and +discriminated-union dispatch is built out of — compiled to a `js_eq` → +`js_jsvalue_equals` call pair. On `gc-handoff/apps/interp.ts` that pair plus the +`memcmp` under it was **~21% of the program's runtime**, against 30% for the +user code itself. + +The call was never necessary. When one operand is a string literal, *both* of a +string's runtime representations are known at compile time, so the whole +dispatch folds into a few integer ops: + +* the pooled heap `StringHeader` (one per literal per module — `crate::strings` + hoists literals to module init), so **pointer identity** settles the true case + in a single `icmp`. `{ kind: "num" }` stores that very pointer, and GC + evacuation rewrites the pool root and the object slot together, so identity + survives collection; +* the SSO immediate — a compile-time constant for literals of ≤ 5 bytes. + `charAt` and `JSON.parse` produce inline `SHORT_STRING_TAG` values, and + `"+" === "+"` across those two representations still has to be true; +* every other NaN-box tag is a different ECMAScript value, so a number, an + int32, a pointer (including a boxed `new String("x")`), a bigint, + null/undefined/bool, or an SSO value with different bytes is decided *false* + without touching memory; +* a heap string that is not the pooled pointer is compared by `byte_len` and by + its first and last byte — all three compile-time constants, all inside bytes + the length check has already proved the header owns. For literals of ≤ 2 bytes + that settles it outright; only a same-length, same-endpoints heap string + reaches `js_string_equals`. + +The two string-equality arms with *no* literal operand (`names[i] === name`) +gained the two shortcuts that need no compile-time facts: identical bits, and +SSO × SSO with differing bits (the SSO encoding is canonical, so equal content +*is* equal bits). The pre-existing fallbacks are unchanged, which matters for +the legacy arm — it keeps `js_get_string_pointer_unified`'s number-coercing +behaviour for operands whose `string` annotation lies. That composition +*materializes* an SSO operand onto the heap, so routing SSO × SSO around it +removes two throwaway allocations per comparison as well as the calls. + +Semantics are exact `===`, not the approximation the old `both_strings` arm +reached through `js_get_string_pointer_unified`: `NaN !== NaN`, `+0 === -0`, +distinct heap strings with equal contents are equal, int32 and double +representations of the same Number are equal, and `new String("num") !== "num"`. +`test-files/test_strict_eq_string_literal_inline.ts` pins those against Node, +including the multi-byte-UTF-8 cases where "first byte" is not "first +character". `expr/compare_tests.rs` is the IR census — the `streqlit.*` blocks +present *and* `js_eq` absent — with negatives for loose `==` (which coerces, and +must keep its helper) and for a comparison with no literal operand. diff --git a/crates/perry-codegen/src/expr/compare.rs b/crates/perry-codegen/src/expr/compare.rs index 42cfa2be66..b5458401ba 100644 --- a/crates/perry-codegen/src/expr/compare.rs +++ b/crates/perry-codegen/src/expr/compare.rs @@ -13,7 +13,7 @@ use crate::type_analysis::{ expr_may_return_boxed_value_from_raw_f64_fallback, is_bigint_expr, is_bool_expr, is_numeric_expr, is_string_expr, }; -use crate::types::{DOUBLE, I32, I64}; +use crate::types::{DOUBLE, I1, I32, I64, I8}; use super::{lower_expr, unbox_str_handle, unbox_to_i64, FnCtx}; @@ -70,6 +70,307 @@ fn canonical_str_cmp_dispatch( .phi(I32, &[(&res_heap, &heap_pred), (&res_boxed, &boxed_pred)]) } +/// `StringHeader` field offsets, duplicated from +/// `perry-runtime::string::STRING_HEADER_ABI_MATCHES_CODEGEN` (which asserts +/// them at the definition, so a layout change fails the runtime build rather +/// than silently miscompiling these loads). The same three numbers +/// `lower_string_method/char_code_at.rs` pins. +const STRING_HEADER_BYTE_LEN_OFFSET: &str = "4"; +const STRING_HEADER_SIZE: usize = 20; + +/// The SSO (`SHORT_STRING_TAG`) immediate for `bytes`, or `None` when the +/// literal is too long to have one. The encoding is canonical — length in bits +/// 40..=47, bytes little-endian in bits 0..=39, everything else zero — which is +/// what `JSValue::try_short_string` builds and what `js_jsvalue_equals`'s "both +/// SSO ⇒ the bits decide" fast path already relies on. +pub(super) fn sso_immediate(bytes: &[u8]) -> Option { + if bytes.len() > 5 { + return None; + } + let mut payload = 0u64; + for (i, &b) in bytes.iter().enumerate() { + payload |= (b as u64) << (i * 8); + } + Some(crate::nanbox::SHORT_STRING_TAG | ((bytes.len() as u64) << 40) | payload) +} + +/// LLVM `i8` literals are signed, so a byte >= 0x80 must be written in its +/// two's-complement form. +pub(super) fn i8_literal(b: u8) -> String { + (b as i8).to_string() +} + +/// Inline ECMAScript `===` against a compile-time string literal. +/// +/// The motivating shape is a tree-walking interpreter's tag dispatch — +/// `n.kind === "num"`, `n.op === "+"` — where the operand is `any`-typed, so +/// every comparison became a `js_eq` -> `js_jsvalue_equals` call pair (~21% of +/// `gc-handoff/apps/interp.ts`). The literal side makes the dispatch decidable +/// inline, because *both* of a string's runtime representations are known at +/// compile time: +/// +/// * the pooled heap `StringHeader` — one per literal per module, see +/// `crate::strings` — so **pointer identity** settles the true case in one +/// `icmp`. Every `{ kind: "num" }` object literal stores that same pooled +/// pointer, and GC evacuation rewrites the pool root and the object slot +/// together, so identity survives collection; +/// * the SSO immediate, a compile-time constant for literals of <= 5 bytes. +/// `charAt` and `JSON.parse` hand back SSO values, and `"+" === "+"` across +/// those two representations has to be true. +/// +/// Everything else is decided by type: a value whose tag is not `STRING_TAG` +/// can never be `===` a string (a boxed `new String("x")` is `POINTER_TAG`, and +/// correctly unequal), and a heap string whose `byte_len` or whose first / last +/// byte differs from the literal's is unequal without reading a byte the length +/// check has not already proved the header owns. Only a same-length, +/// same-endpoints heap string reaches `js_string_equals`. +/// +/// Returns an `i1` that is true iff the two operands are `===`. +fn lower_string_literal_strict_eq( + ctx: &mut FnCtx<'_>, + val: &str, + lit_box: &str, + lit: &str, +) -> String { + let bytes = lit.as_bytes().to_vec(); + let n = bytes.len(); + + let bits = ctx.block().bitcast_double_to_i64(val); + let lit_bits = ctx.block().bitcast_double_to_i64(lit_box); + + // Blocks, in the order control flows through them. Only the ones this + // literal's length needs are created — an empty block would have no + // terminator and fail the LLVM verifier. + let sso_idx = sso_immediate(&bytes).map(|_| ctx.new_block("streqlit.sso")); + let tag_idx = ctx.new_block("streqlit.tag"); + let len_idx = ctx.new_block("streqlit.len"); + let b0_idx = (n >= 1).then(|| ctx.new_block("streqlit.b0")); + let bl_idx = (n >= 2).then(|| ctx.new_block("streqlit.bl")); + let slow_idx = (n >= 3).then(|| ctx.new_block("streqlit.slow")); + let true_idx = ctx.new_block("streqlit.true"); + let false_idx = ctx.new_block("streqlit.false"); + let merge_idx = ctx.new_block("streqlit.merge"); + + let tag_l = ctx.block_label(tag_idx); + let len_l = ctx.block_label(len_idx); + let true_l = ctx.block_label(true_idx); + let false_l = ctx.block_label(false_idx); + let merge_l = ctx.block_label(merge_idx); + let sso_l = sso_idx.map(|i| ctx.block_label(i)); + let b0_l = b0_idx.map(|i| ctx.block_label(i)); + let bl_l = bl_idx.map(|i| ctx.block_label(i)); + let slow_l = slow_idx.map(|i| ctx.block_label(i)); + + // Entry: pooled-pointer identity. This is the hot true case — the value + // under test and the literal are the same pool entry. + let ident = ctx.block().icmp_eq(I64, &bits, &lit_bits); + let after_ident = sso_l.clone().unwrap_or_else(|| tag_l.clone()); + ctx.block().cond_br(&ident, &true_l, &after_ident); + + // SSO immediate: equal => true. SSO but a *different* immediate => the + // encoding is canonical, so the contents differ; the tag block below + // reports that as false, since SSO is not `STRING_TAG`. + if let Some(idx) = sso_idx { + let imm = crate::nanbox::i64_literal(sso_immediate(&bytes).unwrap()); + ctx.current_block = idx; + let sso_eq = ctx.block().icmp_eq(I64, &bits, &imm); + ctx.block().cond_br(&sso_eq, &true_l, &tag_l); + } + + // Neither the pooled pointer nor the SSO form: only a *heap* string can + // still be equal. Every other tag — number, int32, pointer (including a + // boxed String wrapper), bigint, null/undefined/bool, SSO with different + // bytes — is a different ECMAScript value. + ctx.current_block = tag_idx; + let tag = ctx.block().lshr(I64, &bits, "48"); + let is_heap = ctx + .block() + .icmp_eq(I64, &tag, crate::nanbox::STRING_TAG_TOP16_I64); + let hp = ctx.block().and(I64, &bits, POINTER_MASK_I64); + // The floor `safe_load_i32_from_ptr` uses: a `STRING_TAG` value with a null + // or tiny payload is not a dereferenceable header. + let hp_ok = ctx.block().icmp_ugt(I64, &hp, "4095"); + let heap_ok = ctx.block().and(I1, &is_heap, &hp_ok); + ctx.block().cond_br(&heap_ok, &len_l, &false_l); + + // `byte_len` is the pool's `value.len()`, hence a compile-time constant. + ctx.current_block = len_idx; + let hdr_ptr = ctx.block().inttoptr(I64, &hp); + let blen_ptr = ctx + .block() + .gep_inbounds(I8, &hdr_ptr, &[(I64, STRING_HEADER_BYTE_LEN_OFFSET)]); + let blen = ctx.block().load(I32, &blen_ptr); + let len_ok = ctx.block().icmp_eq(I32, &blen, &n.to_string()); + let after_len = b0_l.clone().unwrap_or_else(|| true_l.clone()); + ctx.block().cond_br(&len_ok, &after_len, &false_l); + + // First and last byte. Both sit inside the `n` bytes the length check just + // proved this header owns, so the loads need no further guard. For n <= 2 + // they settle the answer outright. + if let Some(idx) = b0_idx { + ctx.current_block = idx; + let off = STRING_HEADER_SIZE.to_string(); + let p = ctx.block().gep_inbounds(I8, &hdr_ptr, &[(I64, &off)]); + let b = ctx.block().load(I8, &p); + let ok = ctx.block().icmp_eq(I8, &b, &i8_literal(bytes[0])); + let next = bl_l.clone().unwrap_or_else(|| true_l.clone()); + ctx.block().cond_br(&ok, &next, &false_l); + } + if let Some(idx) = bl_idx { + ctx.current_block = idx; + let off = (STRING_HEADER_SIZE + n - 1).to_string(); + let p = ctx.block().gep_inbounds(I8, &hdr_ptr, &[(I64, &off)]); + let b = ctx.block().load(I8, &p); + let ok = ctx.block().icmp_eq(I8, &b, &i8_literal(bytes[n - 1])); + let next = slow_l.clone().unwrap_or_else(|| true_l.clone()); + ctx.block().cond_br(&ok, &next, &false_l); + } + + // Same length, same endpoints, different pointer: a real content compare. + // Both operands are proven heap `StringHeader*` here, so this is the narrow + // two-pointer helper, not the generic value-equality tower. + let slow_arm = slow_idx.map(|idx| { + ctx.current_block = idx; + let rp = ctx.block().and(I64, &lit_bits, POINTER_MASK_I64); + let res = ctx + .block() + .call(I32, "js_string_equals", &[(I64, &hp), (I64, &rp)]); + let bit = ctx.block().icmp_ne(I32, &res, "0"); + let pred = ctx.block().label.clone(); + ctx.block().br(&merge_l); + (bit, pred) + }); + + ctx.current_block = true_idx; + let true_pred = ctx.block().label.clone(); + ctx.block().br(&merge_l); + ctx.current_block = false_idx; + let false_pred = ctx.block().label.clone(); + ctx.block().br(&merge_l); + + ctx.current_block = merge_idx; + let mut incoming: Vec<(&str, &str)> = vec![("true", &true_pred), ("false", &false_pred)]; + if let Some((bit, pred)) = slow_arm.as_ref() { + incoming.push((bit, pred)); + } + ctx.block().phi(I1, &incoming) +} + +/// Inline prefix for the `===`/`!==` string arms that have **no** literal +/// operand — `names[i] === name` in an environment lookup, say. +/// +/// Two cases are settled without leaving the function, and both were paying a +/// runtime call before: +/// +/// * identical bits. True for a pooled literal against itself and, more +/// importantly, for SSO vs SSO: `charAt` and `JSON.parse` hand back inline +/// values whose encoding is canonical, so equal content *is* equal bits; +/// * both operands SSO with different bits => different content, again by +/// canonicality. +/// +/// The remaining arms are exactly what each caller emitted before, so this is +/// behaviour-preserving. That matters most for `legacy_unified`, whose fallback +/// keeps the `js_get_string_pointer_unified` composition — including its +/// number-coercing behaviour for operands whose `string` annotation lies. Note +/// that composition *materializes* an SSO operand onto the heap, so routing +/// SSO x SSO around it removes two allocations per comparison as well as the +/// calls. +/// +/// Returns an `i32` that is 1 iff the operands are `===`. +fn lower_string_strict_eq_inline( + ctx: &mut FnCtx<'_>, + l: &str, + r: &str, + legacy_unified: bool, +) -> String { + let l_bits = ctx.block().bitcast_double_to_i64(l); + let r_bits = ctx.block().bitcast_double_to_i64(r); + + let tag_idx = ctx.new_block("streq.tag"); + let heap_idx = ctx.new_block("streq.heap"); + let sso_idx = ctx.new_block("streq.ssochk"); + let boxed_idx = ctx.new_block("streq.boxed"); + let true_idx = ctx.new_block("streq.true"); + let false_idx = ctx.new_block("streq.false"); + let merge_idx = ctx.new_block("streq.merge"); + let tag_l = ctx.block_label(tag_idx); + let heap_l = ctx.block_label(heap_idx); + let sso_l = ctx.block_label(sso_idx); + let boxed_l = ctx.block_label(boxed_idx); + let true_l = ctx.block_label(true_idx); + let false_l = ctx.block_label(false_idx); + let merge_l = ctx.block_label(merge_idx); + + let ident = ctx.block().icmp_eq(I64, &l_bits, &r_bits); + ctx.block().cond_br(&ident, &true_l, &tag_l); + + ctx.current_block = tag_idx; + let l_tag = ctx.block().lshr(I64, &l_bits, "48"); + let r_tag = ctx.block().lshr(I64, &r_bits, "48"); + let l_heap = ctx + .block() + .icmp_eq(I64, &l_tag, crate::nanbox::STRING_TAG_TOP16_I64); + let r_heap = ctx + .block() + .icmp_eq(I64, &r_tag, crate::nanbox::STRING_TAG_TOP16_I64); + let both_heap = ctx.block().and(I1, &l_heap, &r_heap); + ctx.block().cond_br(&both_heap, &heap_l, &sso_l); + + ctx.current_block = heap_idx; + let lh = ctx.block().and(I64, &l_bits, POINTER_MASK_I64); + let rh = ctx.block().and(I64, &r_bits, POINTER_MASK_I64); + let heap_res = ctx + .block() + .call(I32, "js_string_equals", &[(I64, &lh), (I64, &rh)]); + let heap_pred = ctx.block().label.clone(); + ctx.block().br(&merge_l); + + ctx.current_block = sso_idx; + let l_sso = ctx + .block() + .icmp_eq(I64, &l_tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64); + let r_sso = ctx + .block() + .icmp_eq(I64, &r_tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64); + let both_sso = ctx.block().and(I1, &l_sso, &r_sso); + ctx.block().cond_br(&both_sso, &false_l, &boxed_l); + + ctx.current_block = boxed_idx; + let boxed_res = if legacy_unified { + let lu = ctx + .block() + .call(I64, "js_get_string_pointer_unified", &[(DOUBLE, l)]); + let ru = ctx + .block() + .call(I64, "js_get_string_pointer_unified", &[(DOUBLE, r)]); + ctx.block() + .call(I32, "js_string_equals", &[(I64, &lu), (I64, &ru)]) + } else { + ctx.block() + .call(I32, "js_jsvalue_equals", &[(DOUBLE, l), (DOUBLE, r)]) + }; + let boxed_pred = ctx.block().label.clone(); + ctx.block().br(&merge_l); + + ctx.current_block = true_idx; + let true_pred = ctx.block().label.clone(); + ctx.block().br(&merge_l); + ctx.current_block = false_idx; + let false_pred = ctx.block().label.clone(); + ctx.block().br(&merge_l); + + ctx.current_block = merge_idx; + ctx.block().phi( + I32, + &[ + ("1", &true_pred), + ("0", &false_pred), + (&heap_res, &heap_pred), + (&boxed_res, &boxed_pred), + ], + ) +} + pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::Compare { op, left, right } => { @@ -208,6 +509,46 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ); return Ok(blk.bitcast_i64_to_double(&tagged)); } + // Strict equality against a string LITERAL. Decidable inline for + // every runtime shape (see `lower_string_literal_strict_eq`), so it + // pre-empts all the arms below — including the `js_eq` tail that an + // `any`-typed operand like `n.kind` would otherwise take, one call + // pair per comparison. Strict only: loose `==` coerces (`"5" == 5`) + // and stays on `js_loose_eq`. `Expr::WtfString` is excluded — its + // pool bytes are the WTF-8 encoding, not `str::as_bytes`. + let lit_on_right = matches!(right.as_ref(), Expr::String(_)); + let lit_on_left = !lit_on_right && matches!(left.as_ref(), Expr::String(_)); + if (lit_on_right || lit_on_left) && matches!(op, CompareOp::Eq | CompareOp::Ne) { + // Source order: the non-literal operand may have side effects. + let l = lower_expr(ctx, left)?; + let r = lower_expr(ctx, right)?; + let (val, lit_box, lit) = if lit_on_right { + let Expr::String(s) = right.as_ref() else { + unreachable!("lit_on_right implies Expr::String") + }; + (l, r, s.clone()) + } else { + let Expr::String(s) = left.as_ref() else { + unreachable!("lit_on_left implies Expr::String") + }; + (r, l, s.clone()) + }; + let bit = lower_string_literal_strict_eq(ctx, &val, &lit_box, &lit); + let blk = ctx.block(); + let bit_final = if matches!(op, CompareOp::Ne) { + blk.xor(I1, &bit, "true") + } else { + bit + }; + let tagged = blk.select( + I1, + &bit_final, + I64, + crate::nanbox::TAG_TRUE_I64, + crate::nanbox::TAG_FALSE_I64, + ); + return Ok(blk.bitcast_i64_to_double(&tagged)); + } // "One side is statically string, other is unknown" // fallback: `c === Color.Red` where Color is a const // object. Neither js_eq (bit-compare, wrong for string @@ -368,14 +709,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { { let l = lower_expr(ctx, left)?; let r = lower_expr(ctx, right)?; - let i32_eq = canonical_str_cmp_dispatch( - ctx, - &l, - &r, - "js_string_equals", - "js_jsvalue_equals", - "streq", - ); + let i32_eq = lower_string_strict_eq_inline(ctx, &l, &r, false); let blk = ctx.block(); let bit = blk.icmp_ne(I32, &i32_eq, "0"); let bit_final = if matches!(op, CompareOp::Ne | CompareOp::LooseNe) { @@ -400,18 +734,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { { let l = lower_expr(ctx, left)?; let r = lower_expr(ctx, right)?; - let blk = ctx.block(); // Issue #214: SSO-safe unbox — the inline mask returns // garbage for SHORT_STRING_TAG values (e.g. SSO results // from `JSON.parse('["hello"]')[0]`), causing // `js_string_equals` to deref the inline payload bytes. - let l_handle = unbox_str_handle(blk, &l); - let r_handle = unbox_str_handle(blk, &r); - let i32_eq = blk.call( - I32, - "js_string_equals", - &[(I64, &l_handle), (I64, &r_handle)], - ); + // That unbox is now the *fallback* arm: identical bits and + // SSO x SSO are answered inline, which is what keeps a pair of + // short runtime strings (`charAt`, `substring`) from + // materializing two throwaway heap copies per comparison. + let i32_eq = lower_string_strict_eq_inline(ctx, &l, &r, true); + let blk = ctx.block(); let bit = blk.icmp_ne(I32, &i32_eq, "0"); let bit_final = if matches!(op, CompareOp::Ne | CompareOp::LooseNe) { blk.xor(crate::types::I1, &bit, "true") diff --git a/crates/perry-codegen/src/expr/compare_tests.rs b/crates/perry-codegen/src/expr/compare_tests.rs new file mode 100644 index 0000000000..7d5a4d7576 --- /dev/null +++ b/crates/perry-codegen/src/expr/compare_tests.rs @@ -0,0 +1,183 @@ +//! IR-census + encoding tests for the inline strict-equality lowering. +//! +//! The lowering's whole value is that a `===` against a string literal stops +//! being a call, so the assertion that matters is a census: the `streqlit.*` +//! blocks are present AND `js_eq` is gone. A fast path that is implemented but +//! never reached still compiles, still prints the right answer, and still +//! produces a different object file (CLAUDE.md, "a gate must assert its subject +//! was live") — only the emitted labels tell the two apart. +//! +//! Every positive is paired with a negative that must keep the call, so an arm +//! that quietly widened — to loose `==`, which coerces, or to a comparison with +//! no literal operand at all, where none of the compile-time facts hold — fails +//! here rather than in the gap suite. + +use super::compare::{i8_literal, sso_immediate}; +use perry_hir::types::Type; +use perry_hir::{CompareOp, Expr, Stmt}; + +/// Module-init statements compile into `main` for an entry module, and +/// `main_ir_for` returns exactly that function's slice — so every `contains` / +/// `!contains` below is scoped to the code under test instead of the whole +/// module. Shared with the #6951 temp-root family rather than re-spelling its +/// ~90-line `CompileOptions` / `Module` harness. +use crate::temp_root_coverage::main_ir_for as ir_for; + +const X: u32 = 1; +const Y: u32 = 2; +const R: u32 = 3; + +/// `let x: any = "seed"; let y: any = "other"; let r: any = op ;` +fn cmp_ir(name: &str, op: CompareOp, lhs: Expr, rhs: Expr) -> String { + ir_for( + name, + vec![ + Stmt::Let { + id: X, + name: "x".to_string(), + ty: Type::Any, + mutable: true, + init: Some(Expr::String("seed".to_string())), + }, + Stmt::Let { + id: Y, + name: "y".to_string(), + ty: Type::Any, + mutable: true, + init: Some(Expr::String("other".to_string())), + }, + Stmt::Let { + id: R, + name: "r".to_string(), + ty: Type::Any, + mutable: true, + init: Some(Expr::Compare { + op, + left: Box::new(lhs), + right: Box::new(rhs), + }), + }, + ], + ) +} + +/// A CALL, not the unconditional `declare` line. +const JS_EQ_CALL: &str = "call i64 @js_eq("; +const JS_LOOSE_EQ_CALL: &str = "call i64 @js_loose_eq("; + +#[test] +fn strict_eq_against_a_string_literal_emits_the_inline_dispatch_and_no_js_eq_call() { + let ir = cmp_ir( + "streq_lit", + CompareOp::Eq, + Expr::LocalGet(X), + Expr::String("num".to_string()), + ); + assert!( + ir.contains("streqlit.tag"), + "inline literal dispatch not reached:\n{ir}" + ); + assert!( + !ir.contains(JS_EQ_CALL), + "js_eq call survived the inline literal dispatch:\n{ir}" + ); +} + +#[test] +fn the_literal_may_sit_on_either_side() { + let ir = cmp_ir( + "streq_lit_left", + CompareOp::Eq, + Expr::String("num".to_string()), + Expr::LocalGet(X), + ); + assert!(ir.contains("streqlit.tag"), "{ir}"); + assert!(!ir.contains(JS_EQ_CALL), "{ir}"); +} + +#[test] +fn strict_ne_against_a_string_literal_uses_the_same_dispatch() { + let ir = cmp_ir( + "strne_lit", + CompareOp::Ne, + Expr::LocalGet(X), + Expr::String("num".to_string()), + ); + assert!(ir.contains("streqlit.tag"), "{ir}"); + assert!(!ir.contains(JS_EQ_CALL), "{ir}"); +} + +/// Negative pair #1. Loose `==` coerces (`"5" == 5`), which the inline +/// dispatch does not implement, so it must stay on the runtime helper. +#[test] +fn loose_eq_against_a_string_literal_keeps_the_coercing_runtime_call() { + let ir = cmp_ir( + "looseeq_lit", + CompareOp::LooseEq, + Expr::LocalGet(X), + Expr::String("num".to_string()), + ); + assert!( + !ir.contains("streqlit.tag"), + "loose == was captured by the strict-only literal dispatch:\n{ir}" + ); + assert!( + ir.contains(JS_LOOSE_EQ_CALL), + "loose == lost its coercing helper:\n{ir}" + ); +} + +/// Negative pair #2. With no literal operand there is no compile-time pooled +/// pointer, no compile-time SSO immediate and no compile-time length, so the +/// literal dispatch must not appear. +#[test] +fn strict_eq_between_two_any_locals_does_not_use_the_literal_dispatch() { + let ir = cmp_ir( + "streq_nolit", + CompareOp::Eq, + Expr::LocalGet(X), + Expr::LocalGet(Y), + ); + assert!( + !ir.contains("streqlit.tag"), + "literal dispatch fired without a literal operand:\n{ir}" + ); + assert!( + ir.contains(JS_EQ_CALL), + "the no-literal strict-equality fallback disappeared:\n{ir}" + ); +} + +/// The SSO immediate is hand-built here but consumed by `perry-runtime`'s +/// canonical encoding (`JSValue::try_short_string`): tag `0x7FF9` in bits +/// 48..=63, byte length in bits 40..=47, bytes little-endian in bits 0..=39, +/// every other bit zero. `perry-codegen` cannot depend on `perry-runtime`, so +/// these pin the layout numerically. If they drift, `"+" === "+"` between a +/// `charAt` result and a literal silently becomes false. +#[test] +fn sso_immediate_matches_the_runtime_encoding() { + assert_eq!(sso_immediate(b""), Some(0x7FF9_0000_0000_0000)); + assert_eq!(sso_immediate(b"+"), Some(0x7FF9_0100_0000_002B)); + assert_eq!(sso_immediate(b"if"), Some(0x7FF9_0200_0000_6669)); + // 'n' = 0x6E (byte 0, bits 0..8), 'u' = 0x75, 'm' = 0x6D (byte 2). + assert_eq!(sso_immediate(b"num"), Some(0x7FF9_0300_006D_756E)); + assert_eq!(sso_immediate(b"abcde"), Some(0x7FF9_0565_6463_6261)); +} + +#[test] +fn sso_immediate_declines_anything_longer_than_the_inline_payload() { + assert_eq!(sso_immediate(b"abcdef"), None); + assert_eq!(sso_immediate(b"parse error"), None); +} + +/// LLVM integer literals are signed, so a high byte must be written in two's +/// complement or the `icmp eq i8` never matches. Multi-byte UTF-8 literals +/// ("é" = 0xC3 0xA9) are exactly the case that needs it. +#[test] +fn i8_literal_writes_high_bytes_in_twos_complement() { + assert_eq!(i8_literal(0x00), "0"); + assert_eq!(i8_literal(0x7F), "127"); + assert_eq!(i8_literal(0x80), "-128"); + assert_eq!(i8_literal(0xC3), "-61"); + assert_eq!(i8_literal(0xFF), "-1"); +} diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index e6a6a3f7bc..af8b5f38f5 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -1885,6 +1885,8 @@ pub(crate) mod calls; mod child_proc; mod closure; mod compare; +#[cfg(test)] +mod compare_tests; mod conditional; mod dyn_extern_i18n; mod env_clones; diff --git a/test-files/test_strict_eq_string_literal_inline.ts b/test-files/test_strict_eq_string_literal_inline.ts new file mode 100644 index 0000000000..71a232994f --- /dev/null +++ b/test-files/test_strict_eq_string_literal_inline.ts @@ -0,0 +1,126 @@ +// Strict equality against a string literal is lowered inline (no js_eq call). +// The inline sequence decides by NaN-box tag, by the pooled literal's pointer, +// by the literal's compile-time SSO immediate, and by byte_len + first/last +// byte — so every one of those shortcuts needs an ECMAScript edge pinned here. +// +// The interesting inputs are the ones that are NOT the pooled literal pointer: +// concatenations, substrings, charAt (which yields an inline SSO value), +// JSON.parse, String.fromCharCode. Those are the representations the fast path +// has to reconcile with a heap literal. + +function anyv(x: any): any { + return x; +} + +// ---- same content, different representation -------------------------------- +const heapNum: any = anyv("nu" + "m"); // runtime-built heap string +const subNum: any = anyv("xnumx".substring(1, 4)); +const jsonNum: any = anyv(JSON.parse('"num"')); +const codeNum: any = anyv(String.fromCharCode(110, 117, 109)); +console.log(heapNum === "num", subNum === "num", jsonNum === "num", codeNum === "num"); +console.log("num" === heapNum, heapNum !== "num"); + +// charAt yields a 1-byte value; "+" is a 1-byte literal. +const plus: any = anyv("a+b".charAt(1)); +const minus: any = anyv("a-b".charAt(1)); +console.log(plus === "+", minus === "+", plus === "-", plus !== "+"); + +// ---- same length, differing first / last / middle byte --------------------- +const bin: any = anyv("bi" + "n"); +console.log(bin === "num", bin === "bin", bin === "bit"); +const long1: any = anyv("abcd" + "efg"); +console.log(long1 === "abcdefg", long1 === "abcXefg", long1 === "abcdefX", long1 === "Xbcdefg"); +console.log(long1 === "abcdef", long1 === "abcdefgh"); + +// ---- the empty string ------------------------------------------------------ +const empty: any = anyv("ab".substring(0, 0)); +console.log(empty === "", empty === "a", "" === empty, empty !== ""); + +// ---- NaN and signed zero --------------------------------------------------- +const nan: any = anyv(NaN); +const negZero: any = anyv(-0); +console.log(nan === nan, nan !== nan, NaN === NaN); +console.log(negZero === 0, 0 === negZero, negZero === -0, Object.is(negZero, -0)); +console.log(Math.sqrt(-1) === Math.sqrt(-1)); + +// ---- typed vs boxed numeric representations -------------------------------- +// (3 | 0) is an int32-tagged value; 3.0 is a plain double. Same Number. +const asInt: any = anyv(3 | 0); +const asDouble: any = anyv(3.0); +const fromArray: any = anyv([3][0]); +console.log(asInt === asDouble, asDouble === asInt, asInt === fromArray, asInt === 3); +console.log(asInt === "3", "3" === asInt, asInt === 4); + +// ---- cross-type: nothing non-string is === a string ------------------------ +console.log( + (5 as any) === "5", + (true as any) === "true", + (null as any) === "null", + (undefined as any) === "undefined", +); +console.log(null === undefined, (null as any) !== undefined, undefined === undefined); +const boxed: any = anyv(new String("num")); +console.log(boxed === "num", String("num") === "num", boxed == "num"); +const sym: any = anyv(Symbol("num")); +console.log(sym === "num", sym === sym); +const big: any = anyv(BigInt(3)); +console.log(big === "3", big === 3, big === BigInt(3)); + +// ---- object identity ------------------------------------------------------- +const o1: any = anyv({ kind: "num" }); +const o2: any = anyv({ kind: "num" }); +console.log(o1 === o1, o1 === o2, o1 === "num", o1.kind === "num", o2.kind === o1.kind); +const arr: any = anyv([1, 2]); +console.log(arr === arr, arr === "1,2"); + +// ---- multi-byte UTF-8: first/last BYTE, not first/last character ----------- +const acc: any = anyv("é" + ""); +console.log(acc === "é", acc === "è", acc === "e"); +const emoji: any = anyv("\u{1F600}" + ""); +console.log(emoji === "\u{1F600}", emoji === "\u{1F601}"); +const cjk: any = anyv("日本" + ""); +console.log(cjk === "日本", cjk === "日月", cjk === "月本"); + +// ---- the interpreter's own shape: union tag dispatch ----------------------- +type Node = + | { kind: "num"; num: number } + | { kind: "str"; str: string } + | { kind: "bin"; op: string; left: Node; right: Node }; + +function describe(n: Node): string { + if (n.kind === "num") return "N" + n.num; + if (n.kind === "str") return "S" + n.str; + if (n.op === "+") return "(" + describe(n.left) + "+" + describe(n.right) + ")"; + if (n.op === "*") return "(" + describe(n.left) + "*" + describe(n.right) + ")"; + return "?"; +} + +const tree: Node = { + kind: "bin", + op: "+", + left: { kind: "num", num: 1 }, + right: { kind: "bin", op: "*", left: { kind: "str", str: "x" }, right: { kind: "num", num: 2 } }, +}; +console.log(describe(tree)); + +// The op strings here are built at runtime, so they are NOT the pooled literal. +const dynOp: string = ["+", "-", "*"][1]; +console.log(dynOp === "-", dynOp === "+", dynOp !== "-"); + +// ---- switch/case over the same literals (a second lowering of ===) --------- +function classify(s: string): number { + switch (s) { + case "num": + return 1; + case "str": + return 2; + case "bin": + return 3; + default: + return 0; + } +} +console.log(classify(heapNum), classify("str"), classify(subNum), classify("zzz")); + +// ---- literal vs literal ---------------------------------------------------- +console.log("num" === "num", "num" === "bin", "num" !== "bin", "if" === "if", "" === ""); From efc4a0282766e0e800f08ec01fa968fa46a49fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 14:44:02 +0200 Subject: [PATCH 2/4] test(codegen): the no-literal negatives need genuinely any-typed operands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initializing an `any` local with a string literal refines its static type to `string`, which routes the comparison to the both-strings arm and made both negatives vacuous — they asserted the absence of a dispatch that was never going to fire and the presence of a helper the refined path never calls. Claude-Session: https://claude.ai/code/session_015JgLM9UWGa6WAMix7CvhQJ --- crates/perry-codegen/src/expr/compare_tests.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/perry-codegen/src/expr/compare_tests.rs b/crates/perry-codegen/src/expr/compare_tests.rs index 7d5a4d7576..7ee561456b 100644 --- a/crates/perry-codegen/src/expr/compare_tests.rs +++ b/crates/perry-codegen/src/expr/compare_tests.rs @@ -27,7 +27,7 @@ const X: u32 = 1; const Y: u32 = 2; const R: u32 = 3; -/// `let x: any = "seed"; let y: any = "other"; let r: any = op ;` +/// `let x: any = undefined; let y: any = undefined; let r: any = op ;` fn cmp_ir(name: &str, op: CompareOp, lhs: Expr, rhs: Expr) -> String { ir_for( name, @@ -37,14 +37,18 @@ fn cmp_ir(name: &str, op: CompareOp, lhs: Expr, rhs: Expr) -> String { name: "x".to_string(), ty: Type::Any, mutable: true, - init: Some(Expr::String("seed".to_string())), + // `undefined`, not a string literal: initializing an `any` + // local with a string refines its static type to `string`, + // which routes the comparison to the both-strings arm and + // makes the two negatives below vacuous. + init: Some(Expr::Undefined), }, Stmt::Let { id: Y, name: "y".to_string(), ty: Type::Any, mutable: true, - init: Some(Expr::String("other".to_string())), + init: Some(Expr::Undefined), }, Stmt::Let { id: R, From 1585b088a1b0f9bb1ebb3d7841f9303d0d30aad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 14:56:21 +0200 Subject: [PATCH 3/4] docs: key the changelog fragment to PR #7767 Claude-Session: https://claude.ai/code/session_015JgLM9UWGa6WAMix7CvhQJ --- ...t-string-equality.md => 7767-inline-strict-string-equality.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{7768-inline-strict-string-equality.md => 7767-inline-strict-string-equality.md} (100%) diff --git a/changelog.d/7768-inline-strict-string-equality.md b/changelog.d/7767-inline-strict-string-equality.md similarity index 100% rename from changelog.d/7768-inline-strict-string-equality.md rename to changelog.d/7767-inline-strict-string-equality.md From da3fd4a1c8ec1a3dc0e51a4c03c533edc228fdb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 15:42:26 +0200 Subject: [PATCH 4/4] chore: bump version to 0.5.1449 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a89c718942..8f72839563 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1448 +**Current Version:** 0.5.1449 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 79ca5025eb..3f1fdf3518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1448" +version = "0.5.1449" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1448" +version = "0.5.1449" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1448" +version = "0.5.1449" [[package]] name = "perry-ui-tvos" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1448" +version = "0.5.1449" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index bdc607ee79..162baa1092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1448" +version = "0.5.1449" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"