~singpolyma/dhall-ruby

17029e2f1eaf17b3fb3612dd96fa98a2ccd5cd6b — Stephen Paul Weber 2 years ago 9b70037
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.
3 files changed, 132 insertions(+), 44 deletions(-)

M lib/dhall/as_dhall.rb
M lib/dhall/ast.rb
M test/test_as_dhall.rb
M lib/dhall/as_dhall.rb => lib/dhall/as_dhall.rb +77 -21
@@ 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

M lib/dhall/ast.rb => lib/dhall/ast.rb +5 -5
@@ 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)

M test/test_as_dhall.rb => test/test_as_dhall.rb +50 -18
@@ 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(