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
2 changes: 2 additions & 0 deletions docs/BUILTINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ numeric fast paths used by reassignment and `unobserved` blocks.
| `set_at` | `set_at of [list, index, value]` | Set element at index (mutates list); negative indices count from the end, like `[]` |
| `get_at` | `get_at of [list, index]` | Get element at index; negative indices count from the end, like `[]` |
| `copy_into` | `copy_into of [dest, src, offset]` | Copy src elements into dest starting at offset |
| `list_slice` | `list_slice of [list, start, end]` | New list with the elements of [start, end) — dual of `copy_into`. Negative indices count from the end, like `[]`; bounds then clamp to [0, len]. `start >= end` gives `[]`. Never raises on bounds |
| `num_copy` | `num_copy of value` | Create independent copy of numeric value |
| `hex` | `hex of n` or `hex of [n, nibbles]` | Uppercase hex string of a non-negative integer, zero-padded to `nibbles` (never truncated). Raises on negatives, fractions, non-numbers |
| `sort` | `sort of list` | Sort an all-number or all-string list in-place (numeric / lexicographic). Mixed or non-scalar elements raise — use `sort_by` for records. Returns the list |
| `list_truncate` | `list_truncate of [list, new_len]` | Shrink list in-place to new_len items. No-op if new_len >= length. Returns the list |
| `list_remove_at` | `list_remove_at of [list, index]` | Remove element at index, shift tail down (mutates). No-op if out of bounds. Returns the list |
| `list_insert_at` | `list_insert_at of [list, index, value]` | Insert value at index, shift tail up (mutates) — dual of `list_remove_at`. `index == len` appends; any other out-of-bounds index is a no-op. Returns the list |
| `sort_by` | `sort_by of [list, key_fn]` | Sort list by numeric keys from key_fn (qsort, O(n log n), stable). Returns a new sorted list |
| `dispatch` | `dispatch of [table, key, arg]` | Index list `table` by numeric `key` and call the resulting function with `arg` — a jump table (mirrors the `OP_DISPATCH` fast path). `key` must be a number. An ordinary builtin, not a special form: a user binding of the name wins (#459 — the fast path steps aside for any unit that rebinds `dispatch`, references `eval`, or compiles against an env where it is already rebound), and the parenthesized `dispatch of ([t, k, a])` form is one argument per #355/#405 |

Expand Down
71 changes: 70 additions & 1 deletion src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -4056,7 +4056,8 @@ static const char *SANDBOX_ALLOW[] = {
"bit_and", "bit_not", "bit_or", "bit_shl", "bit_shr", "bit_xor",
/* list / sequence (operate on the value handed in) */
"append", "concat", "contains", "copy_into", "get_at", "index_of", "len",
"list_remove_at", "list_truncate", "set_at", "sort", "sort_by", "range",
"list_insert_at", "list_remove_at", "list_slice", "list_truncate", "set_at",
"sort", "sort_by", "range",
/* dict */
"dict_set", "dict_remove", "has_key", "keys", "values",
/* string + regex (pure) */
Expand Down Expand Up @@ -4241,6 +4242,38 @@ Value* builtin_copy_into(Value *arg) {
return dest;
}

/* ==== BUILTIN: list_slice ==== */
/* list_slice of [list, start, end] → new list with the elements of [start, end).
* Dual of copy_into (which copies a range IN). Negative indices count from the
* end (like get_at/set_at, #312); both bounds then clamp to [0, len].
* start >= end gives []. Never raises on bounds. */
Value* builtin_list_slice(Value *arg) {
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) return make_null();
Value *list = arg->data.list.items[0];
if (!list || list->type != VAL_LIST) return make_null();
Value *start_v = arg->data.list.items[1];
Value *end_v = arg->data.list.items[2];
if (!start_v || start_v->type != VAL_NUM || !end_v || end_v->type != VAL_NUM)
return make_null();
int n = list->data.list.count;
int start = (int)start_v->data.num;
int end = (int)end_v->data.num;
if (start < 0) start += n;
if (end < 0) end += n;
if (start < 0) start = 0;
if (start > n) start = n;
if (end < 0) end = 0;
if (end > n) end = n;
if (start >= end) return make_list(0);
int out_n = end - start;
/* #292: charge the new slots, same as concat. */
if (!sandbox_charge((size_t)out_n * sizeof(Value *))) return make_null();
Value *result = make_list(out_n);
for (int i = start; i < end; i++)
list_append(result, list->data.list.items[i]);
return result;
}

/* ==== BUILTIN: num_copy ==== */
/* num_copy of val → fresh heap-allocated copy of a numeric Value.
* Use to extract a scalar from arena before arena_reset. */
Expand Down Expand Up @@ -6319,6 +6352,40 @@ Value* builtin_list_remove_at(Value *arg) {
return list;
}

/* list_insert_at of [list, index, value] — insert value at index, shift tail
* up. Dual of list_remove_at. index == count appends; any other out-of-bounds
* index is a no-op. Returns the list. */
Value* builtin_list_insert_at(Value *arg) {
if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) return make_null();
Value *list = arg->data.list.items[0];
Value *idx_val = arg->data.list.items[1];
Value *val = arg->data.list.items[2];
if (!list || list->type != VAL_LIST) return make_null();
if (!idx_val || idx_val->type != VAL_NUM) return list;
int idx = (int)idx_val->data.num;
int count = list->data.list.count;
if (idx < 0 || idx > count) return list;
if (idx == count) {
list_append(list, val);
return list;
}
/* Grow by one slot (list_append handles capacity + arena lists), then move
* the tail up one place and drop the value into the vacated slot.
* list_append increfs the old last element, but the memmove overwrites
* its original slot with a raw pointer copy (no decref) — balance that
* spurious incref afterwards so old_last keeps exactly the one reference
* its single remaining slot owns. It still occupies slot [count] at that
* point, so the decref cannot free it. */
Value *old_last = list->data.list.items[count - 1];
list_append(list, old_last);
memmove(&list->data.list.items[idx + 1], &list->data.list.items[idx],
(count - idx) * sizeof(Value *));
val_incref(val);
list->data.list.items[idx] = val;
val_decref(old_last);
return list;
}

