diff --git a/manual/src/reference/types/list.md b/manual/src/reference/types/list.md index 3eea96d4..5820baba 100644 --- a/manual/src/reference/types/list.md +++ b/manual/src/reference/types/list.md @@ -24,6 +24,31 @@ let my_list = [1,2,3,4,5,6,7,8,9]; assert_eq(9, my_list[-1]); ``` +### Element accessors + +Besides the `[]` operator, lists provide four element accessor functions. They +differ on two axes: whether negative indexes are allowed, and what happens when +the index is out of bounds. + +| function | index | out of bounds | +|----------|-------|---------------| +| `get` | non-negative only (negative is an error) | error | +| `get?` | non-negative only (negative is an error) | `None` | +| `index` | signed; negatives wrap from the end | error | +| `index?` | signed; negatives wrap from the end | `None` | + +```ndc +let my_list = [10, 20, 30]; + +assert_eq(my_list.get(0), 10); +assert_eq(my_list.get?(9), None); // in range for a usize, past the end +assert_eq(my_list.index(-1), 30); // wraps like `[]` +assert_eq(my_list.index?(-9), None); +``` + +`index` behaves like the `[]` operator (wrap + error on out of bounds); +`index?` is its non-throwing counterpart. + ## Slicing {{#include ../../snippets/slices.md}} diff --git a/manual/src/reference/types/option.md b/manual/src/reference/types/option.md index e406dea5..50e344cb 100644 --- a/manual/src/reference/types/option.md +++ b/manual/src/reference/types/option.md @@ -24,15 +24,24 @@ let my_list = [1,2,3]; let fst = my_list.first?; // Some(1) ``` -`get?` retrieves a list element by index, returning `None` instead of erroring when -the index is out of bounds. Unlike the `[]` operator, a negative index does not wrap -around — it is simply out of bounds: +A list has two pairs of element accessors. `get` / `get?` take a non-negative +index (a `usize`); `index` / `index?` take a signed integer and wrap negative +indices from the end, like the `[]` operator. The `?` variant of each returns +`None` when the index is past the end instead of erroring: ```ndc let my_list = [10, 20, 30]; -my_list.get?(0); // Some(10) -my_list.get?(5); // None -my_list.get?(-1); // None + +my_list.get(0); // 10 +my_list.get(5); // ERROR: index 5 is out of bounds +my_list.get?(0); // Some(10) +my_list.get?(5); // None +my_list.get?(-1); // ERROR: a negative index is not a valid usize + +my_list.index(-1); // 30 (wraps from the end) +my_list.index(5); // ERROR: index out of bounds +my_list.index?(-1); // Some(30) +my_list.index?(5); // None ``` `unwrap_or` extracts the contained value, falling back to a default when the option is `None`: diff --git a/ndc_core/src/num.rs b/ndc_core/src/num.rs index 1584477b..b1f5f7a9 100644 --- a/ndc_core/src/num.rs +++ b/ndc_core/src/num.rs @@ -692,11 +692,11 @@ implement_rounding!(round); #[derive(thiserror::Error, Debug)] pub enum NumberToUsizeError { - #[error("cannot convert from {0} to usize")] + #[error("expected a non-negative integer, got {0}")] UnsupportedVariant(StaticType), - #[error("expected non-negative integer for indexing")] + #[error("expected a non-negative integer, but the value was negative")] FromIntError(#[from] TryFromIntError), - #[error("failed to convert from bigint to number because of: '{0}'")] + #[error("this integer is out of range (must be non-negative and small enough to be an index)")] FromBigIntError(#[from] TryFromBigIntError), } diff --git a/ndc_macros/src/vm_convert.rs b/ndc_macros/src/vm_convert.rs index acfda94e..f53bd8fa 100644 --- a/ndc_macros/src/vm_convert.rs +++ b/ndc_macros/src/vm_convert.rs @@ -160,9 +160,11 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { let err = arg_error(position, "int"); VmInputArg { extract: quote! { - let #temp = match #raw { - ndc_vm::value::Value::Int(i) => *i as usize, - _ => return Err(#err), + let #temp = { + let num = #raw.to_number().ok_or_else(|| #err)?; + usize::try_from(num).map_err(|e| { + ndc_vm::error::VmError::native(format!("arg {}: {}", #position, e)) + })? }; }, pass: quote! { #temp }, diff --git a/ndc_stdlib/src/list.rs b/ndc_stdlib/src/list.rs index f10463ed..0c45b0e2 100644 --- a/ndc_stdlib/src/list.rs +++ b/ndc_stdlib/src/list.rs @@ -6,6 +6,7 @@ mod inner { use std::rc::Rc; use anyhow::anyhow; + use num::{BigInt, ToPrimitive}; /// Converts any sequence into a list #[function(return_type = Vec<_>)] @@ -110,19 +111,60 @@ mod inner { list.remove(0) } - /// Returns a copy of the element at `index`, or `None` if the index is out of bounds. - /// A negative index is always out of bounds. + /// Resolves a possibly-negative index against `len`, wrapping negative + /// indices from the end (Python-style: `-1` is the last element). Returns + /// `None` when the index is out of bounds in either direction. + fn wrap_index(i: &BigInt, len: usize) -> Option { + let i = i.to_i64()?; // too large for i64 => out of bounds + if i >= 0 { + let idx = usize::try_from(i).ok()?; + (idx < len).then_some(idx) + } else { + // -1 => len - 1, -len => 0, anything more negative => out of bounds + len.checked_sub(usize::try_from(i.unsigned_abs()).ok()?) + } + } + + /// Returns a copy of the element at `index`, or an error if the index is out + /// of bounds. The index must be non-negative; use `index` for indexing from + /// the end. + pub fn get(list: &[Value], index: usize) -> anyhow::Result { + list.get(index) + .cloned() + .ok_or_else(|| anyhow!("index {index} is out of bounds")) + } + + /// Returns a copy of the element at `index`, or `None` if `index` is past + /// the end of the list. A negative or oversized index is a conversion error, + /// not `None`; use `index?` for indexing from the end. #[function(name = "get?", return_type = Option<_>)] - pub fn maybe_get(list: &[Value], index: i64) -> Value { - let Ok(idx) = usize::try_from(index) else { - return Value::None; - }; - match list.get(idx) { + pub fn maybe_get(list: &[Value], index: usize) -> Value { + match list.get(index) { None => Value::None, Some(v) => Value::Object(Rc::new(Object::Some(v.clone()))), } } + /// Returns a copy of the element at `i`, wrapping negative indices from the + /// end of the list; errors if the index is out of bounds. The named form of + /// the `[]` operator. + pub fn index(list: &[Value], i: &BigInt) -> anyhow::Result { + wrap_index(i, list.len()) + .map(|idx| list[idx].clone()) + .ok_or_else(|| anyhow!("index out of bounds")) + } + + /// Returns a copy of the element at `i`, wrapping negative indices from the + /// end of the list; returns `None` if the index is out of bounds. The + /// non-throwing twin of the `[]` operator. + #[function(name = "index?", return_type = Option<_>)] + pub fn maybe_index(list: &[Value], i: &BigInt) -> Value { + match wrap_index(i, list.len()) { + Some(idx) => Value::Object(Rc::new(Object::Some(list[idx].clone()))), + None => Value::None, + } + } + /// Creates a copy of the list with its elements in reverse order #[function(return_type = Vec<_>)] pub fn reversed(list: &[Value]) -> Value { diff --git a/tests/functional/programs/601_stdlib_list/004_list_get.ndc b/tests/functional/programs/601_stdlib_list/004_list_get.ndc index bb559239..bfc2f232 100644 --- a/tests/functional/programs/601_stdlib_list/004_list_get.ndc +++ b/tests/functional/programs/601_stdlib_list/004_list_get.ndc @@ -4,12 +4,9 @@ let list = [10, 20, 30]; assert_eq(list.get?(0), Some(10)); assert_eq(list.get?(2), Some(30)); -// out of bounds returns None +// a valid index past the end returns None assert_eq(list.get?(3), None); -// negative indices are always out of bounds (no wrap-around) -assert_eq(list.get?(-1), None); - // empty list assert_eq([].get?(0), None); diff --git a/tests/functional/programs/601_stdlib_list/005_list_get_strict.ndc b/tests/functional/programs/601_stdlib_list/005_list_get_strict.ndc new file mode 100644 index 00000000..8fab4fd2 --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/005_list_get_strict.ndc @@ -0,0 +1,5 @@ +let list = [10, 20, 30]; + +// `get` returns the element directly when the index is in bounds +assert_eq(list.get(0), 10); +assert_eq(list.get(2), 30); diff --git a/tests/functional/programs/601_stdlib_list/006_list_index.ndc b/tests/functional/programs/601_stdlib_list/006_list_index.ndc new file mode 100644 index 00000000..56d885ec --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/006_list_index.ndc @@ -0,0 +1,7 @@ +let list = [10, 20, 30]; + +// `index` reads an element, wrapping negative indices from the end +assert_eq(list.index(0), 10); +assert_eq(list.index(2), 30); +assert_eq(list.index(-1), 30); +assert_eq(list.index(-3), 10); diff --git a/tests/functional/programs/601_stdlib_list/007_list_index_maybe.ndc b/tests/functional/programs/601_stdlib_list/007_list_index_maybe.ndc new file mode 100644 index 00000000..1e385a53 --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/007_list_index_maybe.ndc @@ -0,0 +1,14 @@ +let list = [10, 20, 30]; + +// `index?` wraps negative indices like `index`, but returns None when out of bounds +assert_eq(list.index?(0), Some(10)); +assert_eq(list.index?(-1), Some(30)); +assert_eq(list.index?(-3), Some(10)); + +// out of bounds in either direction is None +assert_eq(list.index?(3), None); +assert_eq(list.index?(-4), None); + +// an index too large to address the list is also None (no error) +assert_eq(list.index?(10^40), None); +assert_eq(list.index?(-(10^40)), None); diff --git a/tests/functional/programs/601_stdlib_list/008_list_get_negative_error.ndc b/tests/functional/programs/601_stdlib_list/008_list_get_negative_error.ndc new file mode 100644 index 00000000..5e39fafb --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/008_list_get_negative_error.ndc @@ -0,0 +1,3 @@ +// `get?` takes a usize, so a negative index is a conversion error, not None +// expect-error: non-negative integer +[10, 20, 30].get?(-1); diff --git a/tests/functional/programs/601_stdlib_list/009_list_get_too_large_error.ndc b/tests/functional/programs/601_stdlib_list/009_list_get_too_large_error.ndc new file mode 100644 index 00000000..d30867d8 --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/009_list_get_too_large_error.ndc @@ -0,0 +1,3 @@ +// an index that does not fit a usize is a conversion error +// expect-error: out of range +[10, 20, 30].get?(10^40); diff --git a/tests/functional/programs/601_stdlib_list/010_list_get_strict_oob_error.ndc b/tests/functional/programs/601_stdlib_list/010_list_get_strict_oob_error.ndc new file mode 100644 index 00000000..91d84ca8 --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/010_list_get_strict_oob_error.ndc @@ -0,0 +1,3 @@ +// strict `get` errors when the index is past the end +// expect-error: out of bounds +[10, 20, 30].get(5); diff --git a/tests/functional/programs/601_stdlib_list/011_list_index_oob_error.ndc b/tests/functional/programs/601_stdlib_list/011_list_index_oob_error.ndc new file mode 100644 index 00000000..1f711b15 --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/011_list_index_oob_error.ndc @@ -0,0 +1,3 @@ +// strict `index` errors when the index is past the end +// expect-error: index out of bounds +[10, 20, 30].index(5); diff --git a/tests/functional/programs/601_stdlib_list/012_list_index_overneg_error.ndc b/tests/functional/programs/601_stdlib_list/012_list_index_overneg_error.ndc new file mode 100644 index 00000000..ab414716 --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/012_list_index_overneg_error.ndc @@ -0,0 +1,3 @@ +// a negative index more negative than -len is out of bounds for strict `index` +// expect-error: index out of bounds +[10, 20, 30].index(-4);