Skip to content

Commit

Permalink
Improvements to Indexable trait
Browse files Browse the repository at this point in the history
- add small docstring (still missing docstrings for the methods)
- add required `size` method
- add default implementation of `each_with_index` and
  `reverse_each_with_index` methods based on `size` and `[]!` methods
- add `first!` and `last!` methods with default implementation
  based on `size` and `[]!` methods
- refactor tests to ensure that pure `Indexable` implementations are
  tested correctly (rather than testing the overrides implemented by `Array`)
- small compiler fix to ensure yield types of default implementations
  are traversed/resolved correctly
  • Loading branch information
jemc committed Aug 31, 2024
1 parent d7fb05d commit 1ee334c
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 58 deletions.
33 changes: 33 additions & 0 deletions core/Indexable.savi
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
:: An collection of elements of type A that can be accessed via numeric index.
:trait box Indexable(A)
:fun size USize
:fun "[]!"(index USize) (@->A)'aliased

//--
Expand All @@ -9,6 +11,16 @@
stride USize = 1
) None
:yields ((@->A)'aliased, USize)
index = from
to = to.at_most(@size)
stride = stride.at_least(1)
while index < to (
try (
value = @[index]!
yield (--value, index)
)
index = try (index +! stride | return)
)

:fun each(
from USize = 0
Expand Down Expand Up @@ -95,6 +107,15 @@
stride USize = 1
) None
:yields ((@->A)'aliased, USize)
index = from.at_most(try (@size -! 1 | return))
stride = stride.at_least(1)
while index >= to (
try (
value = @[index]!
yield (--value, index)
)
index = try (index -! stride | return)
)

:fun reverse_each(
from = USize.max_value
Expand Down Expand Up @@ -129,6 +150,18 @@

//--

:fun first! @->(A'aliased)
@[0]!

:fun last! @->(A'aliased)
@reverse_each_with_index -> (value, index |
return value
None // TODO: this should not be needed
)
error!

//--

:fun select(
from USize = 0
to = USize.max_value
Expand Down
117 changes: 59 additions & 58 deletions spec/core/Indexable.Spec.savi
Original file line number Diff line number Diff line change
@@ -1,121 +1,122 @@
// A little custom class to showcase the minimum trait implementation.
// All other Indexable methods will be based on this minimal implementation.
:class _ASCIILettersExample
:is Indexable(String)

:fun size USize: 26
:fun "[]!"(index USize)
error! if index >= @size
"\((index + 'a').format.printable_ascii)"

:class _ArrayWrapperExample(T val) // TODO: val constraint should not be required
:is Indexable(T)
:let array Array(T)
:new (@array)

:fun size: @array.size
:fun "[]!"(index USize): @array[index]!

:class Savi.Indexable.Spec
:is Spec
:const describes: "Indexable"

:it "yields each element"
array Array(String) = []
["foo", "bar", "baz"].each -> (string | array << string)
assert: array == ["foo", "bar", "baz"]
_ASCIILettersExample.new.each -> (string | array << string)
assert: array == [
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
]

:it "yields each element of a subslice"
array Array(String) = []
["a", "b", "c", "d", "e", "f"].each(1, 5) -> (string | array << string)
_ASCIILettersExample.new.each(1, 5) -> (string | array << string)
assert: array == ["b", "c", "d", "e"]

:it "yields each element along with the index"
array_a Array(String) = []
array_b Array(USize) = []
["foo", "bar", "baz"].each_with_index -> (string, index |
_ASCIILettersExample.new.each_with_index -> (string, index |
array_a << string
array_b << index
)
assert: array_a == ["foo", "bar", "baz"]
assert: array_b == [0, 1, 2]
assert: array_a == [
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
]
assert: array_b == [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25
]

:it "yields each element, in reverse"
array Array(String) = []
["foo", "bar", "baz"].reverse_each -> (string | array << string)
assert: array == ["baz", "bar", "foo"]
_ASCIILettersExample.new.reverse_each -> (string | array << string)
assert: array == [
"z", "y", "x", "w", "v", "u", "t", "s", "r", "q", "p", "o", "n"
"m", "l", "k", "j", "i", "h", "g", "f", "e", "d", "c", "b", "a"
]

:it "yields each element, in reverse, along with the index"
array_a Array(String) = []
array_b Array(USize) = []
["foo", "bar", "baz"].reverse_each_with_index -> (string, index |
_ASCIILettersExample.new.reverse_each_with_index -> (string, index |
array_a << string
array_b << index
)
assert: array_b == [2, 1, 0]
assert:
array_a == ["baz", "bar", "foo"]

:it "yields each element, stopping early if the criteria is met"
array Array(String) = []
early_stop = ["foo", "bar", "baz"].each_until -> (string |
array << string
string == "bar"
)
assert: early_stop
assert: array == ["foo", "bar"]

array.clear
early_stop = ["foo", "bar", "baz"].each_until -> (string |
array << string
string == "bard"
)
assert: early_stop.is_false
assert: array == ["foo", "bar", "baz"]

:it "yields each element of a subslice, stopping early if the criteria is met"
array Array(String) = []
early_stop = ["a", "b", "c", "d", "e", "f"].each_until(1, 5) -> (string |
array << string
string == "d"
)
assert: early_stop
assert: array == ["b", "c", "d"]

array.clear
early_stop = ["a", "b", "c", "d", "e", "f"].each_until(1, 5) -> (string |
array << string
string == "z"
)
assert: early_stop.is_false
assert: array == ["b", "c", "d", "e"]
assert: array_a == [
"z", "y", "x", "w", "v", "u", "t", "s", "r", "q", "p", "o", "n"
"m", "l", "k", "j", "i", "h", "g", "f", "e", "d", "c", "b", "a"
]
assert: array_b == [
25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13
12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
]

:it "returns True if any element meets the criteria"
array Array(U8) = [11, 22, 33, 44, 36, 27, 18]
array = _ArrayWrapperExample(U8).new([11, 22, 33, 44, 36, 27, 18])
assert: array.has_any -> (num | num > 30)
assert: array.has_any -> (num | num > 50).is_false

:it "returns True if all elements meet the criteria"
array Array(U8) = [11, 22, 33, 44, 36, 27, 18]
array = _ArrayWrapperExample(U8).new([11, 22, 33, 44, 36, 27, 18])
assert: array.has_all -> (num | num > 10)
assert: array.has_all -> (num | num > 30).is_false

:it "finds the first element that meets the criteria"
array Array(U8) = [11, 22, 33, 44, 36, 27, 18]
array = _ArrayWrapperExample(U8).new([11, 22, 33, 44, 36, 27, 18])
assert: array.find! -> (num | num > 30) == 33
assert error: array.find! -> (num | num > 50)

:it "finds the first index that meets the criteria"
array Array(U8) = [11, 22, 33, 44, 36, 27, 18]
array = _ArrayWrapperExample(U8).new([11, 22, 33, 44, 36, 27, 18])
assert: array.find_index! -> (num | num > 30) == 2
assert error: array.find_index! -> (num | num > 50)

:it "finds, starting from the end, the first element that meets the criteria"
array Array(U8) = [11, 22, 33, 44, 36, 27, 18]
array = _ArrayWrapperExample(U8).new([11, 22, 33, 44, 36, 27, 18])
assert: array.reverse_find! -> (num | num > 30) == 36
assert error: array.reverse_find! -> (num | num > 50)

:it "finds, starting from the end, the first index that meets the criteria"
array Array(U8) = [11, 22, 33, 44, 36, 27, 18]
array = _ArrayWrapperExample(U8).new([11, 22, 33, 44, 36, 27, 18])
assert: array.reverse_find_index! -> (num | num > 30) == 4
assert error: array.reverse_find_index! -> (num | num > 50)

:it "selects those elements that meet the criteria"
array Array(U8) = [11, 22, 33, 44, 36, 27, 18]
array = _ArrayWrapperExample(U8).new([11, 22, 33, 44, 36, 27, 18])
selected = array.select -> (num | num < 30)
assert: selected == [11, 22, 27, 18]

:it "rejects those elements that do not meet the criteria"
array Array(U8) = [1, 2, 3, 4, 5]
array = _ArrayWrapperExample(U8).new([1, 2, 3, 4, 5])
odds = array.reject -> (num | num % 2 == 0)
assert: odds == [1, 3, 5]

:it "rejects nothing from an empty array"
array Array(U8) = []
assert: array.reject -> (num | num % 2 == 0) == array
array = _ArrayWrapperExample(U8).new([])
assert: array.reject -> (num | num % 2 == 0) == []

:it "rejects nothing if criteria is always false"
array Array(U8) = [1, 2, 3]
assert: array.reject -> (num | False) == array
array = _ArrayWrapperExample(U8).new([1, 2, 3])
assert: array.reject -> (num | False) == [1, 2, 3]
6 changes: 6 additions & 0 deletions src/savi/compiler/populate.cr
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,17 @@ class Savi::Compiler::Populate
visitor = self
params = f.params.try(&.accept(ctx, visitor))
ret = f.ret.try(&.accept(ctx, visitor))
error_out = f.error_out.try(&.accept(ctx, visitor))
yield_out = f.yield_out.try(&.accept(ctx, visitor))
yield_in = f.yield_in.try(&.accept(ctx, visitor))
body = f.body.try(&.accept(ctx, visitor))

f = f.dup
f.params = params
f.ret = ret
f.error_out = error_out
f.yield_out = yield_out
f.yield_in = yield_in
f.body = body
f
}
Expand Down

0 comments on commit 1ee334c

Please sign in to comment.