Skip to content

Commit

Permalink
Add indexing support to maps
Browse files Browse the repository at this point in the history
  • Loading branch information
irh committed Jun 17, 2024
1 parent 314cda6 commit b8f885d
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The Koto project adheres to
- Thanks to [@Tarbetu](https://github.com/Tarbetu) for the contributions.
- `export` can be used with multi-assignment expressions.
- e.g. expressions like `export a, b, c = foo()` are now allowed.
- Maps now support `[]` indexing, returning the Nth entry as a tuple.

#### Core Library

Expand Down
24 changes: 18 additions & 6 deletions crates/cli/docs/language_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ print! (1, 2, 3), (4, 5, 6)
check! ((1, 2, 3), (4, 5, 6))
```

Access tuple elements by index using square brackets, starting from `0`.
You can access tuple elements by index using square brackets, starting from `0`.

```koto
print! x = false, 10
Expand Down Expand Up @@ -628,6 +628,21 @@ print! a.foo
check! Hi!
```

### Entry Order

A map's entries are maintained in a consistent order,
representing the sequence in which its entries were added.

You can access map entries by index using square brackets, starting from `0`.

The entry is returned as a tuple containing the key and its associated value.

```koto
m = {apples: 42, oranges: 99, lemons: 63}
print! m[1]
check! ('oranges', 99)
```

### Shorthand Values

Koto supports a shorthand notation when creating maps with inline syntax.
Expand Down Expand Up @@ -707,16 +722,13 @@ print! m.'true'
check! 42
print! m.key99
check! 99
```

### Map Key Types

Although map keys are typically strings, other value types can be used
as keys by using the [map.insert][map-insert] and [map.get][map-get] functions.

Map keys are typically strings, but any [_immutable_][immutable] value can be
used as a map key.
used as a map key by using the [map.insert][map-insert] and [map.get][map-get]
functions.

The immutable value types in Koto are [strings](#strings), [numbers](#numbers),
[booleans](#booleans), [ranges](#ranges), and [`null`](#null).
Expand Down
2 changes: 1 addition & 1 deletion crates/runtime/src/types/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ impl KValue {
pub fn is_indexable(&self) -> bool {
use KValue::*;
match self {
List(_) | Tuple(_) | Str(_) => true,
List(_) | Map(_) | Str(_) | Tuple(_) => true,
Object(o) => o.try_borrow().map_or(false, |o| o.size().is_some()),
_ => false,
}
Expand Down
76 changes: 35 additions & 41 deletions crates/runtime/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,6 @@ use std::{
};
use unicode_segmentation::UnicodeSegmentation;

macro_rules! call_binary_op_or_else {
($vm:expr,
$result_register:expr,
$lhs_register:expr,
$rhs_value: expr,
$overridden_value:expr,
$op:tt,
$else:tt) => {{
let maybe_op = $overridden_value.get_meta_value(&MetaKey::BinaryOp($op));
if let Some(op) = maybe_op {
let rhs_value = $rhs_value.clone();
return $vm.call_overridden_binary_op($result_register, $lhs_register, rhs_value, op);
} else {
$else
}
}};
}

#[derive(Clone)]
pub enum ControlFlow {
Continue,
Expand Down Expand Up @@ -2272,29 +2254,32 @@ impl KotoVm {
value_register: u8,
index_register: u8,
) -> Result<()> {
use BinaryOp::Index;
use KValue::*;

let value = self.clone_register(value_register);
let index = self.clone_register(index_register);
let index_op = BinaryOp::Index.into();

match (&value, index) {
let result = match (&value, index) {
(List(l), Number(n)) => {
let index = self.validate_index(n, Some(l.len()))?;
self.set_register(result_register, l.data()[index].clone());
l.data()[index].clone()
}
(List(l), Range(range)) => {
let indices = range.indices(l.len());
List(KList::from_slice(&l.data()[indices]))
}
(List(l), Range(range)) => self.set_register(
result_register,
List(KList::from_slice(&l.data()[range.indices(l.len())])),
),
(Tuple(t), Number(n)) => {
let index = self.validate_index(n, Some(t.len()))?;
self.set_register(result_register, t[index].clone());
t[index].clone()
}
(Tuple(t), Range(range)) => {
// Safety: The tuple's length is passed into range.indices, so the range is valid
let result = t.make_sub_tuple(range.indices(t.len())).unwrap();
self.set_register(result_register, Tuple(result))
let indices = range.indices(t.len());
let Some(result) = t.make_sub_tuple(indices) else {
// range.indices is guaranteed to return valid indices for the tuple
unreachable!();
};
Tuple(result)
}
(Str(s), Number(n)) => {
let index = self.validate_index(n, Some(s.len()))?;
Expand All @@ -2303,25 +2288,32 @@ impl KotoVm {
"indexing with ({index}) would result in invalid UTF-8 data"
);
};
self.set_register(result_register, Str(result));
Str(result)
}
(Str(s), Range(range)) => {
let Some(result) = s.with_bounds(range.indices(s.len())) else {
let indices = range.indices(s.len());
let Some(result) = s.with_bounds(indices) else {
return runtime_error!(
"indexing with ({range}) would result in invalid UTF-8 data"
);
};
self.set_register(result_register, Str(result));
}
(Map(m), index) => {
call_binary_op_or_else!(self, result_register, value_register, index, m, Index, {
return runtime_error!("Unable to index {}", value.type_as_string());
});
}
(Object(o), index) => {
let result = o.try_borrow()?.index(&index)?;
self.set_register(result_register, result);
Str(result)
}
(Map(m), index) if m.contains_meta_key(&index_op) => {
let op = m.get_meta_value(&index_op).unwrap();
return self.call_overridden_binary_op(result_register, value_register, index, op);
}
(Map(m), Number(n)) => {
let entries = m.data();
let index = self.validate_index(n, Some(entries.len()))?;
let Some((key, value)) = entries.get_index(index) else {
// The index has just been validated
unreachable!();
};
let result = KTuple::from(vec![key.value().clone(), value.clone()]);
Tuple(result)
}
(Object(o), index) => o.try_borrow()?.index(&index)?,
(unexpected_value, unexpected_index) => {
return runtime_error!(
"Unable to index '{}' with '{}'",
Expand All @@ -2331,6 +2323,8 @@ impl KotoVm {
}
};

self.set_register(result_register, result);

Ok(())
}

Expand Down
11 changes: 10 additions & 1 deletion crates/runtime/tests/vm_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ true
#[test]
fn indexable_matches_indexable_values() {
let script = "
let x: Indexable, y: Indexable, z: Indexable = (1, 2, 3), [], 'foo'
let w: Indexable, x: Indexable, y: Indexable, z: Indexable = {}, (1, 2, 3), [], 'foo'
true
";
check_script_output(script, true);
Expand Down Expand Up @@ -2185,6 +2185,15 @@ c.foo + c.bar
";
check_script_output(script, 141);
}

#[test]
fn indexing() {
let script = "
m = {foo: 42, bar: 'xyz'}
m[0]
";
check_script_output(script, tuple(&["foo".into(), 42.into()]));
}
}

mod chains {
Expand Down
14 changes: 7 additions & 7 deletions koto/tests/enums.koto
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ make_enum = |entries...|
make_bidirectional_enum = |entries...|
entries
.enumerate()
.fold {}, |enum, (index, id)|
enum.insert id, index
enum.insert index, id
enum
.fold {}, |result, (index, id)|
result.insert id, index
result.insert index, id
result

@tests =
@test make_enum: ||
enum = make_enum "foo", "bar", "baz"
assert_eq enum.foo, 0
assert_eq enum.bar, 1
assert_eq enum.baz, 2
assert_eq enum.get_index(0)[0], "foo"
assert_eq enum.get_index(1)[0], "bar"
assert_eq enum.get_index(2)[0], "baz"
assert_eq enum[0][0], "foo"
assert_eq enum[1][0], "bar"
assert_eq enum[2][0], "baz"

@test make_bidirectional_enum: ||
enum = make_bidirectional_enum "foo", "bar", "baz"
Expand Down
2 changes: 1 addition & 1 deletion koto/tests/primes.koto
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
get_primes = |n|
get_primes = |n: Number| -> Iterator
if n <= 1
return

Expand Down

0 comments on commit b8f885d

Please sign in to comment.