Skip to content

Commit 3d576ae

Browse files
authored
Merge pull request #361 from koto-lang/koto-object-index-mut
Mutable indexing improvements
2 parents b6a9a1e + 3f17ee8 commit 3d576ae

File tree

12 files changed

+167
-30
lines changed

12 files changed

+167
-30
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The Koto project adheres to
1717
- `export` can be used with multi-assignment expressions.
1818
- E.g. expressions like `export a, b, c = foo()` are now allowed.
1919
- Maps now support `[]` indexing, returning the Nth entry as a tuple.
20+
- Entries can also be replaced by index by assigning a key/value tuple.
2021
- Objects that implement `KotoObject::call` can now be used in operations that
2122
expect functions.
2223
- `KotoObject::is_callable` has been added to support this, and needs to be
@@ -33,6 +34,8 @@ The Koto project adheres to
3334
- The macros in `koto_derive` have been updated to support generics.
3435
- `KotoField` has been added to reduce boilerplate when using the derive
3536
macros.
37+
- `KotoObject::index_mut` has been added to allow objects to support mutable
38+
indexing operations.
3639
- `TryFrom<KValue>` has been implemented for some `core` and `std` types,
3740
including `bool`, string, and number types.
3841

crates/bytecode/src/compiler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2680,7 +2680,7 @@ impl Compiler {
26802680
}
26812681
ChainNode::Index(_) => {
26822682
let index = index.unwrap(self)?;
2683-
self.push_op(SetIndex, &[container_register, index, value_register]);
2683+
self.push_op(IndexMut, &[container_register, index, value_register]);
26842684
}
26852685
_ => {}
26862686
}

