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
608 609 610 611 612 613 614 615 616 |
# File 'lib/barefoot_js/evaluator.rb', line 608 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
690 691 692 |
# File 'lib/barefoot_js/evaluator.rb', line 690 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.
597 598 599 600 601 602 603 604 605 606 |
# File 'lib/barefoot_js/evaluator.rb', line 597 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
686 687 688 |
# File 'lib/barefoot_js/evaluator.rb', line 686 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).
630 631 632 633 634 635 636 637 638 639 640 |
# File 'lib/barefoot_js/evaluator.rb', line 630 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).
645 646 647 648 649 650 651 652 653 654 655 |
# File 'lib/barefoot_js/evaluator.rb', line 645 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
702 703 704 |
# File 'lib/barefoot_js/evaluator.rb', line 702 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
698 699 700 |
# File 'lib/barefoot_js/evaluator.rb', line 698 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.
660 661 662 663 664 665 666 667 668 669 670 671 |
# File 'lib/barefoot_js/evaluator.rb', line 660 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
706 707 708 |
# File 'lib/barefoot_js/evaluator.rb', line 706 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).
546 547 548 549 550 551 552 553 554 555 556 557 558 559 |
# File 'lib/barefoot_js/evaluator.rb', line 546 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
561 562 563 |
# File 'lib/barefoot_js/evaluator.rb', line 561 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.
333 334 335 336 337 338 339 340 |
# File 'lib/barefoot_js/evaluator.rb', line 333 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.
676 677 678 679 680 681 682 683 684 |
# File 'lib/barefoot_js/evaluator.rb', line 676 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
710 711 712 |
# File 'lib/barefoot_js/evaluator.rb', line 710 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.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/barefoot_js/evaluator.rb', line 206 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.
319 320 321 322 323 324 325 |
# File 'lib/barefoot_js/evaluator.rb', line 319 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
618 619 620 621 622 623 624 625 626 |
# File 'lib/barefoot_js/evaluator.rb', line 618 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
694 695 696 |
# File 'lib/barefoot_js/evaluator.rb', line 694 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.
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 |
# File 'lib/barefoot_js/evaluator.rb', line 570 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
587 588 589 |
# File 'lib/barefoot_js/evaluator.rb', line 587 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 |