# frozen_string_literal: true require "dhall/ast" require "dhall/builtins" module Dhall module Parser def self.parse(*args) CitrusParser.parse(*args) end def self.parse_file(*args) CitrusParser.parse_file(*args) end module CompleteExpression def value capture(:expression).value end end def self.operator_expression(capture, ast_class) Module.new do define_method(:value) do captures(capture).map(&:value).reduce do |lhs, rhs| Operator.const_get(ast_class).new(lhs: lhs, rhs: rhs) end end end end ImportAltExpression = operator_expression(:or_expression, :ImportFallback) OrExpression = operator_expression(:plus_expression, :Or) PlusExpression = operator_expression(:text_append_expression, :Plus) TextAppendExpression = operator_expression(:list_append_expression, :TextConcatenate) ListAppendExpression = operator_expression(:and_expression, :ListConcatenate) AndExpression = operator_expression(:combine_expression, :And) CombineExpression = operator_expression(:prefer_expression, :RecursiveRecordMerge) PreferExpression = operator_expression(:combine_types_expression, :RightBiasedRecordMerge) CombineTypesExpression = operator_expression(:times_expression, :RecursiveRecordTypeMerge) TimesExpression = operator_expression(:equal_expression, :Times) EqualExpression = operator_expression(:not_equal_expression, :Equal) NotEqualExpression = operator_expression(:application_expression, :NotEqual) module ApplicationExpression def value some = capture(:some) ? [Variable["Some"]] : [] els = some + captures(:import_expression).map(&:value) els.reduce do |f, arg| Application.for(function: f, argument: arg) end end end module SelectorExpression def value record = first.value selectors = matches[1].matches selectors.reduce(record) do |rec, sel| if sel.captures.key?(:labels) sels = sel.capture(:labels).captures(:any_label).map(&:value) return EmptyRecordProjection.new(record: rec) if sels.empty? RecordProjection.new(record: rec, selectors: sels) else RecordSelection.new( record: rec, selector: sel.capture(:any_label).value ) end end end end module Label def value if first.string == "`" matches[1].string else string end end end module NonreservedLabel def value if captures.key?(:label) capture(:label).value else string end end end module NaturalLiteral def value Natural.new(value: string.to_i) end end module IntegerLiteral def value Integer.new(value: string.to_i) end end module DoubleLiteral def value key = captures.keys.select { |k| k.is_a?(Symbol) }.first Double.new(value: case key when :infinity string == "-Infinity" ? -Float::INFINITY : Float::INFINITY when :nan Float::NAN else float = string.to_f if float.nan? || float.infinite? raise Citrus::ParseError, input end float end ) end end module DoubleQuoteLiteral def value TextLiteral.for( *captures(:double_quote_chunk) .map(&:value) .chunk { |s| s.is_a?(String) } .flat_map { |(is_string, group)| is_string ? group.join : group } ) end end module DoubleQuoteChunk ESCAPES = { "\"" => "\"", "$" => "$", "\\" => "\\", "/" => "/", "b" => "\b", "f" => "\f", "n" => "\n", "r" => "\r", "t" => "\t" }.freeze def value if first&.string == "\\" && matches[1].string =~ /\Au\h+\Z/i [matches[1].string[1..-1]].pack("H*").force_encoding("UTF-16BE") elsif first&.string == "\\" ESCAPES.fetch(matches[1].string) do raise "Invalid escape: #{string}" end.encode("UTF-16BE") elsif first&.string == "${" matches[1].value else string.encode("UTF-16BE") end end end module SingleQuoteLiteral def value chunks = capture(:single_quote_continue).value.flatten indent = chunks.join.split(/\n/, -1).map { |line| line.match(/^( *|\t*)/).to_s.length }.min TextLiteral.for( *chunks .chunk { |c| c != "\n" } .flat_map { |(line, chunk)| line ? chunk[indent..-1] : chunk } ) end end module SingleQuoteContinue ESCAPES = { "'''" => "''", "''${" => "${" }.freeze def value if matches.length == 2 [ESCAPES.fetch(first.string, first.string), matches[1].value] elsif matches.length == 0 [] else [ capture(:complete_expression).value, capture(:single_quote_continue).value ] end end end module NonEmptyListLiteral def value List.new(elements: captures(:expression).map(&:value)) end end module Identifier def value name = capture(:any_label).value return Dhall::Bool.new(value: true) if name == "True" return Dhall::Bool.new(value: false) if name == "False" Dhall::Builtins::ALL[name]&.new || Variable.new( name: name, index: capture(:natural_literal)&.string.to_i ) end end module PrimitiveExpression def value if first&.string == "(" capture(:expression).value elsif first&.string == "{" capture(:record_type_or_literal).value elsif first&.string == "<" capture(:union_type_or_literal).value else super end end end module UnionTypeOrLiteral def value if captures[0].string == "" UnionType.new(alternatives: {}) else super end end end module NonEmptyUnionTypeOrLiteral def value cont = matches[1].first if cont && cont.matches[1].first.string == "=" Union.new( tag: captures(:any_label).first.value, value: captures(:expression).first.value, alternatives: UnionType.new(alternatives: ::Hash[ captures(:any_label)[1..-1].map(&:value).zip( captures(:expression)[1..-1].map(&:value) ) ]) ) else type = UnionType.new(alternatives: ::Hash[ captures(:any_label).map(&:value).zip( captures(:expression).map(&:value) ) ]) rest = cont && cont.matches[1].capture(:non_empty_union_type_or_literal)&.value if rest.is_a?(Union) rest.with(alternatives: type.merge(rest.alternatives)) elsif rest type.merge(rest) else type end end end end module RecordTypeOrLiteral def value if captures[0].string == "=" EmptyRecord.new elsif captures[0].string == "" EmptyRecordType.new else super end end end module NonEmptyRecordTypeOrLiteral def value if captures.key?(:non_empty_record_literal) capture(:non_empty_record_literal).value( capture(:any_label).value ) else capture(:non_empty_record_type).value( capture(:any_label).value ) end end end module NonEmptyRecordLiteral def value(first_key) keys = [first_key] + captures(:any_label).map(&:value) values = captures(:expression).map(&:value) Record.new(record: ::Hash[keys.zip(values)]) end end module NonEmptyRecordType def value(first_key) keys = [first_key] + captures(:any_label).map(&:value) values = captures(:expression).map(&:value) RecordType.new(record: ::Hash[keys.zip(values)]) end end module EmptyCollection def value if captures.key?(:list) EmptyList.new(element_type: capture(:import_expression).value) else OptionalNone.new(value_type: capture(:import_expression).value) end end end module NonEmptyOptional def value Optional.new( value: capture(:expression).value, value_type: capture(:import_expression).value ) end end module AnnotatedExpression def value if captures.key?(:empty_collection) capture(:empty_collection).value elsif captures.key?(:non_empty_optional) capture(:non_empty_optional).value elsif matches.length == 2 TypeAnnotation.new( value: first.value, type: matches[1].capture(:expression).value ) else super end end end module Expression def value keys = captures.keys.select { |k| k.is_a?(Symbol) } if keys.length == 1 capture(keys.first).value elsif captures.key?(:let) lets = first.matches.map { |let_match| exprs = let_match.captures(:expression) Let.new( var: let_match.capture(:nonreserved_label).value, assign: exprs.last.value, type: exprs.length > 1 ? exprs.first.value : nil, ) } if lets.length == 1 LetIn.new(let: lets.first, body: matches.last.value) else LetBlock.new(lets: lets, body: matches.last.value) end elsif captures.key?(:lambda) Function.new( var: capture(:nonreserved_label).value, type: captures(:expression)[0].value, body: captures(:expression)[1].value ) elsif captures.key?(:forall) Forall.new( var: capture(:nonreserved_label).value, type: captures(:expression)[0].value, body: captures(:expression)[1].value ) elsif captures.key?(:arrow) Forall.of_arguments( capture(:operator_expression).value, body: capture(:expression).value ) elsif captures.key?(:if) If.new( predicate: captures(:expression)[0].value, then: captures(:expression)[1].value, else: captures(:expression)[2].value, ) elsif captures.key?(:merge) Merge.new( record: captures(:import_expression)[0].value, input: captures(:import_expression)[1].value, type: capture(:application_expression)&.value ) else super end end end module Import def value import_type = if captures.key?(:text) Dhall::Import::Text else Dhall::Import::Expression end capture(:import_hashed).value(import_type) end end module ImportHashed def value(import_type) integrity_check = capture(:hash)&.value path = capture(:import_type).value Dhall::Import.new(integrity_check, import_type, path) end end module Hash def value protocol, data = string.split(/:/, 2) Dhall::Import::IntegrityCheck.new(protocol, data) end end module Http SCHEME = { "http" => Dhall::Import::Http, "https" => Dhall::Import::Https, }.freeze def self.escape(s) URI.encode_www_form_component(s).gsub("+", "%20") end def value http = capture(:http_raw) SCHEME.fetch(http.capture(:scheme).value).new( if captures.key?(:import_hashed) capture(:import_hashed).value(Dhall::Import::Expression) end, http.capture(:authority).value, *http.capture(:path).captures(:path_component).map { |c| # https://github.com/dhall-lang/dhall-lang/issues/456 c.value # (Http.method(:escape)) }, http.capture(:query)&.value ) end end module Env def value Dhall::Import::EnvironmentVariable.new( if captures.key?(:bash_environment_variable) capture(:bash_environment_variable).string else capture(:posix_environment_variable).value.encode("utf-8") end ) end end module PosixEnvironmentVariable def value matches.map(&:value).join end end module PosixEnvironmentVariableCharacter ESCAPES = Dhall::Import::EnvironmentVariable::ESCAPES def value if first&.string == "\\" ESCAPES.fetch(matches[1].string) do raise "Invalid escape: #{string}" end.encode("UTF-16BE") else string end end end module Local KLASS = { "/" => Dhall::Import::AbsolutePath, "." => Dhall::Import::RelativePath, ".." => Dhall::Import::RelativeToParentPath, "~" => Dhall::Import::RelativeToHomePath }.freeze def value path = capture(:path).captures(:path_component).map(&:value) klass = KLASS.find { |prefix, _| string.start_with?(prefix) }.last klass.new(*path) end end module PathComponent def value(escaper=:itself.to_proc) if captures.key?(:quoted_path_character) escaper.call(matches[1].matches[1].value) else matches[1].value end end end module Missing def value Dhall::Import::MissingImport.new end end end end require "citrus" Citrus.require "dhall/parser"