view release on metacpan or search on metacpan
lib/BarefootJS.pm view on Meta::CPAN
# ---------------------------------------------------------------------------
# JS-compat callees (#1189) â invoked from generated Mojo templates as
# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's
# `templatePrimitives` registry emits these helper calls in place of the
# corresponding JS callees (`JSON.stringify`, `Math.floor`, â¦) so the SSR
# template can render value-equivalent output without a JS engine.
#
# Failure policy mirrors the Go adapter (#1188): user-data marshalling
# (json) bubbles errors so Mojolicious aborts loudly on cycles /
# unsupported values rather than silently producing an empty payload.
# Numeric coercion follows JS semantics (NaN propagates as the special
# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings
# always coerce to a string representation.
# ---------------------------------------------------------------------------
sub json ($self, $value) {
# Mojo::JSON::to_json returns a character string (not bytes), suitable
# for embedding in HTML output via Mojo::ByteStream / `<%==`.
#
# Documented divergence from JS: JS distinguishes `null` (renders as
# "null") from `undefined` (`JSON.stringify(undefined)` returns the
# JS value `undefined`, not a string). Perl has no such distinction
lib/BarefootJS.pm view on Meta::CPAN
sub string ($self, $value) {
# JS `String(v)` mirror. `undef` renders as the empty string here so
# an unset prop doesn't surface as a literal "undefined" / "null"
# in user-facing HTML â same divergence the Go adapter documents
# for `bf_string`.
return defined $value ? "$value" : '';
}
sub number ($self, $value) {
# JS `Number(v)` mirror. Numeric coerces via Perl's implicit
# numeric context; non-numeric / undef yield real numeric NaN
# (`'nan' + 0`) so downstream arithmetic propagates correctly
# (`Math.floor(NaN) === NaN`). Returning the literal string
# "NaN" would conflate the user-passing-the-string-"NaN" case
# with the parse-failure case, and break NaN detection in
# downstream helpers.
return 0 + 'nan' unless defined $value;
return $value + 0 if looks_like_number($value);
return 0 + 'nan';
}
# NaN is the only float for which `$x != $x` holds. Used as the
# portable sentinel check in floor/ceil/round.
sub _is_nan { my $n = shift; return $n != $n }
# True for +/-Infinity. `9**9**9` is Perl's portable infinity literal; a
# finite number is always strictly less than +Inf in magnitude.
sub _is_inf { my $n = shift; return $n == 9**9**9 || $n == -9**9**9 }
sub floor ($self, $value) {
my $n = $self->number($value);
return $n if _is_nan($n);
lib/BarefootJS.pm view on Meta::CPAN
# ---------------------------------------------------------------------------
#
# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`
# share a method name in JS; the JSX parser can't tell the two
# receiver shapes apart without TS type inference, so both lower to
# the same IR node (`array-method` / method `includes`). This helper
# dispatches at the Perl level via `ref()`:
# - ARRAY ref: scan elements with `BarefootJS::Evaluator::_same_value_zero`,
# matching `Array.prototype.includes`'s SameValueZero
# semantics (no cross-type coercion, e.g. `[2].includes("2")`
# is false; NaN matches NaN) â the same algorithm the
# evaluator's serialized-callback path already uses for
# `.includes`, so both positions agree. This used to be a
# stringy `eq` scan, which coerced numbers to strings
# (`[2].includes("2")` was true) and diverged from JS.
# - scalar: `index($recv, $sub) != -1`, with both args
# coerced through `// ''` so an undef receiver /
# needle doesn't trip Perl's substr warning.
# Anything else (HASH ref, code ref) returns false â matches the
# JS semantic where `.includes` is only defined on Array /
# TypedArray / String.
lib/BarefootJS.pm view on Meta::CPAN
else {
push @out, $el;
}
}
return \@out;
}
# `Array.prototype.flat(depth)` with a DYNAMIC depth (#2094) â the depth is
# itself an arbitrary runtime value (e.g. a prop), not a compile-time literal,
# so it must be coerced with JS's `ToIntegerOrInfinity` before delegating to
# `flat` above: truncate toward zero; negative -> 0; NaN / non-numeric -> 0;
# +Infinity or a huge finite value -> flatten fully.
#
# Deliberately a SEPARATE entry point from `flat`, not a smarter version of
# it: the literal-depth path's `-1` argument is a compile-time SENTINEL baked
# into the template source, meaning "the source literally said `Infinity`". A
# genuinely dynamic depth that happens to evaluate to `-1` at render time
# means the OPPOSITE in real JS (`[1,[2]].flat(-1)` never recurses â same as
# `.flat(0)`). Since both paths would otherwise hand the same literal-looking
# argument to one shared function, that function couldn't tell which case
# it's in â so the two stay separate. Mirrors Go's `FlatDynamicDepth` /
lib/BarefootJS.pm view on Meta::CPAN
return $self->flat($recv, _coerce_flat_depth($depth));
}
# _coerce_flat_depth: JS `ToIntegerOrInfinity` for a dynamic `.flat(depth)`
# argument, collapsed to `flat`'s int contract (`-1` == flatten fully).
#
# Perl's scalar type system blurs string/number duality, so `looks_like_number`
# (already the codebase's ToNumber-style coercion check, see BarefootJS::
# Evaluator's `_to_number`) is reused here rather than inventing a new
# convention. `looks_like_number` on this Perl (5.38) already recognises the
# strings "Infinity" / "-Infinity" / "NaN" as numeric (verified empirically:
# `perl -MScalar::Util=looks_like_number -e 'print looks_like_number("Infinity")'`
# prints 1), and `$str + 0` on those strings yields the corresponding Perl
# non-finite double, so no extra string special-casing is needed here.
sub _coerce_flat_depth ($depth) {
return 0 unless defined $depth;
my $f;
if (ref($depth) eq 'JSON::PP::Boolean') {
$f = $depth ? 1 : 0;
}
elsif (!ref($depth) && looks_like_number($depth)) {
$f = $depth + 0;
}
else {
# undef handled above; anything else non-numeric (a plain string
# that isn't a number, a HASH/ARRAY ref, ...) coerces via NaN.
return 0;
}
return 0 if $f != $f; # NaN
return -1 if $f == 9**9**9; # +Infinity -> flatten fully sentinel
return 0 if $f == -(9**9**9); # -Infinity -> 0
my $trunc = int($f); # Perl's int() truncates toward zero
return 0 if $trunc < 0;
return -1 if $trunc > 1_000_000; # huge finite ~= flatten fully
return $trunc;
}
# `Array.prototype.flatMap(fn)` value-returning field projection
# (#1448 Tier C) â map each element through a self / field projection,
lib/BarefootJS.pm view on Meta::CPAN
# `Number.prototype.toFixed(digits)` (#1897) â fixed-decimal string with
# zero-padding. JS rounds the scaled integer half toward +Infinity (the
# spec's "pick the larger n" tie-break), so `(2.5).toFixed(0)` is "3";
# bare `sprintf("%.*f")` would round half-to-even ("2"), diverging. Scale
# by 10**digits, round with `floor(x + 0.5)` (the same tie-break the
# `round` helper uses), then format the exact multiple. A negative
# `digits` clamps to 0, mirroring how the adapters default an omitted
# argument.
sub to_fixed ($self, $value, $digits = 0) {
my $n = $self->number($value);
# JS toFixed returns the STRINGS "NaN" / "Infinity" / "-Infinity" for
# non-finite inputs; the numeric values would stringify per-platform
# ("nan"/"inf"/...) and diverge.
return 'NaN' if _is_nan($n);
return $n < 0 ? '-Infinity' : 'Infinity' if _is_inf($n);
$digits = 0 if !defined $digits || $digits < 0;
my $factor = 10 ** $digits;
my $rounded = POSIX::floor($n * $factor + 0.5);
return sprintf('%.*f', $digits, $rounded / $factor);
}
# `String.prototype.split(sep)` (#1448 Tier B) â string â ARRAY ref.
#
# Two JS-parity wrinkles drive the helper (a bare `split` emit would
lib/BarefootJS/DevReload.pm view on Meta::CPAN
return $content;
}
# The browser snippet: a small IIFE â EventSource subscriber + scrollY
# preservation across reloads. Idempotent across duplicate mounts (the
# window.__bfDevReload guard). Returns a plain HTML string; callers mark it raw
# for their template engine.
sub snippet ($class, $endpoint) {
my $ep = _js_str($endpoint);
my $sk = _js_str($SCROLL_STORAGE_KEY);
return qq{<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;try{var s=sessionStorage.getItem($sk);if(s){sessionStorage.removeItem($sk);var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};if(docu...
}
# A ready-made PSGI app for the SSE endpoint. Streams `event: reload` whenever
# <dist>/.dev/build-id changes, with `: hb` heartbeats in between.
#
# Implemented with the PSGI streaming interface and a blocking poll loop, so it
# holds one worker per open connection for the connection's lifetime â run it
# under a prefork PSGI server (Starman / Starlet) in dev, which is the natural
# choice for an app that also streams (e.g. an AI-chat SSE route). DevReload is
# automatically a no-op unless you mount it, and you should only mount it in
lib/BarefootJS/Evaluator.pm view on Meta::CPAN
$i++;
}
return \@out;
}
# _join_array($obj, $sep): `Array.prototype.join` semantics â non-ARRAY
# receiver -> '' (the evaluator's degrade-rather-than-die convention,
# matching `.includes` above); a `null`/`undefined` (Perl `undef`) element
# joins as '' (not the string "null"); every other element goes through
# `_to_string` (JS `String(x)` â a JS boolean stringifies "true"/"false", a
# number "42"/"NaN"/"Infinity", etc.). Mirrors Go's evalJoin.
sub _join_array ($obj, $sep) {
return '' unless ref $obj eq 'ARRAY';
return join($sep, map { defined $_ ? _to_string($_) : '' } @$obj);
}
# eval_json($json, $env): decode a ParsedExpr JSON string and evaluate it.
# Mirrors the Go EvalExpr entry point. Requires JSON::PP only when used.
sub eval_json ($json, $env) {
require JSON::PP;
my $node = JSON::PP->new->decode($json);
lib/BarefootJS/Evaluator.pm view on Meta::CPAN
$t =~ s/\s+\z//;
return 0 if $t eq '';
return looks_like_number($t) ? ($t + 0) : _nan();
}
return $v + 0;
}
sub _to_string ($v) {
return 'null' if !defined $v;
if (ref $v eq 'JSON::PP::Boolean') { return $v ? 'true' : 'false' }
# JS spells the non-finite doubles "Infinity" / "-Infinity" / "NaN";
# Perl stringifies them "Inf" / "-Inf" / "NaN", so the non-finite cases
# are pinned here to stay JS-faithful (and match the Go evaluator's
# evalToString). Finite numbers and strings fall through to plain
# interpolation.
if (_is_number($v)) {
my $n = $v + 0;
return 'NaN' if $n != $n;
return 'Infinity' if $n == 9**9**9;
return '-Infinity' if $n == -(9**9**9);
}
return "$v";
}
sub _truthy ($v) {
return 0 if !defined $v;
if (ref $v eq 'JSON::PP::Boolean') { return $v ? 1 : 0 }
return 1 if ref $v; # arrays / objects are always truthy in JS
if (_is_string($v)) { return $v ne '' ? 1 : 0 } # incl. the truthy "0"
my $n = $v + 0;
return ($n != 0 && $n == $n) ? 1 : 0; # nonzero and not NaN
}
# _bool: wrap a Perl truthy/falsy into a JS boolean (JSON::PP::Boolean), so
# boolean-valued operators (relational, ===, !, Boolean()) return a real
# boolean rather than 1/0 â matching the Go evaluator's bool (e.g.
# String(a < b) is "true", and `'x' + (a < b)` is "xtrue"). The coercions
# above already treat JSON::PP::Boolean correctly.
sub _bool ($t) { $t ? JSON::PP::true() : JSON::PP::false() }
# ---------------------------------------------------------------------------
lib/BarefootJS/Evaluator.pm view on Meta::CPAN
# numeric addition otherwise.
return _to_string($l) . _to_string($r) if _is_string($l) || _is_string($r);
return _to_number($l) + _to_number($r);
}
return _to_number($l) - _to_number($r) if $op eq '-';
return _to_number($l) * _to_number($r) if $op eq '*';
if ($op eq '/') {
my $ln = _to_number($l);
my $rn = _to_number($r);
# JS division by zero is finite-valued, not an error: x/0 is ±Infinity
# (NaN for 0/0). Perl's native `/` dies on a zero divisor, so guard it
# to stay JS-faithful and match the Go evaluator (Go float division
# already yields ±Inf / NaN).
if ($rn == 0) {
return _nan() if $ln == 0 || $ln != $ln;
return $ln > 0 ? 9**9**9 : -(9**9**9);
}
return $ln / $rn;
}
if ($op eq '%') {
my $rn = _to_number($r);
return _nan() if $rn == 0;
return POSIX::fmod(_to_number($l), $rn);
}
return _relational($op, $l, $r) if $op eq '<' || $op eq '<=' || $op eq '>' || $op eq '>=';
return _bool(_strict_eq($l, $r)) if $op eq '===';
return _bool(!_strict_eq($l, $r)) if $op eq '!==';
# Loose equality / bitwise / shift are out of the subset.
return undef;
}
sub _relational ($op, $l, $r) {
# JS Abstract Relational Comparison: both strings â compare by code unit;
# otherwise coerce both to numbers (a NaN operand makes it false).
my $c;
if (_is_string($l) && _is_string($r)) {
$c = $l lt $r ? -1 : $l gt $r ? 1 : 0;
}
else {
my $ln = _to_number($l);
my $rn = _to_number($r);
return _bool(0) if $ln != $ln || $rn != $rn; # NaN â false
$c = $ln < $rn ? -1 : $ln > $rn ? 1 : 0;
}
return _bool($c < 0) if $op eq '<';
return _bool($c <= 0) if $op eq '<=';
return _bool($c > 0) if $op eq '>';
return _bool($c >= 0) if $op eq '>=';
return _bool(0);
}
sub _strict_eq ($l, $r) {
# Strict `===`: equal JS type and value, no coercion.
my $ln = _is_number($l);
my $rn = _is_number($r);
if ($ln && $rn) {
my ($lf, $rf) = ($l + 0, $r + 0);
return 0 if $lf != $lf || $rf != $rf; # NaN
return $lf == $rf ? 1 : 0;
}
return 0 if $ln != $rn; # one numeric, one not
if (!defined $l) { return !defined $r ? 1 : 0 }
return 0 if !defined $r;
my $lb = ref $l eq 'JSON::PP::Boolean';
my $rb = ref $r eq 'JSON::PP::Boolean';
if ($lb || $rb) {
return 0 unless $lb && $rb;
return ((!!$l) == (!!$r)) ? 1 : 0;
}
return ($l eq $r ? 1 : 0) if _is_string($l) && _is_string($r);
return 0;
}
# _same_value_zero: `Array.prototype.includes` membership test â `===`
# except `NaN` equals itself (and +0/-0 are not distinguished, which the
# JSON-decoded values here can't represent anyway). Reuses `_strict_eq`'s
# type/value rules and only special-cases the two-NaN case that `_strict_eq`
# (deliberately, for `===`) reports as unequal.
sub _same_value_zero ($l, $r) {
if (_is_number($l) && _is_number($r)) {
my ($lf, $rf) = ($l + 0, $r + 0);
return 1 if $lf != $lf && $rf != $rf; # NaN sameValueZero NaN
}
return _strict_eq($l, $r);
}
sub _unary ($op, $v) {
return _bool(!_truthy($v)) if $op eq '!';
return -_to_number($v) if $op eq '-';
return _to_number($v) if $op eq '+';
return undef;
}
lib/BarefootJS/Evaluator.pm view on Meta::CPAN
# matching the existing round helper rather than half-away-from-zero.
sub _math_round ($n) {
return POSIX::floor($n + 0.5);
}
sub _call_builtin ($name, $args) {
if ($name eq 'Math.max') {
my $m = -(9**9**9); # JS Math.max() with no args is -Infinity
for my $a (@$args) {
my $n = _to_number($a);
return $n if $n != $n; # any NaN argument â NaN (JS / Go)
$m = $n if $n > $m;
}
return $m;
}
if ($name eq 'Math.min') {
my $m = 9**9**9; # JS Math.min() with no args is +Infinity
for my $a (@$args) {
my $n = _to_number($a);
return $n if $n != $n; # any NaN argument â NaN (JS / Go)
$m = $n if $n < $m;
}
return $m;
}
return abs(_to_number($args->[0])) if $name eq 'Math.abs';
return POSIX::floor(_to_number($args->[0])) if $name eq 'Math.floor';
return POSIX::ceil(_to_number($args->[0])) if $name eq 'Math.ceil';
return _math_round(_to_number($args->[0])) if $name eq 'Math.round';
return _to_string($args->[0]) if $name eq 'String';
return _to_number($args->[0]) if $name eq 'Number';
lib/BarefootJS/Evaluator.pm view on Meta::CPAN
my %env = $base_env ? %$base_env : ();
# Decorate each element with its original index and tie-break on it when
# the comparator returns 0, so stability is explicit and independent of
# the `sort` pragma / build (portable to the declared minimum Perl, and
# matching Go's sort.SliceStable).
my @decorated = map { [ $_, $items->[$_] ] } 0 .. $#$items;
my @sorted = sort {
$env{$param_a} = $a->[1];
$env{$param_b} = $b->[1];
my $c = _to_number(evaluate($cmp, \%env));
# Explicit sign test rather than `<=> 0`: a NaN comparator result
# warns / is undefined under `<=>`, whereas `< 0` / `> 0` are both
# false for NaN, yielding 0 (no reordering) â matching JS (NaN
# comparator â keep order) and the Go SortEval sign test. The
# original-index tie-break then preserves input order for equal keys.
($c < 0 ? -1 : $c > 0 ? 1 : 0) || ($a->[0] <=> $b->[0])
} @decorated;
return [ map { $_->[1] } @sorted ];
}
# JSON entry points for the adapters: decode the callback body once, then fold /
# sort. Mirror the Go `bf_reduce_eval` / `bf_sort_eval` template funcs, which the
# adapters emit with a serialized-ParsedExpr body argument.
t/helper_vectors.t view on Meta::CPAN
# _match: boolean form of the spec's value-compat comparison against a
# JSON-decoded expect â sentinel hashes, booleans by truthiness,
# numbers numerically, arrays/hashes recursively.
sub _match {
my ($got, $expect) = @_;
return !defined $got if !defined $expect;
if (ref $expect eq 'HASH' && exists $expect->{'$num'}) {
my $kind = $expect->{'$num'};
return 0 unless defined $got && looks_like_number($got);
return $got != $got ? 1 : 0 if $kind eq 'NaN';
my $inf = 9**9**9;
return $got == ($kind eq 'Infinity' ? $inf : -$inf) ? 1 : 0;
}
if (JSON::PP::is_bool($expect)) {
return (!!$got eq !!$expect) ? 1 : 0;
}
if (ref $expect eq 'ARRAY') {
return 0 unless ref $got eq 'ARRAY' && @$got == @$expect;
_match($got->[$_], $expect->[$_]) or return 0 for 0 .. $#$expect;
return 1;
t/template_primitives.t view on Meta::CPAN
is $bf->json(undef), 'null', 'undef â "null" (matches JS null; diverges from JS undefined)';
};
subtest 'string â JS String(v) mirror' => sub {
is $bf->string(42), '42', 'int';
is $bf->string('hi'), 'hi', 'string passthrough';
# Documented divergence from JS String(null) === "null".
is $bf->string(undef), '', 'undef â "" (intentional divergence)';
};
# Real numeric NaN is the only float for which `$x != $x` holds.
# Tests check for it directly rather than string-comparing against
# "NaN", which stringifies platform-dependently.
sub is_nan { my $n = shift; return $n != $n }
subtest 'number â JS Number(v) mirror; NaN on parse failure' => sub {
is $bf->number('3.14'), 3.14, 'numeric string';
is $bf->number(42), 42, 'integer passthrough';
ok is_nan($bf->number('not a num')), 'non-numeric â NaN';
ok is_nan($bf->number(undef)), 'undef â NaN';
};
subtest 'floor / ceil / round â Math.* mirrors; propagate NaN' => sub {
is $bf->floor(3.7), 3, '3.7 â 3';
is $bf->floor(-3.2), -4, '-3.2 â -4';
ok is_nan($bf->floor('not')), 'floor: NaN propagates';
is $bf->ceil(3.1), 4, '3.1 â 4';
is $bf->ceil(-3.7), -3, '-3.7 â -3';
ok is_nan($bf->ceil('not')), 'ceil: NaN propagates';
is $bf->round(3.5), 4, '3.5 â 4';
is $bf->round(3.4), 3, '3.4 â 3';
# JS `Math.round` ties go toward +Infinity, NOT away from zero â
# so -1.5 rounds to -1 (not -2). Pin both halves of the negative
# tie-break so a future POSIX::floor swap doesn't silently
# regress the JS-compat contract.
is $bf->round(-1.5), -1, '-1.5 â -1 (JS half-toward-+Inf, not half-away-from-zero)';
is $bf->round(-1.6), -2, '-1.6 â -2';
ok is_nan($bf->round('not')), 'round: NaN propagates';
};
# `Array.prototype.includes(x)` + `String.prototype.includes(sub)` lower
# to the same `$bf->includes($recv, $elem)` shape â see #1448 Tier A.
# The Perl helper dispatches on `ref()`: ARRAY ref scans elements with
# `BarefootJS::Evaluator::_same_value_zero` (SameValueZero â no cross-type
# coercion, NaN matches NaN), matching the evaluator's serialized-callback
# `.includes` path so both positions agree; scalar falls back to
# `index(..., ...) != -1`. Anything else (HASH ref, code ref) returns
# false to match the JS semantic that `.includes` is only defined on
# Array / TypedArray / String.
subtest 'includes â array + string + non-array/string dispatch' => sub {
# Array receiver: SameValueZero element search (handles defined/undef
# parity, and no numeric/string coercion â see the cross-type cases
# below).
ok $bf->includes(['a', 'b', 'c'], 'b'), 'array contains element â 1';
ok !$bf->includes(['a', 'b', 'c'], 'z'), 'array does not contain â 0';