require File.join(File.dirname(__FILE__), 'ast.rb') require File.join(File.dirname(__FILE__), 'types.rb') require File.join(File.dirname(__FILE__), 'utils.rb')

grammar DhallishGrammar

rule root
        space? exp:expression space? { def to_node()  exp.to_node() end }
end

rule expression
        exp:if_then_else_expression             { def to_node() exp.to_node() end } /
        exp:let_expression                      { def to_node() exp.to_node() end } /
        exp:import_alternative_expression       { def to_node() exp.to_node() end }
end

rule eol
        "\r"? "\n"
end

## Comments
rule single_line_comment
        "--" (!"\n" .)* "\n"
end

rule multi_line_comment
        "{-" multi_line_comment_continue
end

rule multi_line_comment_chunk
        multi_line_comment / (!"{-" .)
end

rule multi_line_comment_continue
        "-}" / multi_line_comment_chunk multi_line_comment_continue
end

rule space
        (" " / "\t" / eol / single_line_comment / multi_line_comment)+
end

## Number Literals
rule integer_literal
        ("+" / "-") [0-9]+ { def to_node() Dhallish::Ast::Literal_Node.new(text_value.to_i, Dhallish::Types::Integer)  end }
end

rule exponent
        "e" ("+" / "-")? [0-9]+

end

rule double_literal
        ("+" / "-")? [0-9]+ (("." [0-9]+ exponent?) / exponent) { def to_node() Dhallish::Ast::Literal_Node.new(text_value.to_f, Dhallish::Types::Double) end }

end

rule natural_literal
        [0-9]+ { def to_node() Dhallish::Ast::Literal_Node.new(text_value.to_i, Dhallish::Types::Natural) end }
end

## Bools
rule bool_literal
        "True" { def to_node() Dhallish::Ast::Literal_Node.new(true, Dhallish::Types::Bool) end }
        /
        "False" { def to_node() Dhallish::Ast::Literal_Node.new(false, Dhallish::Types::Bool) end }
end

## Lists
rule empty_list_literal
        "[" space? "]" space? ":" space? "List" space type:expression
        {
                def to_node()
                        Dhallish::Ast::ListNode.new [], type.to_node()
                end
        }
end

rule non_empty_list_literal
        "[" space? fst:expression tail:(space? "," space? exp:expression)* space? "]" annot:(space? ":" space? "List" space type:expression)?
        {
                def to_node()
                        list = []
                        list.append fst.to_node()
                        tail.elements.each { |node|
                                list.append node.exp.to_node()
                        }
                        if annot.respond_to? :type
                                Dhallish::Ast::ListNode.new list, annot.type.to_node()
                        else
                                Dhallish::Ast::ListNode.new list, nil
                        end
                end
        }
end

rule list_literal
        exp:empty_list_literal          { def to_node() exp.to_node() end } /
        exp:non_empty_list_literal      { def to_node() exp.to_node() end }
end

## Records
rule record_type_literal
        "{" space? "}" { def to_node() Dhallish::Ast::RecordTypeNode.new({}) end } /
        "{" space? fstkey:label space? ":" space? fstexp:expression tail:(space? "," space? key:label space? ":" space? exp:expression)* space? "}"
        {
                def to_node()
                        data = { fstkey.text_value => fstexp.to_node() }
                        tail.elements.each { |node|
                                key = node.key.text_value
                                assert("no key should apeare multiple times in a record: `#{key}`") { !data.key?(key) }
                                data[key] = node.exp.to_node()
                        }
                        Dhallish::Ast::RecordTypeNode.new data
                end
        }
end

rule record_literal
        "{" space? "=" space? "}" { def to_node() Dhallish::Ast::RecordNode.new({}) end } /
        "{" space? fstkey:label space? "=" space? fstexp:expression tail:(space? "," space? key:label space? "=" space? exp:expression)* space? "}"
        {
                def to_node()
                        data = { fstkey.text_value => fstexp.to_node() }
                        tail.elements.each { |node|
                                key = node.key.text_value
                                assert("no key should apeare multiple times in a record: `#{key}`") { !data.key?(key) }
                                data[key] = node.exp.to_node()
                        }
                        Dhallish::Ast::RecordNode.new data
                end
        }
