# 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 module Expression def value key = [:let_binding, :lambda, :forall, :arrow, :if, :merge] .find { |k| captures.key?(k) } return public_send(key) if key key = [:empty_collection, :non_empty_optional] .find { |k| captures.key?(k) } key ? capture(key).value : super end def let_binding LetBlock.for( lets: captures(:let_binding).map(&:value), body: capture(:expression).value ) end def lambda Function.new( var: capture(:nonreserved_label).value, type: captures(:expression)[0].value, body: captures(:expression)[1].value ) end def forall Forall.new( var: capture(:nonreserved_label).value, type: captures(:expression)[0].value, body: captures(:expression)[1].value ) end def arrow Forall.of_arguments( capture(:operator_expression).value, body: capture(:expression).value ) end def if If.new( predicate: captures(:expression)[0].value, then: captures(:expression)[1].value, else: captures(:expression)[2].value ) end def merge Merge.new( record: captures(:import_expression)[0].value, input: captures(:import_expression)[1].value, type: capture(:application_expression)&.value ) end end OPERATORS = { import_alt_expression: :ImportFallback, or_expression: :Or, plus_expression: :Plus, text_append_expression: :TextConcatenate, list_append_expression: :ListConcatenate, and_expression: :And, combine_expression: :RecursiveRecordMerge, prefer_expression: :RightBiasedRecordMerge, combine_types_expression: :RecursiveRecordTypeMerge, times_expression: :Times, equal_expression: :Equal, not_equal_expression: :NotEqual }.freeze OPERATORS.to_a.zip( OPERATORS.to_a[1..-1] + [[:application_expression]] ).each do |((rule, ast_class), (next_rule, _))| const_set(rule.to_s.split(/_/).map(&:capitalize).join, Module.new do define_method(:value) do captures(next_rule).map(&:value).reduce do |lhs, rhs| Operator.const_get(ast_class).new(lhs: lhs, rhs: rhs) end end end) end 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 = capture(:primitive_expression).value selectors = captures(:selector).map(&:value) selectors.reduce(record) do |rec, sels| if sels.is_a?(Array) return EmptyRecordProjection.new(record: rec) if sels.empty? RecordProjection.new(record: rec, selectors: sels) else RecordSelection.new(record: rec, selector: sels) end end end end module Labels def value captures(:any_label).map(&:value) 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 NumericDoubleLiteral def value float = string.to_f raise Citrus::ParseError, input if float.nan? || float.infinite? Double.new(value: float) end end module MinusInfinityLiteral def value Double.new(value: -Float::INFINITY) end end module PlusInfinityLiteral def value Double.new(value: Float::INFINITY) end end module Nan def value Double.new(value: Float::NAN) end end module DoubleQuoteLiteral def value TextLiteral.for( *captures(:double_quote_chunk) .map(&:value) .chunk { |s| s.is_a?(String) } .flat_map do |(strs, group)| strs ? group.map { |s| s.encode("UTF-16BE") }.join : group end ) end end module DoubleQuoteChunk def value if captures.key?(:double_quote_escaped) capture(:double_quote_escaped).value else super end end end module DoubleQuoteEscaped ESCAPES = { "\"" => "\"", "$" => "$", "\\" => "\\", "/" => "/", "b" => "\b", "f" => "\f", "n" => "\n", "r" => "\r", "t" => "\t" }.freeze def value ESCAPES.fetch(string) do [string[1..-1]].pack("H*").force_encoding("UTF-16BE") end end end module SingleQuoteLiteral def value chunks = capture(:single_quote_continue).value 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 def value ([first].compact + captures(:single_quote_continue)).flat_map(&:value) end end module Interpolation def value capture(:complete_expression).value end end module EscapedQuotePair def value "''" end end module EscapedInterpolation def value "${" 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 key = [ :complete_expression, :record_type_or_literal, :union_type_or_literal ].find { |k| captures.key?(k) } key ? capture(key).value : super end end module EmptyUnionType def value UnionType.new(alternatives: {}) end end module UnionTypeOrLiteralVariantType def value(label) rest = capture(:non_empty_union_type_or_literal)&.value type = UnionType.new( alternatives: { label => capture(:expression)&.value } ) if rest.is_a?(Union) rest.with(alternatives: type.merge(rest.alternatives)) else rest ? type.merge(rest) : type end end end module UnionLiteralVariantValue def value(label) Union.new( tag: label, value: capture(:expression).value, alternatives: captures(:union_type_entry).map(&:value) .reduce(UnionType.new(alternatives: {}), &:merge) ) end end module UnionTypeEntry def value UnionType.new( alternatives: { capture(:any_label).value => capture(:expression)&.value } ) end end module NonEmptyUnionTypeOrLiteral def value key = [ :union_literal_variant_value, :union_type_or_literal_variant_type ].find { |k| captures.key?(k) } if key capture(key).value(capture(:any_label).value) else no_alts = UnionType.new(alternatives: {}) Union.from(no_alts, capture(:any_label).value, nil) end end end module EmptyRecordLiteral def value EmptyRecord.new end end module EmptyRecordType def value Dhall::EmptyRecordType.new end end module NonEmptyRecordTypeOrLiteral def value key = [ :non_empty_record_literal, :non_empty_record_type ].find { |k| captures.key?(k) } capture(key).value(capture(:any_label).value) end end module NonEmptyRecordLiteral def value(first_key) Record.new( record: captures(:record_literal_entry).map(&:value).reduce( first_key => capture(:expression).value ) do |final, rec| final.merge(rec) { raise TypeError, "duplicate field" } end ) end end module RecordLiteralEntry def value { capture(:any_label).value => capture(:expression).value } end end module NonEmptyRecordType def value(first_key) RecordType.new( record: captures(:record_type_entry).map(&:value).reduce( { first_key => capture(:expression).value }, &:merge ) ) end end RecordTypeEntry = RecordLiteralEntry 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 matches[1].string.empty? first.value else TypeAnnotation.new( value: first.value, type: capture(:expression).value ) end end end module LetBinding def value exprs = captures(:expression) Let.new( var: capture(:nonreserved_label).value, assign: exprs.last.value, type: exprs.length > 1 ? exprs.first.value : nil ) 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 value http = capture(:http_raw) SCHEME.fetch(http.capture(:scheme).value).new( capture(:import_hashed)&.value(Dhall::Import::Expression), http.capture(:authority).value, *unescaped_components, http.capture(:query)&.value ) end def unescaped_components capture(:http_raw) .capture(:path) .captures(:path_component) .map do |pc| pc.value(URI.method(:unescape)) end 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) { raise "Invalid escape: #{string}" }.encode("UTF-16BE") else string end end end module AbsolutePath def value Dhall::Import::AbsolutePath.new(*super) end end module HerePath def value Dhall::Import::RelativePath.new(*capture(:path).value) end end module ParentPath def value Dhall::Import::RelativeToParentPath.new(*capture(:path).value) end end module HomePath def value Dhall::Import::RelativeToHomePath.new(*capture(:path).value) end end module Path def value captures(:path_component).map(&:value) end end module PathComponent def value(unescaper=:itself.to_proc) if captures.key?(:quoted_path_component) capture(:quoted_path_component).value else unescaper.call(capture(:unquoted_path_component).value) end end end module Missing def value Dhall::Import::MissingImport.new end end end end require "citrus" Citrus.require "dhall/parser"