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).
140 141 142 |
# File 'lib/barefoot_js/evaluator.rb', line 140 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 |
# 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' out = {} (node[:properties] || []).each { |prop| out[prop[:key].to_sym] = evaluate(prop[:value], env) } 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
621 622 623 624 625 626 627 628 629 |
# File 'lib/barefoot_js/evaluator.rb', line 621 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
703 704 705 |
# File 'lib/barefoot_js/evaluator.rb', line 703 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.
610 611 612 613 614 615 616 617 618 619 |
# File 'lib/barefoot_js/evaluator.rb', line 610 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
699 700 701 |
# File 'lib/barefoot_js/evaluator.rb', line 699 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).
643 644 645 646 647 648 649 650 651 652 653 |
# File 'lib/barefoot_js/evaluator.rb', line 643 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).
658 659 660 661 662 663 664 665 666 667 668 |
# File 'lib/barefoot_js/evaluator.rb', line 658 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
715 716 717 |
# File 'lib/barefoot_js/evaluator.rb', line 715 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
711 712 713 |
# File 'lib/barefoot_js/evaluator.rb', line 711 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.
673 674 675 676 677 678 679 680 681 682 683 684 |
# File 'lib/barefoot_js/evaluator.rb', line 673 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
719 720 721 |
# File 'lib/barefoot_js/evaluator.rb', line 719 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).
559 560 561 562 563 564 565 566 567 568 569 570 571 572 |
# File 'lib/barefoot_js/evaluator.rb', line 559 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
574 575 576 |
# File 'lib/barefoot_js/evaluator.rb', line 574 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.
346 347 348 349 350 351 352 353 |
# File 'lib/barefoot_js/evaluator.rb', line 346 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.
689 690 691 692 693 694 695 696 697 |
# File 'lib/barefoot_js/evaluator.rb', line 689 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
723 724 725 |
# File 'lib/barefoot_js/evaluator.rb', line 723 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.
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/barefoot_js/evaluator.rb', line 219 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.
332 333 334 335 336 337 338 |
# File 'lib/barefoot_js/evaluator.rb', line 332 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
631 632 633 634 635 636 637 638 639 |
# File 'lib/barefoot_js/evaluator.rb', line 631 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
707 708 709 |
# File 'lib/barefoot_js/evaluator.rb', line 707 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.
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 |
# File 'lib/barefoot_js/evaluator.rb', line 583 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
600 601 602 |
# File 'lib/barefoot_js/evaluator.rb', line 600 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).
148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/barefoot_js/evaluator.rb', line 148 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
160 161 162 163 164 165 166 |
# File 'lib/barefoot_js/evaluator.rb', line 160 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
168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/barefoot_js/evaluator.rb', line 168 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 |