end

## Strings
rule text_literal
        '"' tail:(exp:('${' space? innerexp:expression space? '}') / esc:('\"') / any:(!'"' .))* '"'
        {
                def to_node()
                        parts = []
                        tail.elements.each { |node|
                                if node.respond_to?(:exp)
                                        parts.push node.exp.innerexp.to_node()
                                elsif node.respond_to?(:esc)
                                        parts.push "\""
                                else
                                        parts.push node.any.text_value
                                end
                        }
                        Dhallish::Ast::TextInterpolationNode.new parts
                end
        }
end

## Optionals
rule optional_literal
        prefix:("Some" / "None") space exp:or_expression { def to_node() Dhallish::Ast::OptionalNode.new prefix.text_value, exp.to_node() end }
end

## Type Literals
rule forall_literal
        ("forall" / "∀") space? "(" space? lb:label space? ":" space? type:expression space? ")" space? ("->" / "→") space? res_type:expression
        {
                def to_node()
                        Dhallish::Ast::FunctionType.new(type.to_node(), res_type.to_node(), lb.text_value)
                end
        }
end

## Union Literals
rule union_literal
        "<" space? start:(lb:label space? ":" space? type:record_merge_expression space? "|" space?)* lb:label space? "=" space? expr:add_sub_expression tail:(space? "|" space? lb:label space? ":" space? type:record_merge_expression)* space? ">"
        {
                def to_node()
                        typed_labels = {}
                        start.elements.each { |node|
                                typed_labels[node.lb.text_value] = node.type.to_node()
                        }
                        tail.elements.each { |node|
                                typed_labels[node.lb.text_value] = node.type.to_node()
                        }
                        Dhallish::Ast::UnionLiteral.new(typed_labels, lb.text_value, expr.to_node())
                end
        }
end

rule empty_union_type_literal
        "<" space? ">"
end

rule union_type_literal
        empty_union_type_literal { def to_node() Dhallish::Ast::UnionType.new({})  end } /
        "<" space? lb:label space? ":" space? type:record_merge_expression tail:(space? "|" space? lb:label space? ":" space? type:record_merge_expression)* space? ">"
        {
                def to_node()
                        type_map = {lb.text_value => type.to_node()}
                        tail.elements.each { |node|
                                type_map[node.lb.text_value] = node.type.to_node()
                        }
                        Dhallish::Ast::UnionType.new(type_map)
                end
        }

end

## Rules for operator expressions
## rules are reversely ordered by their precedence
## e.g. if rule X is over rule Y, the operator associated with rule Y has a stronger precedence than Xs operator

rule import_alternative_expression
        exp:annotated_expression tail:(space? "?" space? alternative:annotated_expression)*
        {
                def to_node()
                tail.elements.reduce(exp.to_node()) { |tree, node|
                        Dhallish::Ast::Import_Alternative.new tree, node.alternative.to_node()
                }
                end
        }
end

rule annotated_expression
        exp:function_type_expression tail:(space? ":" space? type_expr:function_type_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                Dhallish::Ast::TypeAnnotationNode.new tree, node.type_expr.to_node()
                        }
                end
        }
end

rule function_type_expression
        arg_type:or_expression tail:(space? ("->" / "→") space? res_type:or_expression)*
        {
                def to_node()
                        if tail.elements.size == 0
                                arg_type.to_node()
                        else
                                tree = nil
                                tail.elements.reverse.each { |node|
                                        if tree.nil?
                                                tree = node.res_type.to_node()
                                        else
                                                tree = Dhallish::Ast::FunctionType.new node.res_type.to_node(), tree
                                        end
                                }
                                Dhallish::Ast::FunctionType.new arg_type.to_node(), tree
                        end
                end
        }
end

rule or_expression
        exp:and_expression tail:(space? "||" space? exp:and_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                Dhallish::Ast::BinaryArithOpNode.new([Dhallish::Types::Bool], tree, node.exp.to_node(), "||") { |x, y| x || y }
                        }
                end
        }
end

rule and_expression
        exp:comparison_expression tail:(space? "&&" space? exp:comparison_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                Dhallish::Ast::BinaryArithOpNode.new([Dhallish::Types::Bool], tree, node.exp.to_node(), "&&") { |x, y| x && y }
                        }
                end
        }
