From 17029e2f1eaf17b3fb3612dd96fa98a2ccd5cd6b Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 12 May 2019 15:23:15 -0500 Subject: [PATCH] Disambiguate hoisted union tags properly An array may contain two nomnatively-equivalent types who are structurally different -- so they need different (but related) union tags since they will not share a type. --- lib/dhall/as_dhall.rb | 98 +++++++++++++++++++++++++++++++++---------- lib/dhall/ast.rb | 10 ++--- test/test_as_dhall.rb | 68 ++++++++++++++++++++++-------- 3 files changed, 132 insertions(+), 44 deletions(-) diff --git a/lib/dhall/as_dhall.rb b/lib/dhall/as_dhall.rb index f1ff8f1..3ddb044 100644 --- a/lib/dhall/as_dhall.rb +++ b/lib/dhall/as_dhall.rb @@ -15,24 +15,73 @@ module Dhall ::TrueClass => "Bool" }.freeze - def self.tag_for(o, type) + def self.tag_for(o) return "Natural" if o.is_a?(::Integer) && !o.negative? TAGS.fetch(o.class) do - "#{o.class.name}_#{type.digest.hexdigest}" + o.class.name end end - def self.union_of(values_and_types) - values_and_types.reduce([UnionType.new, []]) do |(ut, tags), (v, t)| - tag = tag_for(v, t) - if t.is_a?(UnionType) && t.alternatives.length == 1 - tag, t = t.alternatives.to_a.first + class AnnotatedExpressionList + attr_reader :type + attr_reader :exprs + + def self.from(type_annotation) + if type_annotation.nil? + new(nil, [nil]) + else + new(type_annotation.type, [type_annotation.value]) end - [ - ut.merge(UnionType.new(alternatives: { tag => t })), - tags + [tag] - ] + end + + def initialize(type, exprs) + @type = type + @exprs = exprs + end + + def +(other) + raise "#{type} != #{other.type}" if type != other.type + self.class.new(type, exprs + other.exprs) + end + end + + class UnionInferer + def initialize(tagged={}) + @tagged = tagged + end + + def union_type + UnionType.new(alternatives: Hash[@tagged.map { |k, v| [k, v.type] }]) + end + + def union_for(expr) + if expr.is_a?(Enum) + tag = expr.tag + expr = nil + else + tag = @tagged.keys.find { |k| @tagged[k].exprs.include?(expr) } + end + expr = expr.extract if expr.is_a?(Union) + Union.from(union_type, tag, expr) + end + + def with(tag, type_annotation) + anno = AnnotatedExpressionList.from(type_annotation) + if @tagged.key?(tag) && @tagged[tag].type != anno.type + disambiguate_against(tag, anno) + else + self.class.new(@tagged.merge(tag => anno) { |_, x, y| x + y }) + end + end + + def disambiguate_against(tag, anno) + self.class.new( + @tagged.reject { |k, _| k == tag }.merge( + "#{tag}_#{@tagged[tag].type.digest.hexdigest}" => @tagged[tag], + "#{tag}_#{anno.type.digest.hexdigest}" => anno + ) + ) end end @@ -144,21 +193,28 @@ module Dhall class Union def initialize(values, exprs, types) - @values = values + @tags, @types = values.zip(types).map { |(value, type)| + if type.is_a?(UnionType) && type.alternatives.length == 1 + type.alternatives.to_a.first + else + [AsDhall.tag_for(value), type] + end + }.transpose @exprs = exprs - @types = types + @inferer = UnionInferer.new end def list - ut, tags = AsDhall.union_of(@values.zip(@types)) - - List.new(elements: @exprs.zip(tags).map do |(expr, tag)| - if expr.is_a?(Dhall::Union) && expr.alternatives.empty? - expr = expr.is_a?(Dhall::Enum) ? nil : expr.extract + final_inferer = + @tags + .zip(@exprs, @types) + .reduce(@inferer) do |inferer, (tag, expr, type)| + inferer.with( + tag, + type.nil? ? nil : TypeAnnotation.new(value: expr, type: type) + ) end - - Dhall::Union.from(ut, tag, expr) - end) + List.new(elements: @exprs.map(&final_inferer.method(:union_for))) end end end diff --git a/lib/dhall/ast.rb b/lib/dhall/ast.rb index 738abc2..7e6a4e6 100644 --- a/lib/dhall/ast.rb +++ b/lib/dhall/ast.rb @@ -825,9 +825,9 @@ module Dhall alternatives.fetch(k) end - def without(key) - key = key.to_s - with(alternatives: alternatives.reject { |k, _| k == key }) + def without(*keys) + keys.map!(&:to_s) + with(alternatives: alternatives.reject { |k, _| keys.include?(k) }) end def record @@ -842,8 +842,8 @@ module Dhall self == other end - def merge(other) - with(alternatives: alternatives.merge(other.alternatives)) + def merge(other, &block) + with(alternatives: alternatives.merge(other.alternatives, &block)) end def fetch(k, default=nil) diff --git a/test/test_as_dhall.rb b/test/test_as_dhall.rb index 810aabb..9e085f0 100644 --- a/test/test_as_dhall.rb +++ b/test/test_as_dhall.rb @@ -112,48 +112,87 @@ class TestAsDhall < Minitest::Test ) end + class SomeTestClass + def initialize(a: 1, b: "hai") + @a = a + @b = b + end + end + def test_array_mixed - array_key = "Array_f256441295d38d19e84f2de0596f5ae2377" \ - "c923c4162351d88f7648d741cdd0c" - hash_key = "Hash_76cf2d18fa656820d79d13cad11bf3e613fdb0" \ - "6ff80f968ba1755d27cdf5eab3" + cla_key_a = "TestAsDhall::SomeTestClass_5af4256bc953a" \ + "30de368b9707b0d79bb119acc3098ba9589f234125eed296a19" + cla_key_b = "TestAsDhall::SomeTestClass_eca4d00787f4b" \ + "c9f52ef84ce193c62e28a31a9de5704319d738b1b9ca4a6abd7" union_type = Dhall::UnionType.new( alternatives: { "Natural" => Dhall::Builtins[:Natural], "Text" => Dhall::Builtins[:Text], "None" => nil, "boop" => nil, - "Object" => Dhall::EmptyRecordType.new, "Bool" => Dhall::Builtins[:Bool], - hash_key => Dhall::RecordType.new( + "Hash" => Dhall::RecordType.new( record: { "a" => Dhall::Builtins[:Natural] } ), - array_key => Dhall::Application.new( + "Array" => Dhall::Application.new( function: Dhall::Builtins[:List], argument: Dhall::Builtins[:Natural] + ), + cla_key_a => Dhall::RecordType.new( + record: { + "a" => Dhall::Builtins[:Natural], + "b" => Dhall::Builtins[:Text] + } + ), + cla_key_b => Dhall::RecordType.new( + record: { + "a" => Dhall::Builtins[:Text], + "b" => Dhall::Builtins[:Natural] + } ) } ) + expr = [ + 1, "hai", nil, :boop, true, false, { a: 1 }, [1], + SomeTestClass.new, SomeTestClass.new(a: "hai", b: 1) + ].as_dhall + + assert_equal( + Dhall::Builtins[:List].call(union_type).normalize, + Dhall::TypeChecker.type_of(expr).normalize + ) + assert_equal( Dhall::List.new(elements: [ Dhall::Union.from(union_type, "Natural", Dhall::Natural.new(value: 1)), Dhall::Union.from(union_type, "Text", Dhall::Text.new(value: "hai")), Dhall::Union.from(union_type, "None", nil), Dhall::Union.from(union_type, "boop", nil), - Dhall::Union.from(union_type, "Object", Dhall::EmptyRecord.new), Dhall::Union.from(union_type, "Bool", Dhall::Bool.new(value: true)), Dhall::Union.from(union_type, "Bool", Dhall::Bool.new(value: false)), - Dhall::Union.from(union_type, hash_key, Dhall::Record.new( + Dhall::Union.from(union_type, "Hash", Dhall::Record.new( record: { "a" => Dhall::Natural.new(value: 1) } )), - Dhall::Union.from(union_type, array_key, Dhall::List.new( + Dhall::Union.from(union_type, "Array", Dhall::List.new( elements: [Dhall::Natural.new(value: 1)] + )), + Dhall::Union.from(union_type, cla_key_a, Dhall::Record.new( + record: { + "a" => Dhall::Natural.new(value: 1), + "b" => Dhall::Text.new(value: "hai") + } + )), + Dhall::Union.from(union_type, cla_key_b, Dhall::Record.new( + record: { + "a" => Dhall::Text.new(value: "hai"), + "b" => Dhall::Natural.new(value: 1) + } )) ]), - [1, "hai", nil, :boop, Object.new, true, false, { a: 1 }, [1]].as_dhall + expr ) end @@ -213,13 +252,6 @@ class TestAsDhall < Minitest::Test ) end - class SomeTestClass - def initialize - @a = 1 - @b = "hai" - end - end - def test_object assert_equal( Dhall::Union.new( -- 2.38.5