From 60bcac5206dc9d794399960194c4768e6fea7cb6 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Tue, 16 Nov 2021 06:54:11 +0800 Subject: [PATCH] Allow incomplete range arguments for `#[](Range)` macro methods (#11380) --- spec/compiler/macro/macro_methods_spec.cr | 47 +++++++++++-- src/compiler/crystal/macros.cr | 4 ++ src/compiler/crystal/macros/interpreter.cr | 19 +----- src/compiler/crystal/macros/methods.cr | 76 ++++++++++++---------- 4 files changed, 89 insertions(+), 57 deletions(-) diff --git a/spec/compiler/macro/macro_methods_spec.cr b/spec/compiler/macro/macro_methods_spec.cr index 95c24d56c07e..411ab5d02e20 100644 --- a/spec/compiler/macro/macro_methods_spec.cr +++ b/spec/compiler/macro/macro_methods_spec.cr @@ -344,18 +344,27 @@ module Crystal assert_macro %({{"hello".empty?}}), "false" end - it "executes string [Range] inclusive" do + it "executes [] with inclusive range" do assert_macro %({{"hello"[1..-2]}}), %("ell") end - it "executes string [Range] exclusive" do + it "executes [] with exclusive range" do assert_macro %({{"hello"[1...-2]}}), %("el") end - it "executes string [Range] inclusive (computed)" do + it "executes [] with computed range" do assert_macro %({{"hello"[[1].size..-2]}}), %("ell") end + it "executes [] with incomplete range" do + assert_macro %({{"hello"[1..]}}), %("ello") + assert_macro %({{"hello"[1..nil]}}), %("ello") + assert_macro %({{"hello"[...3]}}), %("hel") + assert_macro %({{"hello"[nil...3]}}), %("hel") + assert_macro %({{"hello"[..]}}), %("hello") + assert_macro %({{"hello"[nil..nil]}}), %("hello") + end + it "executes string chomp" do assert_macro %({{"hello\n".chomp}}), %("hello") end @@ -817,6 +826,15 @@ module Crystal assert_macro %({{ [1, 2, 3, 4][[1].size...-1] }}), %([2, 3]) end + it "executes [] with incomplete range" do + assert_macro %({{ [1, 2, 3, 4][1..] }}), %([2, 3, 4]) + assert_macro %({{ [1, 2, 3, 4][1..nil] }}), %([2, 3, 4]) + assert_macro %({{ [1, 2, 3, 4][...2] }}), %([1, 2]) + assert_macro %({{ [1, 2, 3, 4][nil...2] }}), %([1, 2]) + assert_macro %({{ [1, 2, 3, 4][..] }}), %([1, 2, 3, 4]) + assert_macro %({{ [1, 2, 3, 4][nil..nil] }}), %([1, 2, 3, 4]) + end + it "executes [] with two numbers" do assert_macro %({{ [1, 2, 3, 4, 5][1, 3] }}), %([2, 3, 4]) end @@ -1076,18 +1094,35 @@ module Crystal end describe TupleLiteral do - it "executes index 0" do + it "executes [] with 0" do assert_macro %({{ {1, 2, 3}[0] }}), "1" end - it "executes index 1" do + it "executes [] with 1" do assert_macro %({{ {1, 2, 3}[1] }}), "2" end - it "executes index out of bounds" do + it "executes [] out of bounds" do assert_macro %({{ {1, 2, 3}[3] }}), "nil" end + it "executes [] with range" do + assert_macro %({{ {1, 2, 3, 4}[1...-1] }}), %({2, 3}) + end + + it "executes [] with computed range" do + assert_macro %({{ {1, 2, 3, 4}[[1].size...-1] }}), %({2, 3}) + end + + it "executes [] with incomplete range" do + assert_macro %({{ {1, 2, 3, 4}[1..] }}), %({2, 3, 4}) + assert_macro %({{ {1, 2, 3, 4}[1..nil] }}), %({2, 3, 4}) + assert_macro %({{ {1, 2, 3, 4}[...2] }}), %({1, 2}) + assert_macro %({{ {1, 2, 3, 4}[nil...2] }}), %({1, 2}) + assert_macro %({{ {1, 2, 3, 4}[..] }}), %({1, 2, 3, 4}) + assert_macro %({{ {1, 2, 3, 4}[nil..nil] }}), %({1, 2, 3, 4}) + end + it "executes size" do assert_macro %({{ {1, 2, 3}.size }}), "3" end diff --git a/src/compiler/crystal/macros.cr b/src/compiler/crystal/macros.cr index 99bed1e94a01..11de3a3cf4e2 100644 --- a/src/compiler/crystal/macros.cr +++ b/src/compiler/crystal/macros.cr @@ -649,6 +649,10 @@ module Crystal::Macros def [](index : NumberLiteral) : ASTNode end + # Similar to `Array#[]`. + def [](index : RangeLiteral) : ArrayLiteral(ASTNode) + end + # Similar to `Array#[]=`. def []=(index : NumberLiteral, value : ASTNode) : ASTNode end diff --git a/src/compiler/crystal/macros/interpreter.cr b/src/compiler/crystal/macros/interpreter.cr index 223ffe71fd0b..b6f3a07de570 100644 --- a/src/compiler/crystal/macros/interpreter.cr +++ b/src/compiler/crystal/macros/interpreter.cr @@ -197,28 +197,11 @@ module Crystal {MacroId.new(entry.key), entry.value} end when RangeLiteral - exp.from.accept self - from = @last - - unless from.is_a?(NumberLiteral) - node.raise "range begin #{exp.from} must evaluate to a NumberLiteral" - end - - from = from.to_number.to_i - - exp.to.accept self - to = @last - - unless to.is_a?(NumberLiteral) - node.raise "range end #{exp.to} must evaluate to a NumberLiteral" - end - - to = to.to_number.to_i + range = exp.interpret_to_range(self) element_var = node.vars[0] index_var = node.vars[1]? - range = Range.new(from, to, exp.exclusive?) range.each_with_index do |element, index| @vars[element_var.name] = NumberLiteral.new(element) if index_var diff --git a/src/compiler/crystal/macros/methods.cr b/src/compiler/crystal/macros/methods.cr index 75535a0b8b1c..561a414772b4 100644 --- a/src/compiler/crystal/macros/methods.cr +++ b/src/compiler/crystal/macros/methods.cr @@ -568,20 +568,7 @@ module Crystal interpret_check_args do |arg| case arg when RangeLiteral - from, to = arg.from, arg.to - from = interpreter.accept(from) - to = interpreter.accept(to) - - unless from.is_a?(NumberLiteral) - raise "range from in StringLiteral#[] must be a number, not #{from.class_desc}: #{from}" - end - - unless to.is_a?(NumberLiteral) - raise "range to in StringLiteral#[] must be a number, not #{to.class_desc}: #{from}" - end - - from, to = from.to_number.to_i, to = to.to_number.to_i - range = Range.new(from, to, arg.exclusive?) + range = arg.interpret_to_nilable_range(interpreter) StringLiteral.new(@value[range]) else raise "wrong argument for StringLiteral#[] (#{arg.class_desc}): #{arg}" @@ -1068,24 +1055,47 @@ module Crystal end def interpret_to_range(interpreter) - from = self.from - to = self.to - - from = interpreter.accept(from) - to = interpreter.accept(to) - - unless from.is_a?(NumberLiteral) - raise "range begin must be a NumberLiteral, not #{from.class_desc}" - end - - unless to.is_a?(NumberLiteral) - raise "range end must be a NumberLiteral, not #{to.class_desc}" - end - - from = from.to_number.to_i - to = to.to_number.to_i - - self.exclusive? ? (from...to) : (from..to) + node = interpreter.accept(self.from) + from = case node + when NumberLiteral + node.to_number.to_i + else + raise "range begin must be a NumberLiteral, not #{node.class_desc}" + end + + node = interpreter.accept(self.to) + to = case node + when NumberLiteral + node.to_number.to_i + else + raise "range end must be a NumberLiteral, not #{node.class_desc}" + end + + Range.new(from, to, self.exclusive?) + end + + def interpret_to_nilable_range(interpreter) + node = interpreter.accept(self.from) + from = case node + when NumberLiteral + node.to_number.to_i + when NilLiteral, Nop + nil + else + raise "range begin must be a NumberLiteral | NilLiteral | Nop, not #{node.class_desc}" + end + + node = interpreter.accept(self.to) + to = case node + when NumberLiteral + node.to_number.to_i + when NilLiteral, Nop + nil + else + raise "range end must be a NumberLiteral | NilLiteral | Nop, not #{node.class_desc}" + end + + Range.new(from, to, self.exclusive?) end end @@ -2434,7 +2444,7 @@ private def interpret_array_or_tuple_method(object, klass, method, args, named_a index = arg.to_number.to_i value = object.elements[index]? || Crystal::NilLiteral.new when Crystal::RangeLiteral - range = arg.interpret_to_range(interpreter) + range = arg.interpret_to_nilable_range(interpreter) begin klass.new(object.elements[range]) rescue ex