end

rule comparison_expression
        exp:add_sub_expression tail:(space? op:("==" / "!=" / "<=" / "<" / ">=" / ">")  space? exp:add_sub_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                Dhallish::Ast::ComparisonOpNode.new(tree, node.exp.to_node(), node.op.text_value) { |lhs_val, rhs_val| lhs_val.send(node.op.text_value, rhs_val )}
                        }
                end
        }
end

rule add_sub_expression
        exp:mult_div_expression tail:(space? op:("+"/"-") space? exp:mult_div_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                exp = node.exp.to_node()
                                if node.op.text_value == "+"
                                        Dhallish::Ast::BinaryArithOpNode.new(Dhallish::Types::Numbers, tree, exp, "+") { |x, y| x + y }
                                else
                                        Dhallish::Ast::BinaryArithOpNode.new([Dhallish::Types::Double, Dhallish::Types::Integer], tree, exp, "-") { |x, y| x - y }
                                end
                        }
                end
        }
end

rule mult_div_expression
        exp:list_concat_expression tail:(space? op:("*"/"/") space? exp:list_concat_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                exp = node.exp.to_node()
                                if node.op.text_value == "*"
                                        Dhallish::Ast::BinaryArithOpNode.new(Dhallish::Types::Numbers, tree, exp, "*") { |x, y| x * y }
                                else
                                        Dhallish::Ast::BinaryArithOpNode.new([Dhallish::Types::Double, Dhallish::Types::Integer], tree, exp, "/") { |x, y| x / y }
                                end

                        }
                end
        }
end

rule list_concat_expression
        exp:text_append_expression tail:(space? "#" space? exp:text_append_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                Dhallish::Ast::ListConcatNode.new(tree, node.exp.to_node())
                        }
                end
        }
end

rule text_append_expression
        exp:record_merge_expression tail:(space? "++" space? exp:record_merge_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                Dhallish::Ast::BinaryArithOpNode.new([Dhallish::Types::Text], tree, node.exp.to_node(), "+") { |x, y| x + y }
                        }
                end
        }
end

rule record_merge_expression
        exp:application_expression tail:(space? op:("//\\\\" / "⩓" / "/\\" / "∧" / "//" / "⫽") space? exp:application_expression)*
        {
                def to_node()
                        tail.elements.reduce(exp.to_node()) { |tree, node|
                                if node.op.text_value == "//\\\\" or node.op.text_value == "⩓"
                                        Dhallish::Ast::RecordTypeRecursiveMergeNode.new tree, node.exp.to_node()
                                elsif node.op.text_value == "/\\" or node.op.text_value == "∧"
                                        Dhallish::Ast::RecordRecursiveMergeNode.new tree, node.exp.to_node()
                                else
                                        Dhallish::Ast::RecordNonRecursiveMergeNode.new tree, node.exp.to_node()
                                end
                        }
                end
        }
end

rule application_expression
        fn:proj_sel_expression tail:(space? exp:(!reserved arg:proj_sel_expression))*
        {
                def to_node()
                        tail.elements.reduce(fn.to_node()) { |tree, node|
                                Dhallish::Ast::FunctionCallNode.new(tree, node.exp.arg.to_node())
                        }
                end
        }
end

rule key_list
        "{" space? "}" { def to_list() [] end } /
        "{" space? head:label tail:(space? "," space? lbl:label)* space? "}"
        {
                def to_list()
                        tail.elements.reduce([head.text_value]) { |list, node|
                                list.push node.lbl.text_value
                                list
                        }
                end
        }
end

rule proj_sel_expression
        pe:primitive_expression tail:(space? "." space? sel:(a:label / b:key_list))*
        {
                def to_node()
                        tail.elements.reduce(pe.to_node()) { |tree, node|
                                if node.sel.respond_to? :a
                                        Dhallish::Ast::RecordUnionSelector.new tree, node.sel.a.text_value
                                else
                                        Dhallish::Ast::RecordProjection.new tree, node.sel.b.to_list()
                                end
                        }
                end
        }
end