crates/bytecode/src/instruction.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ pub enum Instruction {
270270
value: u8,
271271
index: u8,
272272
},
273-
SetIndex {
273+
IndexMut {
274274
register: u8,
275275
index: u8,
276276
value: u8,
@@ -693,13 +693,13 @@ impl fmt::Debug for Instruction {
693693
f,
694694
"Index\t\tresult: {register}\tvalue: {value}\tindex: {index}"
695695
),
696-
SetIndex {
696+
IndexMut {
697697
register,
698698
index,
699699
value,
700700
} => write!(
701701
f,
702-
"SetIndex\tregister: {register}\tindex: {index}\tvalue: {value}"
702+
"IndexMut\tregister: {register}\tindex: {index}\tvalue: {value}"
703703
),
704704
MapInsert {
705705
register,

crates/bytecode/src/instruction_reader.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ impl Iterator for InstructionReader {
386386
value: get_u8!(),
387387
index: get_u8!(),
388388
}),
389-
Op::SetIndex => Some(SetIndex {
389+
Op::IndexMut => Some(IndexMut {
390390
register: get_u8!(),
391391
index: get_u8!(),
392392
value: get_u8!(),

crates/bytecode/src/op.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ pub enum Op {
413413
/// Sets a contained value via index
414414
///
415415
/// `[*indexable, *value, *index]`
416-
SetIndex,
416+
IndexMut,
417417

418418
/// Inserts a key/value entry into a map
419419
///

crates/cli/docs/language_guide.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,15 @@ print! m[1]
643643
check! ('oranges', 99)
644644
```
645645

646+
Entries can also be replaced by assigning a key/value tuple to the entry's index.
647+
648+
```koto
649+
m = {apples: 42, oranges: 99, lemons: 63}
650+
m[1] = ('pears', 123)
651+
print! m
652+
check! {apples: 42, pears: 123, lemons: 63}
653+
```
654+
646655
### Shorthand Values
647656

648657
Koto supports a shorthand notation when creating maps with inline syntax.

crates/cli/docs/libs/color.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,18 @@ check! Color(RGB, r: 0.2, g: 0.4, b: 0.3, a: 0.5)
214214

215215
The `color` module's core color type.
216216

217+
An `alpha` value is always present as the color's fourth component.
218+
217219
The color may belong to various different color spaces,
218220
with the space's components available via iteration or indexing.
219221

220-
An `alpha` value is always present as the fourth component.
222+
Components can be modified via index.
223+
221224

222225
### Example
223226

224227
```koto
225-
r, g, b = color('yellow')
228+
r, g, b = color 'yellow'
226229
print! r, g, b
227230
check! (1.0, 1.0, 0.0)
228231
@@ -232,6 +235,13 @@ check! (90.0, 0.5, 0.25, 1.0)
232235
233236
print! color('red')[0]
234237
check! 1.0
238+
239+
print! c = color.oklch 0.5, 0.1, 180
240+
check! Color(Oklch, l: 0.5, c: 0.1, h: 180, a: 1)
241+
c[0] = 0.25 # Set the lightness component to 0.25
242+
c[3] = 0.1 # Set the alpha component to 0.1
243+
print c
244+
check! Color(Oklch, l: 0.25, c: 0.1, h: 180, a: 0.1)
235245
```
236246

237247
## Color.mix

crates/runtime/src/types/object.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ pub trait KotoObject: KotoType + KotoCopy + KotoEntries + KotoSend + KotoSync +
116116
///
117117
/// By default, the object's type is used as the display string.
118118
///
119-
/// The [`DisplayContext`] is used to append strings to the result, and also provides context
119+
/// The [`DisplayContext`] is used to append strings to the result, and provides information
120120
/// about any parent containers.
121121
fn display(&self, ctx: &mut DisplayContext) -> Result<()> {
122122
ctx.append(self.type_string());
@@ -130,6 +130,13 @@ pub trait KotoObject: KotoType + KotoCopy + KotoEntries + KotoSend + KotoSync +
130130
unimplemented_error("@index", self.type_string())
131131
}
132132

133+
/// Called when mutating an object via indexing, e.g. `x[0] = 99`
134+
///
135+
/// See also: [KotoObject::size]
136+
fn index_mut(&mut self, _index: &KValue, _value: &KValue) -> Result<()> {
137+
unimplemented_error("@index_mut", self.type_string())
138+
}
139+
133140
/// Called when checking for the number of elements contained in the object
134141
///
135142
/// The size should represent the maximum valid index that can be passed to

crates/runtime/src/vm.rs

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -892,11 +892,11 @@ impl KotoVm {
892892
value,
893893
index,
894894
} => self.run_index(register, value, index)?,
895-
SetIndex {
895+
IndexMut {
896896
register,
897897
index,
898898
value,
899-
} => self.run_set_index(register, index, value)?,
899+
} => self.run_index_mut(register, index, value)?,
900900
MapInsert {
901901
register,
902902
key,
@@ -2221,7 +2221,7 @@ impl KotoVm {
22212221
import_result
22222222
}
22232223

2224-
fn run_set_index(
2224+
fn run_index_mut(
22252225
&mut self,
22262226
indexable_register: u8,
22272227
index_register: u8,
@@ -2230,8 +2230,8 @@ impl KotoVm {
22302230
use KValue::*;
22312231

22322232
let indexable = self.clone_register(indexable_register);
2233-
let index_value = self.clone_register(index_register);
2234-
let value = self.clone_register(value_register);
2233+
let index_value = self.get_register(index_register);
2234+
let value = self.get_register(value_register);
22352235

22362236
match indexable {
22372237
List(list) => {
@@ -2240,24 +2240,50 @@ impl KotoVm {
22402240
match index_value {
22412241
Number(index) => {
22422242
let u_index = usize::from(index);
2243-
if index >= 0.0 && u_index < list_len {
2244-
list_data[u_index] = value;
2243+
if *index >= 0.0 && u_index < list_len {
2244+
list_data[u_index] = value.clone();
22452245
} else {
2246-
return runtime_error!("Index '{index}' not in List");
2246+
return runtime_error!("Invalid index ({index})");
22472247
}
22482248
}
22492249
Range(range) => {
22502250
for i in range.indices(list_len) {
22512251
list_data[i] = value.clone();
22522252
}
22532253
}
2254-
unexpected => return unexpected_type("index", &unexpected),
2254+
unexpected => return unexpected_type("Number or Range", unexpected),
22552255
}
2256+
Ok(())
22562257
}
2257-
unexpected => return unexpected_type("a mutable indexable value", &unexpected),
2258-
};
2259-
2260-
Ok(())
2258+
Map(map) => match index_value {
2259+
Number(index) => {
2260+
let mut map_data = map.data_mut();
2261+
let map_len = map_data.len();
2262+
let u_index = usize::from(index);
2263+
if *index >= 0.0 && u_index < map_len {
2264+
match value {
2265+
Tuple(new_entry) if new_entry.len() == 2 => {
2266+
let key = ValueKey::try_from(new_entry[0].clone())?;
2267+
// There's no API on IndexMap for replacing an entry,
2268+
// so use swap_remove_index to remove the old entry,
2269+
// then insert the new entry at the end of the map,
2270+
// followed by swap_indices to swap the new entry back into position.
2271+
map_data.swap_remove_index(u_index);
2272+
map_data.insert(key, new_entry[1].clone());
2273+
map_data.swap_indices(u_index, map_len - 1);
2274+
Ok(())
2275+
}
2276+
unexpected => unexpected_type("Tuple with 2 elements", unexpected),
2277+
}
2278+
} else {
2279+
runtime_error!("Invalid index ({index})")
2280+
}
2281+
}
2282+
unexpected => unexpected_type("Number", unexpected),
2283+
},
2284+
Object(o) => o.try_borrow_mut()?.index_mut(index_value, value),
2285+
unexpected => unexpected_type("a mutable indexable value", &unexpected),
2286+
}
22612287
}
22622288

22632289
fn validate_index(&self, n: KNumber, size: Option<usize>) -> Result<usize> {

crates/runtime/tests/object_tests.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,22 @@ mod objects {
140140
}
141141
}
142142

143+
fn index_mut(&mut self, index: &KValue, value: &KValue) -> Result<()> {
144+
match index {
145+
KValue::Number(index) => {
146+
assert_eq!(usize::from(index), 0);
147+
match value {
148+
KValue::Number(value) => {
149+
self.x = value.into();
150+
Ok(())
151+
}
152+
unexpected => unexpected_type("Number as value", unexpected),
153+
}
154+
}
155+
unexpected => unexpected_type("Number as index", unexpected),
156+
}
157+
}
158+
143159
fn size(&self) -> Option<usize> {
144160
Some(self.x.unsigned_abs() as usize)
145161
}
@@ -749,6 +765,24 @@ match make_object 10
749765
";
750766
test_object_script(script, 45);
751767
}
768+
769+
#[test]
770+
fn index_mut_assign() {
771+
let script = "
772+
x = make_object 100
773+
x[0] = 23
774+
";
775+
test_object_script(script, 23);
776+
}
777+
778+
#[test]
779+
fn index_mut_compound_assign() {
780+
let script = "
781+
x = make_object 100
782+
x[0] += 1
783+
";
784+
test_object_script(script, 101);
785+
}
752786
}
753787

754788
#[test]

koto/tests/libs/color.koto

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import color
22

3-
assert_color_near = |a, b|
3+
assert_color_near = |(a0, a1, a2, a3), (b0, b1, b2, b3)|
44
allowed_error = 1.0e-3
5-
for component_a, component_b in a.zip b
6-
assert_near component_a, component_b, allowed_error
7-
assert_near component_a, component_b, allowed_error
8-
assert_near component_a, component_b, allowed_error
9-
assert_near component_a, component_b, allowed_error
5+
assert_near a0, b0, allowed_error
6+
assert_near a1, b1, allowed_error
7+
assert_near a2, b2, allowed_error
8+
assert_near a3, b3, allowed_error
109

1110
@tests =
1211
@test rgb: ||
@@ -38,6 +37,11 @@ assert_color_near = |a, b|
3837
assert_eq (color 'blue')[1], 0
3938
assert_eq (color 'blue')[2], 1
4039

40+
@test index_mut: ||
41+
c = color 'red'
42+
c[0] = 0.5
43+
assert_eq c[0], 0.5
44+
4145
@test iterator: ||
4246
assert_eq (color 'blue').to_tuple(), (0, 0, 1, 1)
4347

libs/color/src/color.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ impl Color {
3232
s.parse::<palette::Srgb<u8>>().ok().map(Self::from)
3333
}
3434

35-
pub fn get_component(&self, n: usize) -> Option<f32> {
35+
pub fn get_component(&self, index: usize) -> Option<f32> {
3636
use Color::*;
3737

38-
let result = match (self, n) {
38+
let result = match (self, index) {
3939
(Srgb(c), 0) => c.color.red,
4040
(Srgb(c), 1) => c.color.green,
4141
(Srgb(c), 2) => c.color.blue,
@@ -62,6 +62,36 @@ impl Color {
6262
Some(result)
6363
}
6464

65+
pub fn set_component(&mut self, index: usize, value: f32) -> Result<()> {
66+
use Color::*;
67+
68+
match (self, index) {
69+
(Srgb(c), 0) => c.color.red = value,
70+
(Srgb(c), 1) => c.color.green = value,
71+
(Srgb(c), 2) => c.color.blue = value,
72+
(Srgb(c), 3) => c.alpha = value,
73+
(Hsl(c), 0) => c.color.hue = value.into(),
74+
(Hsl(c), 1) => c.color.saturation = value,
75+
(Hsl(c), 2) => c.color.lightness = value,
76+
(Hsl(c), 3) => c.alpha = value,
77+
(Hsv(c), 0) => c.color.hue = value.into(),
78+
(Hsv(c), 1) => c.color.saturation = value,
79+
(Hsv(c), 2) => c.color.value = value,
80+
(Hsv(c), 3) => c.alpha = value,
81+
(Oklab(c), 0) => c.color.l = value,
82+
(Oklab(c), 1) => c.color.a = value,
83+
(Oklab(c), 2) => c.color.b = value,
84+
(Oklab(c), 3) => c.alpha = value,
85+
(Oklch(c), 0) => c.color.l = value,
86+
(Oklch(c), 1) => c.color.chroma = value,
87+
(Oklch(c), 2) => c.color.hue = value.into(),
88+
(Oklch(c), 3) => c.alpha = value,
89+
_ => return runtime_error!("Invalid component index ({index})"),
90+
}
91+
92+
Ok(())
93+
}
94+
6595
pub fn color_space_str(&self) -> &str {
6696
use Color::*;
6797

@@ -174,6 +204,20 @@ impl KotoObject for Color {
174204
}
175205
}
176206

207+
fn size(&self) -> Option<usize> {
208+
// All current color spaces have 4 components
209+
Some(4)
210+
}
211+
212+
fn index_mut(&mut self, index: &KValue, value: &KValue) -> Result<()> {
213+
use KValue::Number;
214+
215+
match (index, value) {
216+
(Number(index), Number(value)) => self.set_component(index.into(), value.into()),
217+
_ => unexpected_args("two Numbers", &[index.clone(), value.clone()]),
218+
}
219+
}
220+
177221
fn is_iterable(&self) -> IsIterable {
178222
IsIterable::Iterable
179223
}

0 commit comments

Comments
 (0)