Class: Fusion::Interpreter
- Inherits:
-
Object
- Object
- Fusion::Interpreter
- Defined in:
- lib/fusion.rb
Overview
EVALUATOR
Instance Attribute Summary collapse
-
#root_env ⇒ Object
readonly
Returns the value of attribute root_env.
Instance Method Summary collapse
-
#apply(f, v) ⇒ Object
—- Application & matching ——————————————.
- #deep_equal?(a, b) ⇒ Boolean
-
#error?(v) ⇒ Boolean
—- Equality & helpers ———————————————-.
-
#eval_array(node, env) ⇒ Object
Array/object literals propagate any error encountered during construction.
-
#eval_expr(node, env) ⇒ Object
—- Expression evaluation ——————————————-.
- #eval_index(node, env) ⇒ Object
- #eval_member(node, env) ⇒ Object
- #eval_object(node, env) ⇒ Object
- #eval_pipe(node, env) ⇒ Object
- #evaluate_file(abspath) ⇒ Object
-
#initialize(stdlib_dir: nil, env_vars: nil) ⇒ Interpreter
constructor
A new instance of Interpreter.
-
#load_file(abspath) ⇒ Object
—- File loading —————————————————–.
-
#match(pattern, value, bindings, env) ⇒ Object
Returns true (match), false (no match), or an ErrorVal (predicate errored).
- #match_array(pattern, value, bindings, env) ⇒ Object
- #match_object(pattern, value, bindings, env) ⇒ Object
-
#resolve_name(name, dir) ⇒ Object
Resolve a bare “@name”: sibling file > builtin (incl. load, ENV) > stdlib > !.
-
#resolve_path(relpath, dir) ⇒ Object
Resolve a pure path “@dir/a” or “@../a”: file only, never builtin/stdlib.
Constructor Details
#initialize(stdlib_dir: nil, env_vars: nil) ⇒ Interpreter
Returns a new instance of Interpreter.
614 615 616 617 618 619 620 621 622 |
# File 'lib/fusion.rb', line 614 def initialize(stdlib_dir: nil, env_vars: nil) @stdlib_dir = stdlib_dir @env_vars = env_vars || ENV.to_h @file_cache = {} # abspath -> FileThunk @ast_cache = {} # abspath -> AST @builtins = {} # name -> NativeFunc (consulted by @name, not via env) Builtins.install(@builtins, self) @root_env = Env.new # holds no builtins now; bare identifiers are holes only end |
Instance Attribute Details
#root_env ⇒ Object (readonly)
Returns the value of attribute root_env.
612 613 614 |
# File 'lib/fusion.rb', line 612 def root_env @root_env end |
Instance Method Details
#apply(f, v) ⇒ Object
—- Application & matching ——————————————
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 |
# File 'lib/fusion.rb', line 785 def apply(f, v) # An errored function value propagates as-is (more useful than a generic # "applied a non-function" wrapper). return f if Fusion.error?(f) if f.is_a?(NativeFunc) # Uniform propagation: built-ins never receive errors as inputs. return v if Fusion.error?(v) return f.fn.call(v) end unless f.is_a?(Func) return Fusion.mkerr({"kind" => "apply_non_function", "got" => f.class.name}) end f.clauses.each do |pattern, body| bindings = {} m = match(pattern, v, bindings, f.env) # A `?` predicate raised an error during matching: bubble it up as the # function's return value (no further clauses are tried). return m if Fusion.error?(m) if m return eval_expr(body, f.env.child(bindings)) end end # No clause matched. If the input was an error, it keeps propagating # (an unmatched error must never be silently swallowed). Otherwise the # lenient default is `null`. Fusion.error?(v) ? v : NULL end |
#deep_equal?(a, b) ⇒ Boolean
916 917 918 919 920 921 922 923 924 925 926 927 |
# File 'lib/fusion.rb', line 916 def deep_equal?(a, b) return true if a.equal?(b) return false if a.class != b.class case a when Array a.length == b.length && a.each_index.all? { |i| deep_equal?(a[i], b[i]) } when Hash a.length == b.length && a.all? { |k, v| b.key?(k) && deep_equal?(v, b[k]) } else a == b end end |
#error?(v) ⇒ Boolean
—- Equality & helpers ———————————————-
914 |
# File 'lib/fusion.rb', line 914 def error?(v) = Fusion.error?(v) |
#eval_array(node, env) ⇒ Object
Array/object literals propagate any error encountered during construction. Errors are not first-class: at any point during execution there is either a value or an error in motion, never both.
721 722 723 724 725 726 727 728 729 730 731 732 733 734 |
# File 'lib/fusion.rb', line 721 def eval_array(node, env) out = [] node.elems.each do |kind, expr| v = eval_expr(expr, env) return v if Fusion.error?(v) if kind == :spread return Fusion.mkerr({"kind" => "spread_non_array", "got" => v.class.name}) unless v.is_a?(Array) out.concat(v) else out << v end end out end |
#eval_expr(node, env) ⇒ Object
—- Expression evaluation ——————————————-
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 |
# File 'lib/fusion.rb', line 680 def eval_expr(node, env) case node when Lit then node.value when ErrLit # Bare `!` means `!null`; `!expr` wraps expr's value as an error. # If the payload expression itself errors, propagate THAT error rather # than wrapping it -- prevents accidental error-burying. if node.payload.nil? Fusion.mkerr(NULL) else p = eval_expr(node.payload, env) Fusion.error?(p) ? p : Fusion.mkerr(p) end when Ident v = env.lookup(node.name) v == :__unbound__ ? Fusion.mkerr({"kind" => "unbound", "name" => node.name}) : v when FileRef dir = env.lookup("__dir__") dir = Dir.pwd if dir == :__unbound__ case node.variety when :self f = env.lookup("__file__") f == :__unbound__ ? Fusion.mkerr({"kind" => "no_current_file"}) : load_file(f).force when :name resolve_name(node.path, dir) else # :path resolve_path(node.path, dir) end when ArrLit then eval_array(node, env) when ObjLit then eval_object(node, env) when FuncLit then Func.new(node.clauses, env) when Pipe then eval_pipe(node, env) when Member then eval_member(node, env) when Index then eval_index(node, env) else raise FusionError, "Cannot evaluate node #{node.class}" end end |
#eval_index(node, env) ⇒ Object
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 |
# File 'lib/fusion.rb', line 768 def eval_index(node, env) obj = eval_expr(node.obj, env) return obj if Fusion.error?(obj) idx = eval_expr(node.idx, env) return idx if Fusion.error?(idx) if obj.is_a?(Array) && idx.is_a?(Integer) i = idx >= 0 ? idx : obj.length + idx return obj[i] if i >= 0 && i < obj.length return Fusion.mkerr({"kind" => "index_out_of_range", "index" => idx, "length" => obj.length}) elsif obj.is_a?(Hash) && idx.is_a?(String) return obj[idx] if obj.key?(idx) return Fusion.mkerr({"kind" => "missing_key", "key" => idx}) end Fusion.mkerr({"kind" => "bad_index", "obj" => obj.class.name, "idx" => idx.class.name}) end |
#eval_member(node, env) ⇒ Object
760 761 762 763 764 765 766 |
# File 'lib/fusion.rb', line 760 def eval_member(node, env) obj = eval_expr(node.obj, env) return obj if Fusion.error?(obj) return Fusion.mkerr({"kind" => "member_on_non_object", "key" => node.key}) unless obj.is_a?(Hash) return Fusion.mkerr({"kind" => "missing_key", "key" => node.key}) unless obj.key?(node.key) obj[node.key] end |
#eval_object(node, env) ⇒ Object
736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 |
# File 'lib/fusion.rb', line 736 def eval_object(node, env) out = {} node.members.each do |m| if m[0] == :spread v = eval_expr(m[1], env) return v if Fusion.error?(v) return Fusion.mkerr({"kind" => "spread_non_object", "got" => v.class.name}) unless v.is_a?(Hash) out.merge!(v) else _, key, expr = m v = eval_expr(expr, env) return v if Fusion.error?(v) out[key] = v end end out end |
#eval_pipe(node, env) ⇒ Object
754 755 756 757 758 |
# File 'lib/fusion.rb', line 754 def eval_pipe(node, env) v = eval_expr(node.left, env) f = eval_expr(node.right, env) apply(f, v) end |
#evaluate_file(abspath) ⇒ Object
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 |
# File 'lib/fusion.rb', line 629 def evaluate_file(abspath) ast = (@ast_cache[abspath] ||= begin src = File.read(abspath) Parser.parse_file(src) end) # A file's value is evaluated in a fresh env whose parent is root (builtins), # plus knowledge of its own directory for resolving @refs. env = @root_env.child env.define("__dir__", File.dirname(abspath)) env.define("__file__", abspath) eval_expr(ast, env) rescue Errno::ENOENT warn "[fusion] file not found: #{abspath}" if ENV["FUSION_DEBUG"] Fusion.mkerr({"kind" => "file_not_found", "path" => abspath}) rescue ParseError => err warn "[fusion] parse error in #{abspath}: #{err.}" if ENV["FUSION_DEBUG"] Fusion.mkerr({"kind" => "parse_error", "path" => abspath, "message" => err.}) end |
#load_file(abspath) ⇒ Object
—- File loading —————————————————–
625 626 627 |
# File 'lib/fusion.rb', line 625 def load_file(abspath) @file_cache[abspath] ||= FileThunk.new(self, abspath) end |
#match(pattern, value, bindings, env) ⇒ Object
Returns true (match), false (no match), or an ErrorVal (predicate errored).
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 |
# File 'lib/fusion.rb', line 814 def match(pattern, value, bindings, env) case pattern when PLit deep_equal?(pattern.value, value) when PErr # If the value is an error, match the inner pattern against its # payload. The inner is always a non-`!` pattern (the parser ensures # that), so for a bare `!` we synthesized PWild as the inner. return false unless Fusion.error?(value) match(pattern.inner, value.payload, bindings, env) when PWild # `_` matches anything EXCEPT an error value. !Fusion.error?(value) when PBind return false if Fusion.error?(value) # binders never capture an error bindings[pattern.name] = value true when PArr match_array(pattern, value, bindings, env) when PObj match_object(pattern, value, bindings, env) when PGuard inner_res = match(pattern.inner, value, bindings, env) return inner_res if Fusion.error?(inner_res) return false unless inner_res pred = eval_expr(pattern.pred_expr, env) return pred if Fusion.error?(pred) # predicate expression itself errored # The predicate sees whatever value reached this PGuard, which is # already the right value because `!pat ? pred` parses as # PErr(PGuard(pat, pred)) — by the time PGuard runs, the value is # already the payload. r = apply(pred, value) return r if Fusion.error?(r) # predicate raised during application r == true else raise FusionError, "Unknown pattern #{pattern.class}" end end |
#match_array(pattern, value, bindings, env) ⇒ Object
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 |
# File 'lib/fusion.rb', line 853 def match_array(pattern, value, bindings, env) return false unless value.is_a?(Array) elems = pattern.elems rest_index = elems.index { |e| e[0] == :rest } if rest_index.nil? return false unless value.length == elems.length elems.each_with_index do |(_, p), i| r = match(p, value[i], bindings, env) return r if Fusion.error?(r) return false unless r end true else before = elems[0...rest_index] after = elems[(rest_index + 1)..] return false if value.length < before.length + after.length before.each_with_index do |(_, p), i| r = match(p, value[i], bindings, env) return r if Fusion.error?(r) return false unless r end after.each_with_index do |(_, p), k| vi = value.length - after.length + k r = match(p, value[vi], bindings, env) return r if Fusion.error?(r) return false unless r end rest_name = elems[rest_index][1] if rest_name mid = value[before.length...(value.length - after.length)] bindings[rest_name] = mid end true end end |
#match_object(pattern, value, bindings, env) ⇒ Object
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 |
# File 'lib/fusion.rb', line 890 def match_object(pattern, value, bindings, env) return false unless value.is_a?(Hash) matched_keys = [] rest_name = :__none__ pattern.members.each do |m| if m[0] == :rest rest_name = m[1] # may be nil (ignore) or a string else _, key, p = m return false unless value.key?(key) r = match(p, value[key], bindings, env) return r if Fusion.error?(r) return false unless r matched_keys << key end end if rest_name != :__none__ && rest_name remaining = value.reject { |k, _| matched_keys.include?(k) } bindings[rest_name] = remaining end true end |
#resolve_name(name, dir) ⇒ Object
Resolve a bare “@name”: sibling file > builtin (incl. load, ENV) > stdlib > !.
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 |
# File 'lib/fusion.rb', line 649 def resolve_name(name, dir) sib = File.(name + ".fsn", dir) return load_file(sib).force if File.exist?(sib) if name == "ENV" return @env_vars.dup end if name == "load" # @load is a builtin closure capturing the calling file's directory. It # loads a VERBATIM filename (no ".fsn" appended) so arbitrary names work. d = dir return NativeFunc.new("load", lambda do |v| next Fusion.mkerr({"kind" => "load_bad_arg", "got" => v.class.name}) unless v.is_a?(String) target = File.(v, d) next Fusion.mkerr({"kind" => "file_not_found", "path" => target}) unless File.exist?(target) load_file(target).force end) end return @builtins[name] if @builtins.key?(name) if @stdlib_dir std = File.join(@stdlib_dir, name + ".fsn") return load_file(std).force if File.exist?(std) end Fusion.mkerr({"kind" => "unresolved_ref", "name" => name}) end |
#resolve_path(relpath, dir) ⇒ Object
Resolve a pure path “@dir/a” or “@../a”: file only, never builtin/stdlib.
675 676 677 |
# File 'lib/fusion.rb', line 675 def resolve_path(relpath, dir) load_file(File.(relpath + ".fsn", dir)).force end |