A => .builds.dhall/debian-stable.dhall +19 -0
@@ 1,19 @@
+{
+ image = "debian/stable",
+ packages = [
+ "ruby",
+ "bundler",
+ "rubocop"
+ ],
+ sources = ["https://git.sr.ht/~singpolyma/dhall-ruby"],
+ tasks = [
+ { build =
+ ''
+ cd dhall-ruby
+ rubocop
+ bundle install --path="../.gems"
+ bundle exec ruby -Ilib test/test_binary.rb
+ ''
+ }
+ ]
+}
A => .builds.dhall/gen +5 -0
@@ 1,5 @@
+#!/bin/sh
+
+for FILE in .builds.dhall/*.dhall; do
+ echo "./$FILE" | dhall-to-yaml > .builds/$(basename "$FILE" .dhall).yml
+done
A => .builds/debian-stable.yml +13 -0
@@ 1,13 @@
+image: debian/stable
+sources:
+- https://git.sr.ht/~singpolyma/dhall-ruby
+tasks:
+- build: |
+ cd dhall-ruby
+ rubocop
+ bundle install --path="../.gems"
+ bundle exec ruby -Ilib test/test_binary.rb
+packages:
+- ruby
+- bundler
+- rubocop
A => .gitmodules +3 -0
@@ 1,3 @@
+[submodule "dhall-lang"]
+ path = dhall-lang
+ url = https://github.com/dhall-lang/dhall-lang.git
A => .rubocop.yml +47 -0
@@ 1,47 @@
+AllCops:
+ TargetRubyVersion: 2.3.3
+
+Metrics/LineLength:
+ Max: 80
+
+Layout/Tab:
+ Enabled: false
+
+Layout/IndentationWidth:
+ Width: 1 # one tab
+
+Layout/EndAlignment:
+ EnforcedStyleAlignWith: variable
+
+Layout/CaseIndentation:
+ EnforcedStyle: end
+
+Layout/SpaceAroundEqualsInParameterDefault:
+ EnforcedStyle: no_space
+
+Layout/IndentArray:
+ EnforcedStyle: consistent
+
+Layout/FirstParameterIndentation:
+ EnforcedStyle: consistent
+
+Style/BlockDelimiters:
+ EnforcedStyle: braces_for_chaining
+
+Style/Documentation:
+ Enabled: false
+
+Style/FormatString:
+ EnforcedStyle: percent
+
+Style/StringLiterals:
+ EnforcedStyle: double_quotes
+
+Style/SymbolArray:
+ EnforcedStyle: brackets
+
+Naming/UncommunicativeBlockParamName:
+ Enabled: false
+
+Naming/UncommunicativeMethodParamName:
+ Enabled: false
A => Gemfile +5 -0
@@ 1,5 @@
+# frozen_string_literal: true
+source "https://rubygems.org"
+
+gem "cbor"
+gem "rubocop"
A => dhall-lang +1 -0
@@ 1,1 @@
+Subproject commit e4e9c2d52766017e726ec5a0198a9cd3bc461179
A => lib/dhall.rb +4 -0
@@ 1,4 @@
+# frozen_string_literal: true
+
+require "dhall/ast"
+require "dhall/cbor"
A => lib/dhall/ast.rb +248 -0
@@ 1,248 @@
+# frozen_string_literal: true
+
+module Dhall
+ class Expression; end
+
+ class Application < Expression
+ def initialize(f, *args)
+ if args.empty?
+ raise ArgumentError, "Application requires at least one argument"
+ end
+
+ @f = f
+ @args = args
+ end
+ end
+
+ class Function < Expression
+ def initialize(var, type, body)
+ @var = var
+ @type = type
+ @body = body
+ end
+ end
+
+ class Forall < Function; end
+
+ class Bool < Expression
+ def initialize(value)
+ @value = value
+ end
+ end
+
+ class Variable < Expression
+ def initialize(var, index=0)
+ @var = var
+ @index = index
+ end
+ end
+
+ class Operator < Expression
+ def initialize(op, lhs, rhs)
+ @op = op
+ @lhs = lhs
+ @rhs = rhs
+ end
+ end
+
+ class List < Expression
+ def initialize(*els)
+ @els = els
+ end
+ end
+
+ class EmptyList < List
+ def initialize(type)
+ @type = type
+ end
+ end
+
+ class Optional < Expression
+ def initialize(value, type=nil)
+ raise TypeError, "value must not be nil" if value.nil?
+
+ @value = value
+ @type = type
+ end
+ end
+
+ class OptionalNone < Optional
+ def initialize(type)
+ raise TypeError, "type must not be nil" if type.nil?
+
+ @type = type
+ end
+ end
+
+ class Merge < Expression
+ def initialize(record, input, type)
+ @record = record
+ @input = input
+ @type = type
+ end
+ end
+
+ class RecordType < Expression
+ def initialize(record)
+ @record = record
+ end
+ end
+
+ class Record < Expression
+ def initialize(record)
+ @record = record
+ end
+ end
+
+ class RecordFieldAccess < Expression
+ def initialize(record, field)
+ raise TypeError, "field must be a String" unless field.is_a?(String)
+
+ @record = record
+ @field = field
+ end
+ end
+
+ class RecordProjection < Expression
+ def initialize(record, *fields)
+ unless fields.all? { |x| x.is_a?(String) }
+ raise TypeError, "fields must be String"
+ end
+
+ @record = record
+ @fields = fields
+ end
+ end
+
+ class UnionType < Expression
+ def initialize(record)
+ @record = record
+ end
+ end
+
+ class Union < Expression
+ def initialize(tag, value, rest_of_type)
+ raise TypeError, "tag must be a string" unless tag.is_a?(String)
+
+ @tag = tag
+ @value = value
+ @rest_of_type = rest_of_type
+ end
+ end
+
+ class Constructors < Expression
+ extend Gem::Deprecate
+
+ def initialize(arg)
+ @arg = arg
+ end
+ DEPRETATION_WIKI = "https://github.com/dhall-lang/dhall-lang/wiki/" \
+ "Migration:-Deprecation-of-constructors-keyword"
+ deprecate :initialize, DEPRECATION_WIKI, 2019, 4
+ end
+
+ class If < Expression
+ def initialize(cond, thn, els)
+ @cond = cond
+ @thn = thn
+ @els = els
+ end
+ end
+
+ class Number < Expression
+ def initialize(n)
+ @n = n
+ end
+ end
+
+ class Natural < Number; end
+ class Integer < Number; end
+ class Double < Number; end
+
+ class Text < Expression
+ def initialize(string)
+ raise TypeError, "must be a String" unless string.is_a?(String)
+
+ @string = string
+ end
+ end
+
+ class TextLiteral < Text
+ def initialize(*chunks)
+ @chunks = chunks
+ end
+ end
+
+ class Import < Expression
+ def initialize(integrity_check, import_type, path)
+ @integrity_check = integrity_check
+ @import_type = import_type
+ @path = path
+ end
+
+ class URI
+ def initialize(headers, authority, *path, query, fragment)
+ @headers = headers
+ @authority = authority
+ @path = path
+ @query = query
+ @fragment = fragment
+ end
+ end
+
+ class Http < URI; end
+ class Https < URI; end
+
+ class Path
+ def initialize(*path)
+ @path = path
+ end
+ end
+
+ class AbsolutePath < Path; end
+ class RelativePath < Path; end
+ class RelativeToParentPath < Path; end
+ class RelativeToHomePath < Path; end
+
+ class EnvironmentVariable
+ def initialize(var)
+ @var = var
+ end
+ end
+
+ class MissingImport; end
+
+ class IntegrityCheck
+ def initialize(protocol, data)
+ @protocol = protocol
+ @data = data
+ end
+ end
+ end
+
+ class Let
+ def initialize(var, assign, type=nil)
+ @var = var
+ @assign = assign
+ @type = type
+ end
+ end
+
+ class LetBlock < Expression
+ def initialize(body, *lets)
+ unless lets.all? { |x| x.is_a?(Let) }
+ raise TypeError, "LetBlock only contains Let"
+ end
+
+ @lets = lets
+ @body = body
+ end
+ end
+
+ class TypeAnnotation < Expression
+ def initialize(value, type)
+ @value = value
+ @type = type
+ end
+ end
+end
A => lib/dhall/binary.rb +241 -0
@@ 1,241 @@
+# frozen_string_literal: true
+
+require "cbor"
+
+module Dhall
+ def self.from_binary(cbor_binary)
+ data = CBOR.decode(cbor_binary)
+ if data.is_a?(Array) && data[0] == "5.0.0"
+ decode(data[1])
+ else
+ decode(data)
+ end
+ end
+
+ def self.decode(expression)
+ BINARY.each do |match, use|
+ return use[expression] if expression.is_a?(match)
+ end
+
+ raise "Unknown expression: #{expression.inspect}"
+ end
+
+ BINARY = {
+ ::TrueClass => Bool.method(:decode),
+ ::FalseClass => Bool.method(:decode),
+ ::Float => Double.method(:decode),
+ ::String => ->(e) { Variable.decode(e, 0) },
+ ::Integer => ->(e) { Variable.decode("_", e) },
+ ::Array => lambda { |e|
+ if e.length == 2 && e.first.is_a?(::String)
+ Variable.decode(*expression)
+ else
+ tag, *body = expression
+ BINARY_TAGS[tag]&.decode(*body) ||
+ (raise "Unknown expression: #{expression.inspect}")
+ end
+ }
+ }.freeze
+
+ BINARY_TAGS = [
+ Application,
+ Function,
+ Forall,
+ Operator,
+ List,
+ Optional,
+ Merge,
+ RecordType,
+ Record,
+ RecordFieldAccess,
+ RecordProjection,
+ UnionType,
+ Union,
+ Constructors,
+ If,
+ Natural,
+ Integer,
+ nil,
+ TextLiteral,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ Import,
+ LetBlock,
+ TypeAnnotation
+ ].freeze
+
+ class Expression
+ def self.decode(*args)
+ new(*args)
+ end
+ end
+
+ class Application < Expression
+ def self.decode(f, *args)
+ new(Dhall.decode(f), *args.map(&Dhall.method(:decode)))
+ end
+ end
+
+ class Function < Expression
+ def self.decode(var_or_type, type_or_body, body_or_nil=nil)
+ if body_or_nil.nil?
+ new("_", Dhall.decode(var_or_type), Dhall.decode(type_or_body))
+ else
+ unless var_or_type.is_a?(String)
+ raise TypeError, "Function var must be a String"
+ end
+
+ raise ArgumentError, "explicit var named _" if var_or_type == "_"
+
+ new(var_or_type, Dhall.decode(type_or_body), Dhall.decode(body_or_nil))
+ end
+ end
+ end
+
+ class Operator < Expression
+ OPCODES = [
+ :'||', :'&&', :==, :!=, :+, :*, :'++', :'#', :∧, :⫽, :⩓, :'?'
+ ].freeze
+
+ def self.decode(opcode, lhs, rhs)
+ new(
+ OPCODES[opcode] || (raise "Unknown opcode: #{opcode}"),
+ Dhall.decode(lhs),
+ Dhall.decode(rhs)
+ )
+ end
+ end
+
+ class List < Expression
+ def self.decode(type, *els)
+ if type.nil?
+ List.new(*els.map(&Dhall.method(:decode)))
+ else
+ EmptyList.new(Dhall.decode(type))
+ end
+ end
+ end
+
+ class Optional < Expression
+ def self.decode(type, value=nil)
+ if value.nil?
+ OptionalNone.new(Dhall.decode(type))
+ else
+ Optional.new(
+ Dhall.decode(value),
+ type.nil? ? type : Dhall.decode(type)
+ )
+ end
+ end
+ end
+
+ class Merge < Expression
+ def self.decode(record, input, type=nil)
+ new(
+ Dhall.decode(record),
+ Dhall.decode(input),
+ type.nil? ? nil : Dhall.decode(type)
+ )
+ end
+ end
+
+ class RecordType < Expression
+ def self.decode(record)
+ new(Hash[record.map { |k, v| [k, Dhall.decode(v)] }])
+ end
+ end
+
+ class Record < Expression
+ def self.decode(record)
+ new(Hash[record.map { |k, v| [k, Dhall.decode(v)] }])
+ end
+ end
+
+ class RecordFieldAccess < Expression
+ def self.decode(record, field)
+ new(Dhall.decode(record), field)
+ end
+ end
+
+ class RecordProjection < Expression
+ def self.decode(record, *fields)
+ new(Dhall.decode(record), *fields)
+ end
+ end
+
+ class UnionType < Expression
+ def self.decode(record)
+ new(Hash[record.map { |k, v| [k, Dhall.decode(v)] }])
+ end
+ end
+
+ class Union < Expression
+ def self.decode(tag, value, rest_of_type)
+ new(
+ tag,
+ Dhall.decode(value),
+ Hash[rest_of_type.map { |k, v| [k, Dhall.decode(v)] }]
+ )
+ end
+ end
+
+ class If < Expression
+ def self.decode(cond, thn, els)
+ new(Dhall.decode(cond), Dhall.decode(thn), Dhall.decode(els))
+ end
+ end
+
+ class TextLiteral < Text
+ def self.decode(*chunks)
+ if chunks.length == 1 && chunks.is_a?(String)
+ Text.new(chunks.first)
+ else
+ TextLiteral.new(*chunks.map do |chunk|
+ chunk.is_a?(String) ? Text.new(chunk) : Dhall.decode(chunk)
+ end)
+ end
+ end
+ end
+
+ class Import
+ IMPORT_TYPES = [Expression, Text].freeze
+ PATH_TYPES = [
+ Http, Https,
+ AbsolutePath, RelativePath, RelativeToParentPath, RelativeToHomePath,
+ EnvironmentVariable, MissingImport
+ ].freeze
+
+ def self.decode(integrity_check, import_type, path_type, *parts)
+ parts[0] = Dhall.decode(parts[0]) if path_type < 2 && !parts[0].nil?
+ new(
+ integrity_check.nil? ? nil : IntegrityCheck.new(*integrity_check),
+ IMPORT_TYPES[import_type],
+ PATH_TYPES[path_type].new(*parts)
+ )
+ end
+ end
+
+ class LetBlock < Expression
+ def self.decode(*parts)
+ new(
+ Dhall.decode(parts.pop),
+ *parts.each_slice(3).map do |(var, type, assign)|
+ Let.new(
+ var,
+ Dhall.decode(assign),
+ type.nil? ? nil : Dhall.decode(type)
+ )
+ end
+ )
+ end
+ end
+
+ class TypeAnnotation < Expression
+ def self.decode(value, type)
+ new(Dhall.decode(value), Dhall.decode(type))
+ end
+ end
+end
A => test/test_binary.rb +19 -0
@@ 1,19 @@
+# frozen_string_literal: true
+
+require "minitest/autorun"
+require "pathname"
+
+require "dhall/ast"
+require "dhall/binary"
+
+DIRPATH = Pathname.new(File.dirname(__FILE__))
+TESTS = DIRPATH + "/../dhall-lang/tests/parser/success/"
+
+class TestParser < Minitest::Test
+ Pathname.glob(TESTS + "*B.dhallb").each do |path|
+ test = path.basename("B.dhallb").to_s
+ define_method("test_#{test}") do
+ assert_kind_of Dhall::Expression, Dhall.from_binary(path.read)
+ end
+ end
+end