Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an @index_mut metakey #367

Merged
merged 3 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ The Koto project adheres to

#### Language

- Type hints with runtime type checks have been added ([#298](https://github.com/koto-lang/koto/issues/298)).
- Type hints have been added, enabling runtime type checks.
- `let` is used for type-checked assignments, e.g. `let x: String = 'abc'`.
- Other bindings (`for` values, function arguments, etc.) can be similarly
annotated with a type hints.
- Runtime checks can be optionally disabled via
`KotoSettings::enable_type_checks`.
- Thanks to [@Tarbetu](https://github.com/Tarbetu) for the contributions.
- Optional chaining via the `?` operator has been added to simplify writing
expressions that need to check for `.null` on intermediate values.
Expand All @@ -21,6 +26,8 @@ The Koto project adheres to
- E.g. expressions like `export a, b, c = foo()` are now allowed.
- Maps now support `[]` indexing, returning the Nth entry as a tuple.
- Entries can also be replaced by index by assigning a key/value tuple.
- The `@index_mut` metakey has been added to define custom behaviour for
index-assignment operations.
- Objects that implement `KotoObject::call` can now be used in operations that
expect functions.
- `KotoObject::is_callable` has been added to support this, and needs to be
Expand All @@ -30,7 +37,8 @@ The Koto project adheres to

- `file.is_terminal` has been added.
- `string.repeat` has been added.
- `tuple.sort_copy` now supports sorting with a key function, like `list.sort`.
- `tuple.sort_copy` now supports sorting with a key function, following the
behaviour of `list.sort`.

#### API

Expand All @@ -54,7 +62,8 @@ The Koto project adheres to
between `Int` and `Float`.
- Error messages have been improved when calling core library functions with
incorrect arguments.
- `await`, `const`, and `let` have been reserved as keywords for future use.
- `await` and `const` have been reserved as keywords for future use.
- `@||` has been renamed to `@call`, and `@[]` has been renamed to `@index`.

#### API

Expand Down
57 changes: 42 additions & 15 deletions crates/cli/docs/language_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1856,52 +1856,79 @@ print! x.data
check! -100
```

#### `@size` and `@[]`
#### `@size` and `@index`

The `@size` metakey defines how the object should report its size,
while the `@[]` metakey defines what values should be returned when indexing is
while the `@index` metakey defines what values should be returned when indexing is
performed on the object.

If `@size` is implemented, then `@[]` should also be implemented.
If `@size` is implemented, then `@index` should also be implemented.

The `@[]` implementation can support indexing by any input values that make
The `@index` implementation can support indexing by any input values that make
sense for your object type, but for argument unpacking to work correctly the
runtime expects that indexing by both single indices and ranges should be
supported.
runtime expects that indexing should be supported for both single indices and also ranges.

```koto
foo = |data|
data: data
@size: || size self.data
@[]: |index| self.data[index]
@index: |index| self.data[index]

x = foo (100, 200, 300)
x = foo ('a', 'b', 'c')
print! size x
check! 3
print! x[1]
check! 200
check! b
```

# Unpack the first two elements in the argument and multiply them
Implementing `@size` and `@index` allows an object to participate in argument unpacking.

```koto
foo = |data|
data: data
@size: || size self.data
@index: |index| self.data[index]

x = foo (10, 20, 30, 40, 50)

# Unpack the first two elements in the value passed to the function and multiply them
multiply_first_two = |(a, b, ...)| a * b
print! multiply_first_two x
check! 20000
check! 200

# Inspect the first element in the object
print! match x
(first, others...) then 'first: {first}, remaining: {size others}'
check! first: 100, remaining: 2
check! first: 10, remaining: 4
```

#### `@index_mut`

The `@index_mut` metakey defines how the object should behave when index-assignment is used.

The given value should be a function that takes an index as the first argument, and the value to be assigned as the second argument.

```koto
foo = |data|
data: data
@index: |index| self.data[index]
@index_mut: |index, value| self.data[index] = value

x = foo ['a', 'b', 'c']
x[1] = 'hello'
print! x[1]
check! hello
```

#### `@||`
#### `@call`

The `@||` metakey defines how the object should behave when its called as a
The `@call` metakey defines how the object should behave when its called as a
function.

```koto
foo = |n|
data: n
@||: ||
@call: ||
self.data *= 2
self.data

Expand Down
12 changes: 8 additions & 4 deletions crates/parser/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,11 @@ pub enum MetaKeyId {
Equal,
/// @!=
NotEqual,
/// @[]

/// @index
Index,
/// @index_mut
IndexMut,

/// @display
Display,
Expand All @@ -560,7 +563,7 @@ pub enum MetaKeyId {
/// @base
Base,

/// @||
/// @call
Call,

/// @test test_name
Expand Down Expand Up @@ -620,7 +623,8 @@ impl fmt::Display for MetaKeyId {
GreaterOrEqual => ">=",
Equal => "==",
NotEqual => "!=",
Index => "[]",
Index => "index",
IndexMut => "index_mut",
Display => "display",
Iterator => "iterator",
Next => "next",
Expand All @@ -629,7 +633,7 @@ impl fmt::Display for MetaKeyId {
Size => "size",
Type => "type",
Base => "base",
Call => "||",
Call => "call",
Test => "test",
PreTest => "pre_test",
PostTest => "post_test",
Expand Down
11 changes: 3 additions & 8 deletions crates/parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2025,7 +2025,10 @@ impl<'source> Parser<'source> {
Some(Token::Equal) => MetaKeyId::Equal,
Some(Token::NotEqual) => MetaKeyId::NotEqual,
Some(Token::Id) => match self.current_token.slice(self.source) {
"call" => MetaKeyId::Call,
"display" => MetaKeyId::Display,
"index" => MetaKeyId::Index,
"index_mut" => MetaKeyId::IndexMut,
"iterator" => MetaKeyId::Iterator,
"next" => MetaKeyId::Next,
"next_back" => MetaKeyId::NextBack,
Expand Down Expand Up @@ -2054,14 +2057,6 @@ impl<'source> Parser<'source> {
},
_ => return self.error(SyntaxError::UnexpectedMetaKey),
},
Some(Token::SquareOpen) => match self.consume_token() {
Some(Token::SquareClose) => MetaKeyId::Index,
_ => return self.error(SyntaxError::UnexpectedMetaKey),
},
Some(Token::Function) => match self.consume_token() {
Some(Token::Function) => MetaKeyId::Call,
_ => return self.error(SyntaxError::UnexpectedMetaKey),
},
_ => return self.error(SyntaxError::UnexpectedMetaKey),
};

Expand Down
11 changes: 8 additions & 3 deletions crates/runtime/src/types/meta_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ pub enum MetaKey {
///
/// e.g. `@not`
UnaryOp(UnaryOp),
/// Function call - `@||`
/// Function call - `@call`
///
/// Defines the behaviour when performing a function call on the value.
/// Defines the behaviour when performing a function call on the object.
Call,
/// `@index_mut`
///
/// Defines how an object should behave in mutable indexing operations.
IndexMut,
/// A named key
///
/// e.g. `@meta my_named_key`
Expand Down Expand Up @@ -152,7 +156,7 @@ pub enum BinaryOp {
Equal,
/// `@!=`
NotEqual,
/// `@[]`
/// `@index`
Index,
}

Expand Down Expand Up @@ -228,6 +232,7 @@ pub fn meta_id_to_key(id: MetaKeyId, name: Option<KString>) -> Result<MetaKey> {
MetaKeyId::Equal => MetaKey::BinaryOp(Equal),
MetaKeyId::NotEqual => MetaKey::BinaryOp(NotEqual),
MetaKeyId::Index => MetaKey::BinaryOp(Index),
MetaKeyId::IndexMut => MetaKey::IndexMut,
MetaKeyId::Iterator => MetaKey::UnaryOp(Iterator),
MetaKeyId::Next => MetaKey::UnaryOp(Next),
MetaKeyId::NextBack => MetaKey::UnaryOp(NextBack),
Expand Down
30 changes: 27 additions & 3 deletions crates/runtime/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2248,6 +2248,31 @@ impl KotoVm {
}
Ok(())
}
Map(map) if map.contains_meta_key(&MetaKey::IndexMut) => {
let index_mut_fn = map.get_meta_value(&MetaKey::IndexMut).unwrap();
let index_value = index_value.clone();
let value = value.clone();

// Set up the function call.
let frame_base = self.new_frame_base()?;
self.registers.push(map.into()); // Frame base; the map is `self` for `@index_mut`.
self.registers.push(index_value);
self.registers.push(value);
self.call_callable(
&CallInfo {
// The result of a mutable index assignment is always the RHS, so the
// function result can be placed in the frame base where it will be
// immediately discarded.
result_register: frame_base,
arg_count: 2,
frame_base,
instance: Some(frame_base),
},
index_mut_fn,
None,
)?;
Ok(())
}
Map(map) => match index_value {
Number(index) => {
let mut map_data = map.data_mut();
Expand Down Expand Up @@ -2303,7 +2328,6 @@ impl KotoVm {

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

let result = match (&value, index) {
(List(l), Number(n)) => {
Expand Down Expand Up @@ -2344,8 +2368,8 @@ impl KotoVm {
};
Str(result)
}
(Map(m), index) if m.contains_meta_key(&index_op) => {
let op = m.get_meta_value(&index_op).unwrap();
(Map(m), index) if m.contains_meta_key(&BinaryOp::Index.into()) => {
let op = m.get_meta_value(&BinaryOp::Index.into()).unwrap();
return self.call_overridden_binary_op(result_register, value_register, index, op);
}
(Map(m), Number(n)) => {
Expand Down
42 changes: 34 additions & 8 deletions crates/runtime/tests/vm_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3457,7 +3457,7 @@ c.get_a() + c.get_b()
#[test]
fn basic_call() {
let script = "
x = { @||: || 42 }
x = { @call: || 42 }
x()
";
check_script_output(script, 42);
Expand All @@ -3466,7 +3466,7 @@ x()
#[test]
fn with_args() {
let script = "
x = { @||: |a, b| a + b }
x = { @call: |a, b| a + b }
x 12, 34
";
check_script_output(script, 46);
Expand All @@ -3477,7 +3477,7 @@ x 12, 34
let script = "
x =
data: 99
@||: |z| self.data * z
@call: |z| self.data * z
x 10
";
check_script_output(script, 990);
Expand All @@ -3491,10 +3491,36 @@ x 10
fn index() {
let script = "
x =
@[]: |i| i + 10
x[1]
data: [1, 2, 3]
@index: |i| self.data[i]
x[1] + x[2]
";
check_script_output(script, 11);
check_script_output(script, 5);
}

#[test]
fn index_mut() {
let script = "
x =
data: [1, 2, 3]
@index: |i| self.data[i]
@index_mut: |i, x| self.data[i] = x
x[1] = 99
x[2] = 1
x[1] + x[2]
";
check_script_output(script, 100);
}

#[test]
fn index_mut_result_is_rhs() {
let script = "
x =
@index_mut: |_i, _x|
-1 # The result of @index_mut should be discarded
x[1] = 99
";
check_script_output(script, 99);
}

#[test]
Expand All @@ -3514,7 +3540,7 @@ size x
let script = "
foo = |data|
data: data
@[]: |index| self.data[index]
@index: |index| self.data[index]
@size: || size self.data

f = |(a, b, others...)| a + b + size others
Expand All @@ -3529,7 +3555,7 @@ f x # 10 + 11 + 2
let script = "
foo = |data|
data: data
@[]: |index| self.data[index]
@index: |index| self.data[index]
@size: || size self.data

match foo (10, 11, 12, 13)
Expand Down
Loading
Loading