Module: BarefootJS::Evaluator
- Defined in:
- lib/barefoot_js/evaluator.rb
Overview
Lightweight evaluator for the pure ParsedExpr subset, scoped to
higher-order callback bodies (reduce / sort / map / filter / find
(...) => expr) -- issue #2018. Templates cannot carry a lambda in
expression position, which is why the adapters historically special-cased
these callbacks into fixed shapes (bf.sort's comparator catalogue,
bf.reduce's +/* fold). Instead, the callback BODY rides as a pure
ParsedExpr subtree (the structured IR the compiler already produces) and
is evaluated here against an environment ({acc, item, ...captured free vars}).
Ruby port of BarefootJS::Evaluator (Perl), sharing the same contract as the Go evaluator (bf.go). The accepted subset and its semantics are documented in spec/compiler.md ("ParsedExpr Evaluator Semantics") and pinned isomorphically by the cross-language golden vectors (packages/adapter-tests/vectors/eval-vectors.json). The literal JS reference implementation is eval-reference.ts -- this port follows it node-for-node, including its refusal behaviour (EvalUnsupported), which is a closer contract match than the Perl port's silent-nil shortcuts (Perl blurs strings/numbers and can't cheaply enforce every refusal; Ruby's real type distinctions make strict refusal free).
Value domain: JSON-shaped Ruby data with SYMBOL hash keys throughout
(object literals, environments, member/index results). AST nodes
(ParsedExpr, decoded from JSON) also use symbol keys -- node[:kind],
node[:left], etc. String KEYS from the AST that name environment
bindings or object fields (identifier names, member.property,
object-literal property keys) are plain Ruby Strings coming out of the
parser; they are converted to Symbols at the point they touch a
SYMBOL-keyed Hash (env or object value).
Defined Under Namespace
Classes: EvalUnsupported
Constant Summary collapse
- HEX_STRING_RE =
Number <-> String helpers
/\A0[xX][0-9a-fA-F]+\z/- NUMERIC_STRING_RE =
/\A[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?\z/
Class Method Summary collapse
-
.eval_json(json, env) ⇒ Object
eval_json(json, env): decode a ParsedExpr JSON string and evaluate it.
-
.evaluate(node, env) ⇒ Object
evaluate(node, env) -> a Ruby value (Integer/Float, String, true/false, nil, Array, Hash-with-symbol-keys) per the ParsedExpr AST node kind.
- .every(items, pred, param, base_env = nil) ⇒ Object
- .every_json(items, pred_json, param, base_env = nil) ⇒ Object
-
.filter(items, pred, param, base_env = nil) ⇒ Object
--------------------------------------------------------------------- Higher-order predicates -- the generalization of filter / find / find_index / every / some onto the evaluator.
- .filter_json(items, pred_json, param, base_env = nil) ⇒ Object
-
.find(items, pred, param, forward = true, base_env = nil) ⇒ Object
find -- first matching element, or nil.
-
.find_index(items, pred, param, forward = true, base_env = nil) ⇒ Object
find_index -- index of the first matching element, or -1.
- .find_index_json(items, pred_json, param, forward = true, base_env = nil) ⇒ Object
- .find_json(items, pred_json, param, forward = true, base_env = nil) ⇒ Object
-
.flat_map(items, proj, param, base_env = nil) ⇒ Object
flat_map -- project each element through
projand flatten one level. - .flat_map_json(items, proj_json, param, base_env = nil) ⇒ Object
-
.fold(items, body, acc_name, item_name, init, direction = 'left', base_env = nil) ⇒ Object
fold(items, body, acc_name, item_name, init, direction, base_env).
- .fold_json(items, body_json, acc_name, item_name, init, direction = 'left', base_env = nil) ⇒ Object
-
.includes_value(obj, needle) ⇒ Object
includes_value(obj, needle): the receiver-dispatch behind the
array-methodincludesnode above, factored out soBarefootJS::Context#includes(the runtime helper compiled templates call directly, outside any evaluator subtree) can share it too -- mirrorsBarefootJS.pm::includesdelegating toBarefootJS::Evaluator::_same_value_zero. -
.map_items(items, proj, param, base_env = nil) ⇒ Object
map_items -- project each element through
proj, keeping each result as one element (no flatten): value-producing.map(cb). - .map_json(items, proj_json, param, base_env = nil) ⇒ Object
-
.number_to_string(n) ⇒ Object
JS Number#toString.
-
.same_value_zero?(l, r) ⇒ Boolean
same_value_zero?(l, r):
Array.prototype.includesmembership test --===exceptNaNequals itself (and +0/-0 are not distinguished, which the JSON-decoded values here can't represent anyway). - .some(items, pred, param, base_env = nil) ⇒ Object
- .some_json(items, pred_json, param, base_env = nil) ⇒ Object
-
.sort_by(items, cmp, param_a, param_b, base_env = nil) ⇒ Object
sort_by(items, cmp, param_a, param_b, base_env).
- .sort_by_json(items, cmp_json, param_a, param_b, base_env = nil) ⇒ Object
-
.to_number(v) ⇒ Object
--------------------------------------------------------------------- JS coercion primitives (ToNumber / ToString / ToBoolean).
- .to_string(v) ⇒ Object
- .truthy?(v) ⇒ Boolean
Class Method Details
.eval_json(json, env) ⇒ Object
eval_json(json, env): decode a ParsedExpr JSON string and evaluate it.
env is a plain Ruby Hash with symbol keys (caller's responsibility,
matching the SYMBOL-keys-throughout value convention).
158 159 160 |
# File 'lib/barefoot_js/evaluator.rb', line 158 def eval_json(json, env) evaluate(JSON.parse(json, symbolize_names: true), env) end |
.evaluate(node, env) ⇒ Object
evaluate(node, env) -> a Ruby value (Integer/Float, String, true/false, nil, Array, Hash-with-symbol-keys) per the ParsedExpr AST node kind.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/barefoot_js/evaluator.rb', line 43 def evaluate(node, env) return nil unless node.is_a?(Hash) kind = node[:kind] case kind when 'literal' node[:value] when 'identifier' name = node[:name] key = name.to_sym raise EvalUnsupported, "unbound identifier '#{name}'" unless env.key?(key) env[key] when 'binary' binary(node[:op], evaluate(node[:left], env), evaluate(node[:right], env)) when 'unary' unary(node[:op], evaluate(node[:argument], env)) when 'logical' op = node[:op] left = evaluate(node[:left], env) case op when '&&' then truthy?(left) ? evaluate(node[:right], env) : left when '||' then truthy?(left) ? left : evaluate(node[:right], env) else left.nil? ? evaluate(node[:right], env) : left # '??' end when 'conditional' truthy?(evaluate(node[:test], env)) ? evaluate(node[:consequent], env) : evaluate(node[:alternate], env) when 'member' read_property(evaluate(node[:object], env), node[:property]) when 'index-access' read_index(evaluate(node[:object], env), evaluate(node[:index], env)) when 'call' # A nested `.map(cb)` / `.filter(cb)` callback call (#2094): # syntactically a `call` whose callee is `<recv>.map`/`<recv>.filter` # and whose first argument is an `arrow` node -- the same shape # `asCallbackMethodCall` recognizes at compile time, and the shape # the `eval-vectors.json` golden corpus itself carries. Checked # BEFORE the builtin-name check below, since `<recv>.map` would # otherwise resolve to a non-builtin member callee and raise. method, object_node, arrow_node = array_callback_call(node) if method array_callback(method, object_node, arrow_node, env) else name = builtin_name(node[:callee]) raise EvalUnsupported, 'only built-in calls (Math.*, String/Number/Boolean) are in the subset' if name.nil? args = (node[:args] || []).map { |a| evaluate(a, env) } call_builtin(name, args) end when 'template-literal' out = +'' (node[:parts] || []).each do |p| out << if p[:type] == 'string' (p[:value] || '') else to_string(evaluate(p[:expr], env)) end end out when 'array-literal' (node[:elements] || []).map { |e| evaluate(e, env) } when 'object-literal' # Each entry carries its own `kind` ('prop' | 'spread', #2696 Step # 2) -- the same encoding the Go/Perl/Python evaluators and the raw # ParsedExpr shape the eval-vectors.json golden corpus carry. A # 'spread' evaluates its `expr` and shallow-merges the result's own # keys (a non-Hash result -- including a null/undefined JS spread # source -- is a no-op). Every hash this evaluator builds uses # SYMBOL keys (`read_property` below always looks up `key.to_sym`), # so a spread's keys are symbolized on merge for the same reason a # `prop` key already is, regardless of whether the spread source's # own keys started out as symbols or strings. Later entries win on # a shared key, in source order, matching JS object-spread exactly. out = {} (node[:properties] || []).each do |prop| if (prop[:kind] || '') == 'spread' spread = evaluate(prop[:expr], env) spread.each { |k, v| out[k.to_sym] = v } if spread.is_a?(Hash) next end out[prop[:key].to_sym] = evaluate(prop[:value], env) end out when 'array-method' args = node[:args] || [] if node[:method] == 'includes' && args.length == 1 # `.includes(x)` (#2075) -- the one `array-method` in the # evaluator subset, shared between `Array.prototype.includes` # (SameValueZero membership) and `String.prototype.includes` # (substring search), matching the receiver-type dispatch the SSR # template lowering does at runtime (`bf.includes`). Mirrors the # JS reference's `includes()` (eval-reference.ts). includes_value(evaluate(node[:object], env), evaluate(args[0], env)) elsif node[:method] == 'join' && args.length <= 1 # `.join(sep?)` (#2094) -- a nested `.flatMap(p => p.tags.map(...)) # .join(...)` projection composes a `.join` on top of a nested # `.map`, so it must be executable in the same evaluator subset as # `.includes`. Default separator "," (JS); a `nil` element joins # as "" (not the string "null"). Mirrors Go's `evalJoin`. sep = args.empty? ? ',' : to_string(evaluate(args[0], env)) array_join(evaluate(node[:object], env), sep) else # Every other array/string method (`slice`, `flat`, ...) is # outside the subset; a callback body containing one is refused # upstream (BF101) and should never reach here, but the evaluator # refuses explicitly rather than falling through silently, # matching the JS reference. raise EvalUnsupported, "array-method '#{node[:method]}' is not in the evaluator subset" end else raise EvalUnsupported, "node kind '#{kind}' is not in the evaluator subset" end end |
.every(items, pred, param, base_env = nil) ⇒ Object
639 640 641 642 643 644 645 646 647 |
# File 'lib/barefoot_js/evaluator.rb', line 639 def every(items, pred, param, base_env = nil) arr = items.is_a?(Array) ? items : [] env = base_env ? base_env.dup : {} key = param.to_sym arr.all? do |item| env[key] = item truthy?(evaluate(pred, env)) end end |
.every_json(items, pred_json, param, base_env = nil) ⇒ Object
721 722 723 |
# File 'lib/barefoot_js/evaluator.rb', line 721 def every_json(items, pred_json, param, base_env = nil) every(items, JSON.parse(pred_json, symbolize_names: true), param, base_env) end |
.filter(items, pred, param, base_env = nil) ⇒ Object
Higher-order predicates -- the generalization of filter / find /
find_index / every / some onto the evaluator. pred is a pure
ParsedExpr evaluated against {param => item} plus base_env.
628 629 630 631 632 633 634 635 636 637 |
# File 'lib/barefoot_js/evaluator.rb', line 628 def filter(items, pred, param, base_env = nil) return [] unless items.is_a?(Array) env = base_env ? base_env.dup : {} key = param.to_sym items.select do |item| env[key] = item truthy?(evaluate(pred, env)) end end |
.filter_json(items, pred_json, param, base_env = nil) ⇒ Object
717 718 719 |
# File 'lib/barefoot_js/evaluator.rb', line 717 def filter_json(items, pred_json, param, base_env = nil) filter(items, JSON.parse(pred_json, symbolize_names: true), param, base_env) end |
.find(items, pred, param, forward = true, base_env = nil) ⇒ Object
find -- first matching element, or nil. forward false searches from
the end (findLast).
661 662 663 664 665 666 667 668 669 670 671 |
# File 'lib/barefoot_js/evaluator.rb', line 661 def find(items, pred, param, forward = true, base_env = nil) arr = items.is_a?(Array) ? items : [] arr = arr.reverse unless forward env = base_env ? base_env.dup : {} key = param.to_sym arr.each do |item| env[key] = item return item if truthy?(evaluate(pred, env)) end nil end |
.find_index(items, pred, param, forward = true, base_env = nil) ⇒ Object
find_index -- index of the first matching element, or -1. forward
false -> findLastIndex (the index is into the original array either
way).
676 677 678 679 680 681 682 683 684 685 686 |
# File 'lib/barefoot_js/evaluator.rb', line 676 def find_index(items, pred, param, forward = true, base_env = nil) arr = items.is_a?(Array) ? items : [] env = base_env ? base_env.dup : {} key = param.to_sym idxs = forward ? (0...arr.length) : (0...arr.length).to_a.reverse idxs.each do |i| env[key] = arr[i] return i if truthy?(evaluate(pred, env)) end -1 end |
.find_index_json(items, pred_json, param, forward = true, base_env = nil) ⇒ Object
733 734 735 |
# File 'lib/barefoot_js/evaluator.rb', line 733 def find_index_json(items, pred_json, param, forward = true, base_env = nil) find_index(items, JSON.parse(pred_json, symbolize_names: true), param, forward, base_env) end |
.find_json(items, pred_json, param, forward = true, base_env = nil) ⇒ Object
729 730 731 |
# File 'lib/barefoot_js/evaluator.rb', line 729 def find_json(items, pred_json, param, forward = true, base_env = nil) find(items, JSON.parse(pred_json, symbolize_names: true), param, forward, base_env) end |
.flat_map(items, proj, param, base_env = nil) ⇒ Object
flat_map -- project each element through proj and flatten one level.
A projection yielding an array contributes its elements; any other
value contributes itself.
691 692 693 694 695 696 697 698 699 700 701 702 |
# File 'lib/barefoot_js/evaluator.rb', line 691 def flat_map(items, proj, param, base_env = nil) arr = items.is_a?(Array) ? items : [] env = base_env ? base_env.dup : {} key = param.to_sym out = [] arr.each do |item| env[key] = item v = evaluate(proj, env) v.is_a?(Array) ? out.concat(v) : out.push(v) end out end |
.flat_map_json(items, proj_json, param, base_env = nil) ⇒ Object
737 738 739 |
# File 'lib/barefoot_js/evaluator.rb', line 737 def flat_map_json(items, proj_json, param, base_env = nil) flat_map(items, JSON.parse(proj_json, symbolize_names: true), param, base_env) end |
.fold(items, body, acc_name, item_name, init, direction = 'left', base_env = nil) ⇒ Object
fold(items, body, acc_name, item_name, init, direction, base_env)
Fold an array into a value via the evaluator. body is a pure
ParsedExpr node evaluated against {acc_name => acc, item_name => item} plus the captured free vars in base_env, per element.
direction is "left" (reduce) or "right" (reduceRight).
577 578 579 580 581 582 583 584 585 586 587 588 589 590 |
# File 'lib/barefoot_js/evaluator.rb', line 577 def fold(items, body, acc_name, item_name, init, direction = 'left', base_env = nil) arr = items.is_a?(Array) ? items : [] arr = arr.reverse if direction == 'right' env = base_env ? base_env.dup : {} acc = init acc_key = acc_name.to_sym item_key = item_name.to_sym arr.each do |item| env[acc_key] = acc env[item_key] = item acc = evaluate(body, env) end acc end |
.fold_json(items, body_json, acc_name, item_name, init, direction = 'left', base_env = nil) ⇒ Object
592 593 594 |
# File 'lib/barefoot_js/evaluator.rb', line 592 def fold_json(items, body_json, acc_name, item_name, init, direction = 'left', base_env = nil) fold(items, JSON.parse(body_json, symbolize_names: true), acc_name, item_name, init, direction, base_env) end |
.includes_value(obj, needle) ⇒ Object
includes_value(obj, needle): the receiver-dispatch behind the
array-method includes node above, factored out so
BarefootJS::Context#includes (the runtime helper compiled templates
call directly, outside any evaluator subtree) can share it too --
mirrors BarefootJS.pm::includes delegating to
BarefootJS::Evaluator::_same_value_zero.
364 365 366 367 368 369 370 371 |
# File 'lib/barefoot_js/evaluator.rb', line 364 def includes_value(obj, needle) return obj.any? { |el| same_value_zero?(el, needle) } if obj.is_a?(Array) return obj.include?(to_string(needle)) if obj.is_a?(String) # Any other receiver is not a JS `.includes` target -- degrade to # false rather than raising, mirroring the reference. false end |
.map_items(items, proj, param, base_env = nil) ⇒ Object
map_items -- project each element through proj, keeping each result
as one element (no flatten): value-producing .map(cb). Named
map_items (not map) to stay clear of Ruby's own Enumerable#map.
707 708 709 710 711 712 713 714 715 |
# File 'lib/barefoot_js/evaluator.rb', line 707 def map_items(items, proj, param, base_env = nil) arr = items.is_a?(Array) ? items : [] env = base_env ? base_env.dup : {} key = param.to_sym arr.map do |item| env[key] = item evaluate(proj, env) end end |
.map_json(items, proj_json, param, base_env = nil) ⇒ Object
741 742 743 |
# File 'lib/barefoot_js/evaluator.rb', line 741 def map_json(items, proj_json, param, base_env = nil) map_items(items, JSON.parse(proj_json, symbolize_names: true), param, base_env) end |
.number_to_string(n) ⇒ Object
JS Number#toString. Integral finite values (however they arrived -- Integer or an integral Float) render without a decimal point ("1.0" -> "1"); non-finite values use the JS spellings ("NaN" / "Infinity" / "-Infinity"), which Ruby's own Float#to_s does not use. Non-integral floats fall back to Ruby's shortest-round-trip Float#to_s (the same class of algorithm V8 uses), reformatted to JS's exponent style. This is not the full ECMA-262 Number::toString grammar (no attempt to match JS's exact exponential-notation thresholds), but it is exact for every value the golden vectors exercise.
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/barefoot_js/evaluator.rb', line 237 def number_to_string(n) f = n.to_f return 'NaN' if f.nan? return f.negative? ? '-Infinity' : 'Infinity' if f.infinite? return '0' if f.zero? return n.to_i.to_s if f == f.to_i && f.abs < 1e21 s = f.to_s if s.include?('e') mantissa, exp = s.split('e') mantissa = mantissa.sub(/\.0\z/, '') sign = exp.start_with?('-') ? '-' : '+' digits = exp.sub(/\A[+-]/, '').sub(/\A0+(?=\d)/, '') "#{mantissa}e#{sign}#{digits}" else s end end |
.same_value_zero?(l, r) ⇒ Boolean
same_value_zero?(l, r): 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 for the primitive cases and only
special-cases the two-NaN case that strict_eq (deliberately, for
===) reports as unequal. Unlike strict_eq, never raises for a
non-primitive operand -- the JS reference's sameValueZero uses
native === directly (reference equality for objects/arrays, never a
throw), not the subset's throwing strictEquals; two freshly
JSON-decoded structures are never the same object, so this degrades to
false rather than raising. Public (unlike strict_eq) because
BarefootJS::Context#includes (barefoot_js.rb) calls it directly,
matching the Perl port's cross-module _same_value_zero use.
350 351 352 353 354 355 356 |
# File 'lib/barefoot_js/evaluator.rb', line 350 def same_value_zero?(l, r) return true if l.is_a?(Numeric) && r.is_a?(Numeric) && l.to_f.nan? && r.to_f.nan? strict_eq(l, r) rescue EvalUnsupported false end |
.some(items, pred, param, base_env = nil) ⇒ Object
649 650 651 652 653 654 655 656 657 |
# File 'lib/barefoot_js/evaluator.rb', line 649 def some(items, pred, param, base_env = nil) arr = items.is_a?(Array) ? items : [] env = base_env ? base_env.dup : {} key = param.to_sym arr.any? do |item| env[key] = item truthy?(evaluate(pred, env)) end end |
.some_json(items, pred_json, param, base_env = nil) ⇒ Object
725 726 727 |
# File 'lib/barefoot_js/evaluator.rb', line 725 def some_json(items, pred_json, param, base_env = nil) some(items, JSON.parse(pred_json, symbolize_names: true), param, base_env) end |
.sort_by(items, cmp, param_a, param_b, base_env = nil) ⇒ Object
sort_by(items, cmp, param_a, param_b, base_env)
Return a new array ordered by a ParsedExpr comparator cmp evaluated
against {param_a => a, param_b => b} plus base_env. Stable
(ties break on original index) and non-mutating.
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 |
# File 'lib/barefoot_js/evaluator.rb', line 601 def sort_by(items, cmp, param_a, param_b, base_env = nil) return [] unless items.is_a?(Array) env = base_env ? base_env.dup : {} a_key = param_a.to_sym b_key = param_b.to_sym decorated = items.each_with_index.map { |item, i| [i, item] } sorted = decorated.sort do |x, y| env[a_key] = x[1] env[b_key] = y[1] c = to_number(evaluate(cmp, env)).to_f sign = c.nan? ? 0 : (c <=> 0) sign.zero? ? (x[0] <=> y[0]) : sign end sorted.map { |pair| pair[1] } end |
.sort_by_json(items, cmp_json, param_a, param_b, base_env = nil) ⇒ Object
618 619 620 |
# File 'lib/barefoot_js/evaluator.rb', line 618 def sort_by_json(items, cmp_json, param_a, param_b, base_env = nil) sort_by(items, JSON.parse(cmp_json, symbolize_names: true), param_a, param_b, base_env) end |
.to_number(v) ⇒ Object
JS coercion primitives (ToNumber / ToString / ToBoolean).
166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/barefoot_js/evaluator.rb', line 166 def to_number(v) return 0 if v.nil? return v ? 1 : 0 if v.is_a?(TrueClass) || v.is_a?(FalseClass) return v if v.is_a?(Numeric) if v.is_a?(String) t = v.strip return 0 if t.empty? return parse_numeric_string(t) end raise EvalUnsupported, "cannot coerce #{v.class} to number" end |
.to_string(v) ⇒ Object
178 179 180 181 182 183 184 |
# File 'lib/barefoot_js/evaluator.rb', line 178 def to_string(v) return v if v.is_a?(String) return number_to_string(v) if v.is_a?(Numeric) return v ? 'true' : 'false' if v.is_a?(TrueClass) || v.is_a?(FalseClass) return 'null' if v.nil? raise EvalUnsupported, "cannot coerce #{v.class} to string" end |
.truthy?(v) ⇒ Boolean
186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/barefoot_js/evaluator.rb', line 186 def truthy?(v) return false if v.nil? || v.is_a?(FalseClass) return true if v.is_a?(TrueClass) if v.is_a?(Numeric) f = v.to_f return false if f.nan? || f.zero? return true end return v != '' if v.is_a?(String) true # arrays / objects are always truthy in JS end |