Class: Fusion::Interpreter

Inherits:
Object
  • Object
show all
Defined in:
lib/fusion.rb

Overview

EVALUATOR

Instance Attribute Summary collapse

Instance Method Summary collapse

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_envObject (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

Returns:

  • (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 ———————————————-

Returns:

  • (Boolean)


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.message}" if ENV["FUSION_DEBUG"]
  Fusion.mkerr({"kind" => "parse_error", "path" => abspath, "message" => err.message})
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.expand_path(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.expand_path(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.expand_path(relpath + ".fsn", dir)).force
end