/* sort_by of [list, key_fn] — sort list by numeric keys from key_fn.
* Evaluates key_fn once per element, then qsorts by key (ascending).
* Stable tiebreak by original index. Returns a NEW sorted list. */
Expand Down Expand Up @@ -6689,6 +6756,7 @@ void register_builtins(Env *env) {
env_set_local_owned(env, "tensor_load", make_builtin(builtin_tensor_load));
#endif /* !EIGENSCRIPT_FREESTANDING */
env_set_local_owned(env, "copy_into", make_builtin(builtin_copy_into));
env_set_local_owned(env, "list_slice", make_builtin(builtin_list_slice));
env_set_local_owned(env, "num_copy", make_builtin(builtin_num_copy));
env_set_local_owned(env, "concat", make_builtin(builtin_concat));
env_set_local_owned(env, "range", make_builtin(builtin_range));
Expand Down Expand Up @@ -6745,6 +6813,7 @@ void register_builtins(Env *env) {
env_set_local_owned(env, "sort", make_builtin(builtin_sort));
env_set_local_owned(env, "list_truncate", make_builtin(builtin_list_truncate));
env_set_local_owned(env, "list_remove_at", make_builtin(builtin_list_remove_at));
env_set_local_owned(env, "list_insert_at", make_builtin(builtin_list_insert_at));
env_set_local_owned(env, "sort_by", make_builtin(builtin_sort_by));
env_set_local_owned(env, "close_channel", make_builtin(builtin_close_channel));
env_set_local_owned(env, "channel_closed", make_builtin(builtin_channel_closed));
Expand Down
30 changes: 30 additions & 0 deletions tests/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2360,6 +2360,36 @@ else
fi
echo ""

# [121] list_insert_at builtin
echo "[121] List Insert At (8 checks)"
LIA_OUTPUT=$(./eigenscript ../tests/test_list_insert_at.eigs 2>&1); LIA_OUTPUT_RC=$?
if rc_ok "$LIA_OUTPUT_RC" "$LIA_OUTPUT" && echo "$LIA_OUTPUT" | grep -q "All tests passed"; then
TOTAL=$((TOTAL + 8))
PASS=$((PASS + 8))
echo " PASS: all 8 list_insert_at checks"
else
TOTAL=$((TOTAL + 8))
FAIL=$((FAIL + 8))
echo " FAIL: list_insert_at tests"
echo "$LIA_OUTPUT" | grep -iE "assert|error|FAIL" | head -5
fi
echo ""

# [122] list_slice builtin
echo "[122] List Slice (8 checks)"
LSL_OUTPUT=$(./eigenscript ../tests/test_list_slice.eigs 2>&1); LSL_OUTPUT_RC=$?
if rc_ok "$LSL_OUTPUT_RC" "$LSL_OUTPUT" && echo "$LSL_OUTPUT" | grep -q "All tests passed"; then
TOTAL=$((TOTAL + 8))
PASS=$((PASS + 8))
echo " PASS: all 8 list_slice checks"
else
TOTAL=$((TOTAL + 8))
FAIL=$((FAIL + 8))
echo " FAIL: list_slice tests"
echo "$LSL_OUTPUT" | grep -iE "assert|error|FAIL" | head -5
fi
echo ""

# [65] sort_by builtin
echo "[65] Sort By (9 checks)"
SBY_OUTPUT=$(./eigenscript ../tests/test_sort_by.eigs 2>&1); SBY_OUTPUT_RC=$?
Expand Down
81 changes: 81 additions & 0 deletions tests/test_list_insert_at.eigs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
checks is 0
passed is 0

# T1: insert middle element
a is [10, 20, 40]
list_insert_at of [a, 2, 30]
checks += 1
if (len of a) == 4 and a[0] == 10 and a[1] == 20 and a[2] == 30 and a[3] == 40:
passed += 1

# T2: insert at start (prepend)
b is [10, 20, 30]
list_insert_at of [b, 0, 5]
checks += 1
if (len of b) == 4 and b[0] == 5 and b[1] == 10 and b[3] == 30:
passed += 1

# T3: insert at index == length (append)
c is [10, 20, 30]
list_insert_at of [c, 3, 99]
checks += 1
if (len of c) == 4 and c[2] == 30 and c[3] == 99:
passed += 1

# T4: out-of-bounds negative — no-op
d is [1, 2, 3]
list_insert_at of [d, -1, 9]
checks += 1
if (len of d) == 3 and d[0] == 1 and d[2] == 3:
passed += 1

# T5: out-of-bounds too large — no-op
e is [1, 2, 3]
list_insert_at of [e, 5, 9]
checks += 1
if (len of e) == 3 and e[0] == 1 and e[2] == 3:
passed += 1

# T6: insert into empty list
f is []
list_insert_at of [f, 0, 42]
checks += 1
if (len of f) == 1 and f[0] == 42:
passed += 1

# T7: returns the list
g is [1, 2, 3]
result is list_insert_at of [g, 1, 99]
checks += 1
if (len of result) == 4 and result[0] == 1 and result[1] == 99 and result[2] == 2:
passed += 1

# T8: dual round-trip — insert then remove_at restores the original
h is [1, 2, 3, 4]
list_insert_at of [h, 1, 99]
list_remove_at of [h, 1]
checks += 1
if (len of h) == 4 and h[0] == 1 and h[1] == 2 and h[2] == 3 and h[3] == 4:
passed += 1

# T9: repeated middle-inserts — stresses the shifted-tail path (prepend at
# index 0 forces a full tail memmove every iteration; also a refcount-leak
# regression guard for the old-last-element slot)
s is []
n is 1
loop while n <= 200:
list_insert_at of [s, 0, n]
n is n + 1
total is 0
j is 0
loop while j < len of s:
total is total + s[j]
j is j + 1
checks += 1
if (len of s) == 200 and s[0] == 200 and s[199] == 1 and total == 20100:
passed += 1

if passed == checks:
print of "All tests passed"
else:
print of f"FAIL: {passed}/{checks} passed"
56 changes: 56 additions & 0 deletions tests/test_list_slice.eigs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
checks is 0
passed is 0

# T1: normal range — [start, end)
a is list_slice of [[1, 2, 3, 4, 5], 1, 4]
checks += 1
if (len of a) == 3 and a[0] == 2 and a[1] == 3 and a[2] == 4:
passed += 1

# T2: full range
b is list_slice of [[1, 2, 3], 0, 3]
checks += 1
if (len of b) == 3 and b[0] == 1 and b[2] == 3:
passed += 1

# T3: start >= end gives []
c is list_slice of [[1, 2, 3], 2, 2]
checks += 1
if (len of c) == 0:
passed += 1

# T4: out-of-bounds clamps, never raises — [] for an empty intersection
d is list_slice of [[1, 2], 5, 9]
checks += 1
if (len of d) == 0:
passed += 1

# T5: end past length clamps to length
e is list_slice of [[1, 2, 3], 1, 99]
checks += 1
if (len of e) == 2 and e[0] == 2 and e[1] == 3:
passed += 1

# T6: negative indices count from the end
f is list_slice of [[1, 2, 3, 4, 5], -3, -1]
checks += 1
if (len of f) == 2 and f[0] == 3 and f[1] == 4:
passed += 1

# T7: returns a new list — the source is unchanged
g is [1, 2, 3, 4]
s is list_slice of [g, 1, 3]
checks += 1
if (len of g) == 4 and (len of s) == 2 and s[0] == 2 and s[1] == 3:
passed += 1

# T8: slice of empty list is []
h is list_slice of [[], 0, 4]
checks += 1
if (len of h) == 0:
passed += 1

if passed == checks:
print of "All tests passed"
else:
print of f"FAIL: {passed}/{checks} passed"
Loading