From 910a4cd6eeebb6c15abdafeb6c51a567e537dc1a Mon Sep 17 00:00:00 2001
From: Ary Borenszweig <aborenszweig@manas.com.ar>
Date: Tue, 14 Oct 2014 11:59:36 -0300
Subject: [PATCH] Named arguments in macro invocations

---
 spec/compiler/codegen/macro_spec.cr        | 10 ++++++++++
 spec/compiler/type_inference/macro_spec.cr | 22 ++++++++++++++++++++++
 src/compiler/crystal/macros/macros.cr      |  5 +++++
 src/compiler/crystal/semantic/call.cr      | 13 ++++++++++++-
 src/compiler/crystal/syntax/ast.cr         | 20 ++++++++++++++++++--
 src/compiler/crystal/types.cr              | 12 ++++++------
 6 files changed, 73 insertions(+), 9 deletions(-)

diff --git a/spec/compiler/codegen/macro_spec.cr b/spec/compiler/codegen/macro_spec.cr
index b66010fc82..1bf2af79e9 100755
--- a/spec/compiler/codegen/macro_spec.cr
+++ b/spec/compiler/codegen/macro_spec.cr
@@ -578,4 +578,14 @@ describe "Code gen: macro" do
       end
       )).to_i.should eq(2)
   end
+
+  it "executes with named arguments" do
+    run(%(
+      macro foo(x = 1)
+        {{x}} + 1
+      end
+
+      foo x: 2
+      )).to_i.should eq(3)
+  end
 end
diff --git a/spec/compiler/type_inference/macro_spec.cr b/spec/compiler/type_inference/macro_spec.cr
index 5913a915f9..77ad591bab 100755
--- a/spec/compiler/type_inference/macro_spec.cr
+++ b/spec/compiler/type_inference/macro_spec.cr
@@ -202,4 +202,26 @@ describe "Type inference: macro" do
       ->(x : Foo) { x.foo; x.ivars_length }
       )) { fun_of(types["Foo"], no_return) }
   end
+
+  it "errors if non-existent named arg" do
+    assert_error %(
+      macro foo(x = 1)
+        {{x}} + 1
+      end
+
+      foo y: 2
+      ),
+      "no argument named 'y'"
+  end
+
+  it "errors if named arg already specified" do
+    assert_error %(
+      macro foo(x = 1)
+        {{x}} + 1
+      end
+
+      foo 2, x: 2
+      ),
+      "argument 'x' already specified"
+  end
 end
diff --git a/src/compiler/crystal/macros/macros.cr b/src/compiler/crystal/macros/macros.cr
index d0be3d5ffc..24ee8fef5d 100644
--- a/src/compiler/crystal/macros/macros.cr
+++ b/src/compiler/crystal/macros/macros.cr
@@ -165,6 +165,11 @@ module Crystal
           vars[macro_arg.name] = call_arg.to_macro_var
         end
 
+        # The named arguments
+        call.named_args.try &.each do |named_arg|
+          vars[named_arg.name] = named_arg.value
+        end
+
         # The block arg
         call_block = call.block
         macro_block_arg = a_macro.block_arg
diff --git a/src/compiler/crystal/semantic/call.cr b/src/compiler/crystal/semantic/call.cr
index af586b2d9e..1cdd152902 100644
--- a/src/compiler/crystal/semantic/call.cr
+++ b/src/compiler/crystal/semantic/call.cr
@@ -492,7 +492,7 @@ module Crystal
     end
 
     def lookup_macro
-      in_macro_target &.lookup_macro(name, args.length)
+      in_macro_target &.lookup_macro(name, args.length, named_args)
     end
 
     def in_macro_target
@@ -1016,6 +1016,17 @@ module Crystal
       if macros
         all_arguments_lengths = Set(Int32).new
         macros.each do |macro|
+          named_args.try &.each do |named_arg|
+            index = macro.args.index { |arg| arg.name == named_arg.name }
+            if index
+              if index < args.length
+                raise "argument '#{named_arg.name}' already specified"
+              end
+            else
+              raise "no argument named '#{named_arg.name}'"
+            end
+          end
+
           min_length = macro.args.index(&.default_value) || macro.args.length
           min_length.upto(macro.args.length) do |args_length|
             all_arguments_lengths << args_length
diff --git a/src/compiler/crystal/syntax/ast.cr b/src/compiler/crystal/syntax/ast.cr
index 7c19941a7a..8ac517cdd8 100644
--- a/src/compiler/crystal/syntax/ast.cr
+++ b/src/compiler/crystal/syntax/ast.cr
@@ -894,7 +894,7 @@ module Crystal
       name.length
     end
 
-    def matches_args_length?(args_length)
+    def matches?(args_length, named_args)
       my_args_length = args.length
       min_args_length = args.index(&.default_value) || my_args_length
       max_args_length = my_args_length
@@ -902,7 +902,23 @@ module Crystal
         min_args_length -= 1
         max_args_length = Int32::MAX
       end
-      min_args_length <= args_length <= max_args_length
+
+      unless min_args_length <= args_length <= max_args_length
+        return false
+      end
+
+      named_args.try &.each do |named_arg|
+        index = args.index { |arg| arg.name == named_arg.name }
+        if index
+          if index < args_length
+            return false
+          end
+        else
+          return false
+        end
+      end
+
+      true
     end
 
     def clone_without_location
diff --git a/src/compiler/crystal/types.cr b/src/compiler/crystal/types.cr
index 5ac0a6ebac..700a462cc8 100644
--- a/src/compiler/crystal/types.cr
+++ b/src/compiler/crystal/types.cr
@@ -289,7 +289,7 @@ module Crystal
       raise "Bug: #{self} doesn't implement add_macro"
     end
 
-    def lookup_macro(name, args_length)
+    def lookup_macro(name, args_length, named_args)
       raise "Bug: #{self} doesn't implement lookup_macro"
     end
 
@@ -303,7 +303,7 @@ module Crystal
 
     def lookup_method_missing
       # method_missing is actually stored in the metaclass
-      method_missing = metaclass.lookup_macro("method_missing", 3)
+      method_missing = metaclass.lookup_macro("method_missing", 3, nil)
       return method_missing if method_missing
 
       parents.try &.each do |parent|
@@ -697,14 +697,14 @@ module Crystal
       nil
     end
 
-    def lookup_macro(name, args_length)
+    def lookup_macro(name, args_length, named_args)
       if (macros = self.macros) && (array = macros[name]?)
-        match = array.find &.matches_args_length?(args_length)
+        match = array.find &.matches?(args_length, named_args)
         return match if match
       end
 
       parents.try &.each do |parent|
-        parent_macro = parent.lookup_macro(name, args_length)
+        parent_macro = parent.lookup_macro(name, args_length, named_args)
         return parent_macro if parent_macro
       end
 
@@ -2890,7 +2890,7 @@ module Crystal
       true
     end
 
-    def lookup_macro(name, args_length)
+    def lookup_macro(name, args_length, named_args)
       nil
     end
 
-- 
GitLab