~singpolyma/dhall-ruby

134ece750abbc97f6c926b3171af40a41bfa13a2 — Stephen Paul Weber 5 years ago
Decoding Dhall from binary works
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