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