rule primitive_expression
        exp:bool_literal                        { def to_node() exp.to_node() end } /
        exp:double_literal                      { def to_node() exp.to_node() end } /
        exp:natural_literal                     { def to_node() exp.to_node() end } /
        exp:integer_literal                     { def to_node() exp.to_node() end } /
        exp:text_literal                        { def to_node() exp.to_node() end } /
        exp:list_literal                        { def to_node() exp.to_node() end } /
        exp:function_definition                 { def to_node() exp.to_node() end } /
        exp:optional_literal                    { def to_node() exp.to_node() end } /
        exp:forall_literal                      { def to_node() exp.to_node() end } /
        exp:import_expression                   { def to_node() exp.to_node() end } /
        exp:union_merge                         { def to_node() exp.to_node() end } /
        exp:label                               { def to_node() exp.to_node() end } /
        exp:record_literal                      { def to_node() exp.to_node() end } /
        exp:record_type_literal                 { def to_node() exp.to_node() end } /
        exp:union_literal                       { def to_node() exp.to_node() end } /
        exp:union_type_literal                  { def to_node() exp.to_node() end } /
        "(" space? exp:expression space? ")"    { def to_node() exp.to_node() end } /
        "???" { def to_node() Dhallish::Ast::GetContext.new end }
end

## End of operator expressions

rule if_then_else_expression
        "if" space cond:expression space
        "then" space exp_true:expression space
        "else" space exp_false:expression
        {
                def to_node()
                        Dhallish::Ast::IfThenElseNode.new cond.to_node(), exp_true.to_node(), exp_false.to_node()
                end
        }
end

rule label
        ("_" / [a-zA-Z]) ("_" / "-" / "/" / [a-zA-Z0-9])* { def to_node() Dhallish::Ast::VariableNode.new text_value end } /
        "`" lb:(([a-zA-Z0-9] / "-" / "/" / "_" / ":" / "." / "$")+) "`" { def to_node() Dhallish::Ast::VariableNode.new lb.text_value end }
end

rule let_expression
        declarations:("let" space var:label space? annot:(":" space? exp:expression space?)? "=" space? val:expression space)+ "in" space in_expr:expression
        {
                def to_node()
                        vars = []
                        declarations.elements.each { |node|
                                typeannot = nil
                                if node.respond_to? :annot and node.annot.respond_to? :exp
                                        typeannot = node.annot.exp.to_node()
                                end
                                vars.append [node.var.text_value, typeannot, node.val.to_node()]
                        }
                        Dhallish::Ast::LetInNode.new vars, in_expr.to_node()
                end
        }
end

rule function_definition
        ("\\" / "λ") space? "(" space? lb:label space? ":" space? type_exp:expression space? ")" space? ("->" / "→") space? exp:expression
        {
                def to_node()
                        Dhallish::Ast::FunctionDefinitionNode.new lb.text_value, type_exp.to_node(), exp.to_node()
                end
        }
end

rule forbidden_label
        reserved / "Text" / "Bool" / "Optional" / "None" / "Some" / "True" / "False" / "Type" /
        "Natural"       ("/" ("show" / "toInteger" / "toDouble"))? /
        "Integer"       ("/" ("show" / "toNatural" / "toDouble"))? /
        "Double"        ("/" ("show" / "toNatural" / "toInteger"))? /
        "List"          ("/" ("length" / "reduce" / "head" / "tail"))? /
        "Optional"      ("/" ("fold"))?
end

rule reserved
        "if" / "then" / "else" / "let" / "in" / "as" / "?"
end

rule import_expression
        prefix:("../" / "./" / "~/" / "env:" / "http:" / "https:") src:import_source as_type:(space "as" space import_type:"Text")?
        {
                def to_node()
                        import_as_text = (as_type.respond_to? :import_type and as_type.import_type.text_value == "Text")
                        Dhallish::Ast::Import.new prefix.text_value, src.text_value, import_as_text
                end
        }
end

rule import_source
        # can be an environment variable, url or path
        (!([\s] / "\\" / "<" / ">" / "?" / "," / "[" / "]" / "{" / "}" / "#" / "(" / ")") .)*
end

rule union_merge
        "merge" space? handler:primitive_expression space? union:primitive_expression
        {
                def to_node()
                        Dhallish::Ast::UnionMerge.new(handler.to_node(), union.to_node())
                end
        }
end

end