Skip to content
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
25 changes: 25 additions & 0 deletions manual/src/reference/types/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
21 changes: 15 additions & 6 deletions manual/src/reference/types/option.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
6 changes: 3 additions & 3 deletions ndc_core/src/num.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BigInt>),
}

Expand Down
8 changes: 5 additions & 3 deletions ndc_macros/src/vm_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,11 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option<VmInputArg> {
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 },
Expand Down
56 changes: 49 additions & 7 deletions ndc_stdlib/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<_>)]
Expand Down Expand Up @@ -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<usize> {
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<Value> {
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<Value> {
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 {
Expand Down
5 changes: 1 addition & 4 deletions tests/functional/programs/601_stdlib_list/004_list_get.ndc
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
7 changes: 7 additions & 0 deletions tests/functional/programs/601_stdlib_list/006_list_index.ndc
Original file line number Diff line number Diff line change
@@ -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);
14 changes: 14 additions & 0 deletions tests/functional/programs/601_stdlib_list/007_list_index_maybe.ndc
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// strict `get` errors when the index is past the end
// expect-error: out of bounds
[10, 20, 30].get(5);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Loading