Skip to content

Commit

Permalink
Allow incomplete range arguments for #[](Range) macro methods (#11380)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored Nov 15, 2021
1 parent b06c7b2 commit 60bcac5
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 57 deletions.
47 changes: 41 additions & 6 deletions spec/compiler/macro/macro_methods_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/crystal/macros.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 1 addition & 18 deletions src/compiler/crystal/macros/interpreter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 43 additions & 33 deletions src/compiler/crystal/macros/methods.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 60bcac5

Please sign in to comment.