From 9bb43ee19d6ba87445db5e84116a070daa6c7919 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Tue, 2 Jun 2026 06:30:48 +0200 Subject: [PATCH] =?UTF-8?q?feat(stdlib):=20add=20list=20get=3F=20and=20unw?= =?UTF-8?q?rap=5For=20=F0=9F=8E=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- manual/src/reference/types/option.md | 18 ++++++++++++++++++ ndc_stdlib/src/list.rs | 13 +++++++++++++ ndc_stdlib/src/value.rs | 12 ++++++++++++ .../programs/601_stdlib_list/004_list_get.ndc | 18 ++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 tests/functional/programs/601_stdlib_list/004_list_get.ndc diff --git a/manual/src/reference/types/option.md b/manual/src/reference/types/option.md index f106661f..e406dea5 100644 --- a/manual/src/reference/types/option.md +++ b/manual/src/reference/types/option.md @@ -24,4 +24,22 @@ 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: + +```ndc +let my_list = [10, 20, 30]; +my_list.get?(0); // Some(10) +my_list.get?(5); // None +my_list.get?(-1); // None +``` + +`unwrap_or` extracts the contained value, falling back to a default when the option is `None`: + +```ndc +[10, 20, 30].get?(1).unwrap_or(0); // 20 +[10, 20, 30].get?(9).unwrap_or(0); // 0 +``` + > **Note:** unfortunately the language doesn't support pattern matching on options diff --git a/ndc_stdlib/src/list.rs b/ndc_stdlib/src/list.rs index c9e16d66..f10463ed 100644 --- a/ndc_stdlib/src/list.rs +++ b/ndc_stdlib/src/list.rs @@ -110,6 +110,19 @@ 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. + #[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) { + None => Value::None, + Some(v) => Value::Object(Rc::new(Object::Some(v.clone()))), + } + } + /// 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/ndc_stdlib/src/value.rs b/ndc_stdlib/src/value.rs index 747099af..adbd8509 100644 --- a/ndc_stdlib/src/value.rs +++ b/ndc_stdlib/src/value.rs @@ -70,6 +70,18 @@ mod inner { } } + /// Extracts the value from an Option, returning `default` when it is `None`. + pub fn unwrap_or(value: Value, default: Value) -> anyhow::Result { + match value { + Value::Object(obj) => match obj.as_ref() { + Object::Some(inner) => Ok(inner.clone()), + _ => Err(anyhow::anyhow!("incorrect argument to unwrap_or")), + }, + Value::None => Ok(default), + _ => Err(anyhow::anyhow!("incorrect argument to unwrap_or")), + } + } + /// Returns a shallow copy of the given value. pub fn clone(value: Value) -> Value { value.shallow_clone() diff --git a/tests/functional/programs/601_stdlib_list/004_list_get.ndc b/tests/functional/programs/601_stdlib_list/004_list_get.ndc new file mode 100644 index 00000000..bb559239 --- /dev/null +++ b/tests/functional/programs/601_stdlib_list/004_list_get.ndc @@ -0,0 +1,18 @@ +let list = [10, 20, 30]; + +// in-bounds returns Some +assert_eq(list.get?(0), Some(10)); +assert_eq(list.get?(2), Some(30)); + +// out of bounds 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); + +// unwrap_or extracts or falls back to the default +assert_eq(list.get?(1).unwrap_or(99), 20); +assert_eq(list.get?(5).unwrap_or(99), 99);