From e1bef6ce2386a214fd62d7b1119cdd31e191e512 Mon Sep 17 00:00:00 2001
From: Philipp Panzer <philipp.panzer@fau.de>
Date: Tue, 5 Mar 2019 15:13:44 +0100
Subject: [PATCH] add unions

---
 README.md                   |  5 +++-
 lib/DhallishGrammar.treetop | 42 ++++++++++++++++++++++++++++
 lib/ast.rb                  | 56 +++++++++++++++++++++++++++++++++++++
 lib/types.rb                | 46 ++++++++++++++++++++++++++++--
 4 files changed, 146 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 2b788d0..97b4901 100644
--- a/README.md
+++ b/README.md
@@ -32,4 +32,7 @@ ruby ./tests/test.rb
 - [ ] disallow Type Names like `Natural` or `let` as labels
 - [ ] more tests and tests for expected failure
 - [ ] "n-1": currently interpreted as label, but should be a subtraction (sometimes)
-- [ ] Warum funktionieren Typannotationen in lets mit verschiedenen Typvariablen?
+- [ ] Bignums for Integers?
+- [ ] Multi Line strings
+- [ ] quoted labels
+- [ ] MORE UNICODE!
diff --git a/lib/DhallishGrammar.treetop b/lib/DhallishGrammar.treetop
index f88c58e..875427a 100644
--- a/lib/DhallishGrammar.treetop
+++ b/lib/DhallishGrammar.treetop
@@ -166,6 +166,46 @@ grammar DhallishGrammar
 		}
 	end
 
+	## Union Literals
+	rule union_literal_one_member
+		"<" space? label space? "=" space? expression ">"
+	end
+
+	rule union_literal
+		"<" space? start:(lb:label space? ":" space? type:expression space? "|" space?)* lb:label space? "=" space? expr:expression tail:(space? "|" space? lb:label space? ":" space? type:expression)* space? ">"
+		{
+			def to_node(ctx)
+				typed_labels = {}
+				start.elements.each { |node|
+					typed_labels[node.lb.text_value] = node.type.to_node(ctx)
+				}
+				tail.elements.each { |node|
+					typed_labels[node.lb.text_value] = node.type.to_node(ctx)
+				}
+				Dhallish::Ast::UnionLiteral.new(typed_labels, lb.text_value, expr.to_node(ctx))
+			end
+		}
+	end
+
+	rule empty_union_type_literal
+		"<" space? ">"
+	end
+
+	rule union_type_literal
+		empty_union_type_literal { def to_node(ctx) Dhallish::Ast::UnionType.new({})  end } /
+		"<" space? lb:label space? ":" space? type:expression tail:(space? "|" space? lb:label space? ":" space? type:expression)* space? ">"
+		{
+			def to_node(ctx)
+				type_map = {lb.text_value => type.to_node(ctx)}
+				tail.elements.each { |node|
+					type_map[node.lb.text_value] = node.type.to_node(ctx)
+				}
+				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
@@ -371,6 +411,8 @@ grammar DhallishGrammar
 		exp:label 				{ def to_node(ctx) exp.to_node(ctx) end } /
 		exp:record_literal                      { def to_node(ctx) exp.to_node(ctx) end } /
 		exp:record_type_literal                 { def to_node(ctx) exp.to_node(ctx) end } /
+		exp:union_literal			{ def to_node(ctx) exp.to_node(ctx) end } /
+		exp:union_type_literal 	                { def to_node(ctx) exp.to_node(ctx) end } /
 		"(" space? exp:expression space? ")"	{ def to_node(ctx) exp.to_node(ctx) end }
 	end
 
diff --git a/lib/ast.rb b/lib/ast.rb
index 744fce8..36a8c2e 100644
--- a/lib/ast.rb
+++ b/lib/ast.rb
@@ -656,6 +656,62 @@ module Dhallish
 			def evaluate(ctx) @buf end
 		end
 
+		class UnionLiteral
+			# map: label -> type, can be empty
+			attr_accessor :map
+			attr_accessor :init_label
+			attr_accessor :init_val
+			def initialize(map, init_label, init_val)
+				@map = map
+				@init_label = init_label
+				@init_val = init_val
+			end
+
+			def compute_type(ctx)
+				types = {}
+				@map.each { |key, node|
+					assert ("duplicate labels not allowed in union literals") { !types.include? key }
+					types[key] = node.compute_type(ctx)
+				}
+				assert ("duplicate labels not allowed in union literals") { !types.include? @init_label }
+				types[@init_label] = @init_val.compute_type(ctx)
+				Types::Union.new(types)
+			end
+
+			def evaluate(ctx)
+				res = {@init_label => @init_val.evaluate(ctx)}
+				res
+			end
+		end
+
+		class UnionType
+			# map: maps labels to ast-nodes that should be types
+			attr_accessor :map
+
+			def initialize(map)
+				@map = map
+			end
+
+			def compute_type(ctx)
+				types_map = {}
+				@map.each { |key, type|
+					assert ("Duplicate labels in Union Types are not allowed") { !types_map.include? key }
+					type_type = type.compute_type ctx
+					assert ("annotated type in union type not a type") { type_type.is_a? Types::Type }
+					types_map[key] = type_type.metadata
+				}
+				Types::Type.new(Types::Union.new types_map)
+			end
+
+			def evaluate(ctx)
+				res = {}
+				@map.each { |key, node|
+					res[key] = node.evaluate ctx
+				}
+				Types::Union.new(res)
+			end
+		end
+
 		class Import_Alternative
 			attr_accessor :lhs
 			attr_accessor :rhs
diff --git a/lib/types.rb b/lib/types.rb
index 0224f9e..2b2a86a 100644
--- a/lib/types.rb
+++ b/lib/types.rb
@@ -119,6 +119,29 @@ module Dhallish
 			def ==(otype) otype.is_a? Unresolved and otype.name == @name end
 			def to_s() "Unresolved(#{@name})" end
 		end
+		class Union
+			# types: label -> Type
+			attr_accessor :types
+			def initialize(types)
+				@types = types
+			end
+
+			def ==(otype) 
+				if !otype.is_a? Union
+					return false
+				end
+
+				@types.keys.reduce(true) { |isequal, key|
+					isequal and (otype.types.include? key and @types[key] == otype.types[key])
+				}
+			end
+
+			def to_s() 
+				"< #{@types.keys.map { |key|
+					"#{key}: #{@types[key].to_s}"
+				}.join(" | ")} >"
+			end
+		end
 
 		def resolve(orgtype, name, newtype)
 			case orgtype
