~singpolyma/dhall-ruby

d79e79e33b434fb9dc21bbffcdc55662e6aeec7b — Stephen Paul Weber 4 years ago 8946079
Add as_dhall refinement
6 files changed, 440 insertions(+), 0 deletions(-)

M .rubocop.yml
M lib/dhall.rb
A lib/dhall/as_dhall.rb
M lib/dhall/ast.rb
M lib/dhall/typecheck.rb
A test/test_as_dhall.rb
M .rubocop.yml => .rubocop.yml +4 -0
@@ 4,6 4,10 @@ AllCops:
Metrics/LineLength:
  Max: 80

Metrics/AbcSize:
  Exclude:
    - test/*

Metrics/MethodLength:
  Exclude:
    - test/*

M lib/dhall.rb => lib/dhall.rb +1 -0
@@ 13,6 13,7 @@ module Dhall
	end
end

require "dhall/as_dhall"
require "dhall/ast"
require "dhall/binary"
require "dhall/builtins"

A lib/dhall/as_dhall.rb => lib/dhall/as_dhall.rb +195 -0
@@ 0,0 1,195 @@
# frozen_string_literal: true

require "ostruct"

require "dhall/ast"
require "dhall/binary"
require "dhall/typecheck"

module Dhall
	module AsDhall
		TAGS = {
			::Integer    => "Integer",
			::FalseClass => "Bool",
			::Integer    => "Integer",
			::Float      => "Double",
			::NilClass   => "None",
			::String     => "Text",
			::TrueClass  => "Bool"
		}.freeze

		def self.tag_for(o, type)
			return "Natural" if o.is_a?(::Integer) && !o.negative?

			TAGS.fetch(o.class) do
				"#{o.class.name}_#{type.digest.hexdigest}"
			end
		end

		def self.union_of(values_and_types)
			z = [UnionType.new(alternatives: {}), []]
			values_and_types.reduce(z) do |(ut, tags), (v, t)|
				tag = tag_for(v, t)
				[
					ut.merge(UnionType.new(alternatives: { tag => t })),
					tags + [tag]
				]
			end
		end

		refine ::String do
			def as_dhall
				Text.new(value: self)
			end
		end

		refine ::Integer do
			def as_dhall
				if negative?
					Integer.new(value: self)
				else
					Natural.new(value: self)
				end
			end
		end

		refine ::Float do
			def as_dhall
				Double.new(value: self)
			end
		end

		refine ::TrueClass do
			def as_dhall
				Bool.new(value: true)
			end
		end

		refine ::FalseClass do
			def as_dhall
				Bool.new(value: false)
			end
		end

		refine ::NilClass do
			def as_dhall
				raise(
					"Cannot call NilClass#as_dhall directly, " \
					"you probably want to create a Dhall::OptionalNone yourself."
				)
			end
		end

		module ExpressionList
			def self.for(values, exprs)
				types = exprs.map(&TypeChecker.method(:type_of))

				if types.empty?
					Empty
				elsif types.include?(nil) && types.uniq.length <= 2
					Optional
				elsif types.uniq.length == 1
					Mono
				else
					Union
				end.new(values, exprs, types)
			end

			class Empty
				def initialize(*); end

				def list
					EmptyList.new(element_type: UnionType.new(alternatives: {}))
				end
			end

			class Optional
				def initialize(_, exprs, types)
					@type = types.compact.first
					@exprs = exprs
				end

				def list
					List.new(elements: @exprs.map do |x|
						if x.nil?
							Dhall::OptionalNone.new(value_type: @type)
						else
							Dhall::Optional.new(value: x)
						end
					end)
				end
			end

			class Mono
				def initialize(_, exprs, _)
					@exprs = exprs
				end

				def list
					List.new(elements: @exprs)
				end
			end

			class Union
				def initialize(values, exprs, types)
					@values = values
					@exprs = exprs
					@types = types
				end

				def list
					ut, tags = AsDhall.union_of(@values.zip(@types))

					List.new(elements: @exprs.zip(tags).map do |(expr, tag)|
						Dhall::Union.from(ut, tag, expr)
					end)
				end
			end
		end

		refine ::Array do
			def as_dhall
				ExpressionList.for(self, map { |x| x&.as_dhall }).list
			end
		end

		refine ::Hash do
			def as_dhall
				if empty?
					EmptyRecord.new
				else
					Record.new(record: Hash[map { |k, v| [k.to_s, v.as_dhall] }.sort])
				end
			end
		end

		refine ::OpenStruct do
			def as_dhall
				annotation = TypeChecker
					            .for(to_h.as_dhall)
					            .annotate(TypeChecker::Context.new)
				Union.new(
					tag:          "OpenStruct",
					value:        annotation,
					alternatives: UnionType.new(alternatives: {})
				)
			end
		end

		refine ::Object do
			def as_dhall
				ivars = instance_variables.each_with_object({}) { |ivar, h|
					h[ivar.to_s[1..-1]] = instance_variable_get(ivar)
				}.as_dhall

				type = TypeChecker.for(ivars).annotate(TypeChecker::Context.new).type
				tag = self.class.name
				Union.from(
					UnionType.new(alternatives: { tag => type }),
					tag,
					ivars
				)
			end
		end
	end
end

M lib/dhall/ast.rb => lib/dhall/ast.rb +4 -0
@@ 102,6 102,10 @@ module Dhall
				Operator::RecursiveRecordTypeMerge.new(lhs: self, rhs: other)
			end
		end

		def as_dhall
			self
		end
	end

	class Application < Expression

M lib/dhall/typecheck.rb => lib/dhall/typecheck.rb +5 -0
@@ 40,6 40,11 @@ module Dhall
			@typecheckers[node_type] ||= [typechecker, extras]
		end

		def self.type_of(expr)
			return if expr.nil?
			TypeChecker.for(expr).annotate(TypeChecker::Context.new).type
		end

		class Context
			def initialize(bindings=Hash.new([]))
				@bindings = bindings.freeze

A test/test_as_dhall.rb => test/test_as_dhall.rb +231 -0
@@ 0,0 1,231 @@
# frozen_string_literal: true

require "minitest/autorun"

require "dhall/as_dhall"

class TestAsDhall < Minitest::Test
	using Dhall::AsDhall

	def test_string
		assert_equal Dhall::Text.new(value: "hai"), "hai".as_dhall
	end

	def test_string_encoding
		assert_equal(
			Dhall::Text.new(value: "hai"),
			"hai".encode("UTF-16BE").as_dhall
		)
	end

	def test_string_failure
		assert_raises Encoding::UndefinedConversionError do
			"\xff".b.as_dhall
		end
	end

	def test_natural
		assert_equal Dhall::Natural.new(value: 1), 1.as_dhall
	end

	def test_big_natural
		assert_equal(
			Dhall::Natural.new(value: 10000000000000000000000000000000000),
			10000000000000000000000000000000000.as_dhall
		)
	end

	def test_negative_integer
		assert_equal Dhall::Integer.new(value: -1), -1.as_dhall
	end

	def test_double
		assert_equal Dhall::Double.new(value: 1.0), 1.0.as_dhall
	end

	def test_double_infinity
		assert_equal(
			Dhall::Double.new(value: Float::INFINITY),
			Float::INFINITY.as_dhall
		)
	end

	def test_true
		assert_equal Dhall::Bool.new(value: true), true.as_dhall
	end

	def test_false
		assert_equal Dhall::Bool.new(value: false), false.as_dhall
	end

	def test_nil
		assert_raises RuntimeError do
			nil.as_dhall
		end
	end

	def test_empty_array
		assert_equal(
			Dhall::EmptyList.new(element_type: Dhall::UnionType.new(alternatives: {})),
			[].as_dhall
		)
	end

	def test_array_one_natural
		assert_equal(
			Dhall::List.new(elements: [Dhall::Natural.new(value: 1)]),
			[1].as_dhall
		)
	end

	def test_array_natural_and_nil
		assert_equal(
			Dhall::List.new(elements: [
				Dhall::Optional.new(value: Dhall::Natural.new(value: 1)),
				Dhall::OptionalNone.new(value_type: Dhall::Variable["Natural"])
			]),
			[1, nil].as_dhall
		)
	end

	def test_array_natural_and_bignum_and_nil
		assert_equal(
			Dhall::List.new(elements: [
				Dhall::Optional.new(value: Dhall::Natural.new(value: 1)),
				Dhall::Optional.new(value: Dhall::Natural.new(
					value: 10000000000000000000000000000000000
				)),
				Dhall::OptionalNone.new(value_type: Dhall::Variable["Natural"])
			]),
			[1, 10000000000000000000000000000000000, nil].as_dhall
		)
	end

	def test_array_mixed
		array_key = "Array_f256441295d38d19e84f2de0596f5ae2377" \
		            "c923c4162351d88f7648d741cdd0c"
		hash_key = "Hash_76cf2d18fa656820d79d13cad11bf3e613fdb0" \
		           "6ff80f968ba1755d27cdf5eab3"
		union_type = Dhall::UnionType.new(
			alternatives: {
				"Natural" => Dhall::Variable["Natural"],
				"Text"    => Dhall::Variable["Text"],
				"None"    => nil,
				"Bool"    => Dhall::Variable["Bool"],
				hash_key  => Dhall::RecordType.new(
					record: {
						"a" => Dhall::Variable["Natural"]
					}
				),
				array_key => Dhall::Application.new(
					function: Dhall::Variable["List"],
					argument: Dhall::Variable["Natural"]
				)
			}
		)

		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, "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(
					record: { "a" => Dhall::Natural.new(value: 1) }
				)),
				Dhall::Union.from(union_type, array_key, Dhall::List.new(
					elements: [Dhall::Natural.new(value: 1)]
				))
			]),
			[1, "hai", nil, true, false, { a: 1 }, [1]].as_dhall
		)
	end

	def test_empty_hash
		assert_equal Dhall::EmptyRecord.new, {}.as_dhall
	end

	def test_hash_of_natural
		assert_equal(
			Dhall::Record.new(record: { "a" => Dhall::Natural.new(value: 1) }),
			{ "a" => 1 }.as_dhall
		)
	end

	def test_hash_of_natural_symbol_keys
		assert_equal(
			Dhall::Record.new(record: { "a" => Dhall::Natural.new(value: 1) }),
			{ a: 1 }.as_dhall
		)
	end

	def test_hash_mixed
		assert_equal(
			Dhall::Record.new(
				record: {
					"a" => Dhall::Natural.new(value: 1),
					"b" => Dhall::Text.new(value: "hai"),
					"c" => Dhall::Bool.new(value: true)
				}
			),
			{ a: 1, b: "hai", c: true }.as_dhall
		)
	end

	def test_hash_nested
		assert_equal(
			Dhall::Record.new(
				record: { "a" => Dhall::Record.new(
					record: { "b" => Dhall::Natural.new(value: 1) }
				) }
			),
			{ a: { b: 1 } }.as_dhall
		)
	end

	def test_openstruct
		assert_equal(
			Dhall::Union.new(
				tag:          "OpenStruct",
				value:        Dhall::TypeAnnotation.new(
					type:  Dhall::EmptyRecordType.new,
					value: Dhall::EmptyRecord.new
				),
				alternatives: Dhall::UnionType.new(alternatives: {})
			),
			OpenStruct.new({}).as_dhall
		)
	end

	class SomeTestClass
		def initialize
			@a = 1
			@b = "hai"
		end
	end

	def test_object
		assert_equal(
			Dhall::Union.new(
				tag:          "TestAsDhall::SomeTestClass",
				value:        Dhall::TypeAnnotation.new(
					type:  Dhall::RecordType.new(
						record: {
							"a" => Dhall::Variable["Natural"],
							"b" => Dhall::Variable["Text"]
						}
					),
					value: Dhall::Record.new(
						record: {
							"a" => Dhall::Natural.new(value: 1),
							"b" => Dhall::Text.new(value: "hai")
						}
					)
				),
				alternatives: Dhall::UnionType.new(alternatives: {})
			),
			SomeTestClass.new.as_dhall
		)
	end
end