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
14 changes: 4 additions & 10 deletions Sources/SwiflowUI/DataTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,10 @@ public func DataTable<Row: Identifiable>(
}
}

/// Clamp a pixel quantity to a trap-free `Int`. `Int(_:)` on a Double past
/// ±2^31 TRAPS on wasm32 (host Int is 64-bit and HIDES it — the class that
/// bit the query clock, PR #154), and raw Int multiplication overflows the
/// same bound. CSS lengths beyond ~2^31 px are meaningless anyway (browsers
/// cap element sizes far lower), so clamping identically on EVERY platform
/// keeps host tests honest about wasm behavior. Non-finite inputs → 0.
func cssPixelInt(_ value: Double) -> Int {
guard !value.isNaN else { return 0 }
return Int(max(0, min(value, 2_147_483_000))) // just under Int32.max; ±∞ clamp naturally
}
/// Clamp a pixel quantity to a trap-free `Int` — see `WasmSafeInt.pixelClamp`
/// for the wasm32 32-bit-`Int` rationale. Kept as a named alias because the
/// call sites read as pixel math.
func cssPixelInt(_ value: Double) -> Int { WasmSafeInt.pixelClamp(value) }

// MARK: - Erasure (also the test seam)

Expand Down
13 changes: 7 additions & 6 deletions Sources/SwiflowUI/FieldChrome.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,14 @@ func installFieldStyles() { installControlSheet(id: "sw-forms", formControlsShee

/// Trims a trailing `.0` off a whole-number `Double` (`0` → `"0"`, not
/// `"0.0"`), so e.g. `min: 0` emits `min="0"`. Falls back to `String(v)` for
/// fractional values or magnitudes past ±2^31: `Int(v)` beyond that TRAPS on
/// wasm32 (host Int is 64-bit and hides it), and the cutoff must be the same
/// on every platform so host tests see wasm behavior (cf. `cssPixelInt`).
/// Shared by `NumberField` (min/max/step) and `Slider` (range bounds/step) —
/// extracted here at the second consumer rather than duplicated.
/// fractional values or magnitudes past the signed 32-bit range: `Int(v)`
/// beyond that TRAPS on wasm32 (see `WasmSafeInt`), so the whole-number fast
/// path goes through `WasmSafeInt.exact` — identical on every platform, so
/// host tests see wasm behavior. Shared by `NumberField` (min/max/step) and
/// `Slider` (range bounds/step).
func formatControlNumber(_ v: Double) -> String {
v == v.rounded() && v.magnitude < 2_147_483_648 ? String(Int(v)) : String(v)
if v == v.rounded(), let i = WasmSafeInt.exact(v) { return String(i) }
return String(v)
}

/// Deterministically slugs an arbitrary label into a lowercase, hyphenated
Expand Down
37 changes: 37 additions & 0 deletions Sources/SwiflowUI/WasmSafeInt.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Sources/SwiflowUI/WasmSafeInt.swift
//
// One gate for every Double→Int crossing in SwiflowUI. On wasm32 `Int` is
// 32-bit, so `Int(_:)` on a Double outside signed 32-bit range — or NaN/±∞ —
// TRAPS at runtime. Host `Int` is 64-bit and hides it, so a value like an
// epoch-ms slider bound or a multi-billion-row scroll offset passes every
// host test and then kills the release browser. `cssPixelInt` and
// `formatControlNumber` both guard this class; they funnel through
// `WasmSafeInt` here so the trap can't sneak back in via a new call site with
// its own hand-rolled bound.
import Swiflow

enum WasmSafeInt {
/// Signed 32-bit range, as `Double` bounds. `Int32.max`/`.min` are exactly
/// representable in `Double`, so these comparisons are exact.
static let min = -2_147_483_648.0
static let max = 2_147_483_647.0

/// The value as `Int` iff it is finite and lands within signed 32-bit
/// range; else `nil`. The range comparison also rejects NaN (all
/// comparisons with NaN are false) and ±∞, so the `Int(_:)` below is
/// always trap-free — on wasm32 as well as the host.
static func exact(_ value: Double) -> Int? {
guard value >= min, value <= max else { return nil }
return Int(value)
}

/// Clamp a non-negative pixel quantity to a trap-free `Int`. NaN and
/// values below 0 collapse to 0; values past the 32-bit ceiling clamp to
/// just under it (CSS lengths beyond ~2^31 px are meaningless — browsers
/// cap element sizes far lower — so clamping identically on EVERY platform
/// keeps host tests honest about wasm behavior). `+∞` clamps naturally.
static func pixelClamp(_ value: Double) -> Int {
guard !value.isNaN else { return 0 }
return Int(Swift.max(0, Swift.min(value, 2_147_483_000)))
}
}
56 changes: 56 additions & 0 deletions Tests/SwiflowUITests/WasmSafeIntTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Tests/SwiflowUITests/WasmSafeIntTests.swift
//
// The Double→Int gate both cssPixelInt and formatControlNumber funnel
// through. Boundaries are pinned here on the host, where Int is 64-bit — the
// point is that these results are IDENTICAL on wasm32's 32-bit Int (no trap),
// so the host suite is a faithful stand-in.
import Testing
@testable import SwiflowUI

@Suite("WasmSafeInt")
struct WasmSafeIntTests {

@Test("exact accepts the full signed-32-bit range, inclusive of both ends")
func exactInRange() {
#expect(WasmSafeInt.exact(0) == 0)
#expect(WasmSafeInt.exact(-5) == -5)
#expect(WasmSafeInt.exact(2_147_483_647) == 2_147_483_647) // Int32.max
#expect(WasmSafeInt.exact(-2_147_483_648) == -2_147_483_648) // Int32.min — representable, trap-free
}

@Test("exact returns nil just past either end (where Int(_:) would trap on wasm32)")
func exactOutOfRange() {
#expect(WasmSafeInt.exact(2_147_483_648) == nil) // Int32.max + 1
#expect(WasmSafeInt.exact(-2_147_483_649) == nil) // Int32.min − 1
#expect(WasmSafeInt.exact(1e12) == nil) // epoch-ms scale
}

@Test("exact rejects NaN and infinities via the range comparison")
func exactNonFinite() {
#expect(WasmSafeInt.exact(.nan) == nil)
#expect(WasmSafeInt.exact(.infinity) == nil)
#expect(WasmSafeInt.exact(-.infinity) == nil)
}

@Test("exact truncates a fractional value toward zero (caller decides whether that's wanted)")
func exactTruncates() {
#expect(WasmSafeInt.exact(41.9) == 41)
#expect(WasmSafeInt.exact(-41.9) == -41)
}

@Test("pixelClamp floors to a non-negative Int and clamps the 32-bit ceiling")
func pixelClamp() {
#expect(WasmSafeInt.pixelClamp(0) == 0)
#expect(WasmSafeInt.pixelClamp(41.9) == 41) // floor
#expect(WasmSafeInt.pixelClamp(-5) == 0) // pixels are ≥ 0
#expect(WasmSafeInt.pixelClamp(1e12) == 2_147_483_000) // beyond 2^31 → clamp, never trap
#expect(WasmSafeInt.pixelClamp(.infinity) == 2_147_483_000)
#expect(WasmSafeInt.pixelClamp(.nan) == 0)
}

@Test("formatControlNumber integer-formats Int32.min now that exact accepts it")
func formatBoundary() {
#expect(formatControlNumber(-2_147_483_648) == "-2147483648")
#expect(formatControlNumber(2_147_483_648) == String(2_147_483_648.0)) // still Double past the ceiling
}
}
Loading