@@ -148,6 +171,8 @@ module Dhallish
 				List.new(resolve(orgtype.type, name, newtype))
 			when Record
 				Record.new(orgtype.types.map{ |key, val| [key, resolve(val, name, newtype)] }.to_h)
+			when Union
+				Union.new(orgtype.types.map{ |key, val| [key, resolve(val, name, newtype)] }.to_h)
 			else
 				orgtype
 			end
@@ -214,6 +239,19 @@ module Dhallish
 				else
 					nil
 				end
+			when Union
+				if b.is_a? Union and a.types.keys.length == b.types.keys.length
+					for key in a.types.keys
+						aelm = a.types[key]
+						belm = b.types[key]
+						if belm.nil?; return nil end
+						ret = unification(aelm, belm, mapping)
+						if ret.nil?; return nil end
+					end
+					mapping
+				else
+					nil
+				end
 			else
 				if a == b
 					mapping
@@ -228,7 +266,7 @@ module Dhallish
 			if x.is_a? Symbol
 				x != Natural and x != Integer and x != Double and x != Bool and x != Text
 			else
-				!x.is_a? Optional and !x.is_a? List and !x.is_a? Record and !x.is_a? Function and !x.is_a? Unresolved and !x.is_a? Type
+				!x.is_a? Optional and !x.is_a? List and !x.is_a? Record and !x.is_a? Function and !x.is_a? Unresolved and !x.is_a? Type and !x.is_a? Union
 			end
 		end
 		module_function :not_a_type?
@@ -260,7 +298,11 @@ module Dhallish
 		when nil
 			"null"
 		else
-			"\"Dhallish-Internal: #{escape_str(dhallval.inspect)}\""
+			if Types::is_a_type? dhallval
+				"\"#{dhallval.to_s}\""
+			else
+				"\"Dhallish-Internal: #{escape_str(dhallval.inspect)}\""
+			end
 		end
 	end
 	module_function :to_json
-- 
GitLab