Skip to content

Commit 74d5da5

Browse files
feat(parser): slice/range index e[a:b] via slice builtin (#135 slice 2) (#154)
#135 slice 2. The grammar only had `e[i]` (ExprIndex); the stdlib uses slice syntax `list[1:]` (option.affine, collections.affine). Rather than a new AST node (ExprIndex reaches ~20 files incl. every codegen), `e[a:b]` / `e[a:]` / `e[:b]` / `e[:]` desugar in the parser to the `slice` builtin: missing low = 0, missing high = `len(e)`. `slice` is registered like `len` at the four standard points — resolve.ml (builtin name), typecheck.ml (`a -> Int -> Int -> a`), interp.ml (JS-`.slice` semantics: half-open, neg-from-end, clamped), codegen_deno.ml (lowers to `.slice`). Effect: option.affine 238 → 320, collections.affine 24 → 40 (further distinct later-slice defects). Plain `e[i]` indexing unaffected. Two regression tests; full suite green (225 tests). Advances #135. Refs #128, #135. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7f715fd commit 74d5da5

6 files changed

Lines changed: 59 additions & 1 deletion

File tree

lib/codegen_deno.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ let () =
211211
not externs — endsWith/stripSuffix/pathJoin/etc. are NOT here:
212212
they are real AffineScript built on `ends_with`/`substring`/`++`. *)
213213
b "len" (fun a -> Printf.sprintf "((%s).length)" (arg 0 a));
214+
b "slice" (fun a -> Printf.sprintf "((%s).slice(%s, %s))"
215+
(arg 0 a) (arg 1 a) (arg 2 a));
214216
b "string_length" (fun a -> Printf.sprintf "((%s).length)" (arg 0 a));
215217
b "string_get" (fun a -> Printf.sprintf "__as_strGet(%s, %s)" (arg 0 a) (arg 1 a));
216218
b "string_sub" (fun a -> Printf.sprintf "__as_strSub(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));

lib/interp.ml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,23 @@ let create_initial_env () : env =
526526
| _ -> Error (TypeMismatch "len expects array or string")
527527
));
528528

529+
(* slice(coll, lo, hi) — issue #135 slice 2. Half-open [lo, hi);
530+
negative indices count from the end; bounds are clamped (JS .slice
531+
semantics, matching the Deno-ESM backend lowering). *)
532+
("slice", VBuiltin ("slice", fun args ->
533+
let norm n i = if i < 0 then max 0 (n + i) else min i n in
534+
match args with
535+
| [VArray arr; VInt lo; VInt hi] ->
536+
let n = Array.length arr in
537+
let lo = norm n lo and hi = norm n hi in
538+
Ok (VArray (if hi > lo then Array.sub arr lo (hi - lo) else [||]))
539+
| [VString s; VInt lo; VInt hi] ->
540+
let n = String.length s in
541+
let lo = norm n lo and hi = norm n hi in
542+
Ok (VString (if hi > lo then String.sub s lo (hi - lo) else ""))
543+
| _ -> Error (TypeMismatch "slice expects (array|string, Int, Int)")
544+
));
545+
529546
(* -- String builtins --------------------------------------------------- *)
530547
("string_get", VBuiltin ("string_get", fun args ->
531548
match args with

lib/parser.mly

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,23 @@ expr_postfix:
645645
| e = expr_postfix DOT field = field_name { ExprField (e, field) }
646646
| e = expr_postfix DOT n = INT { ExprTupleIndex (e, n) }
647647
| e = expr_postfix LBRACKET idx = expr RBRACKET { ExprIndex (e, idx) }
648+
/* Slice / range index (issue #135 slice 2): `e[a:b]`, `e[a:]`, `e[:b]`,
649+
`e[:]`. Desugars to the `slice` builtin (a -> Int -> Int -> a; lowered
650+
to JS `.slice` on the Deno-ESM backend, like `len`). No new AST node:
651+
missing low = 0, missing high = `len(e)`. Used by stdlib/option.affine
652+
(`list[1:]`) and stdlib/collections.affine. */
653+
| e = expr_postfix LBRACKET lo = expr COLON hi = expr RBRACKET
654+
{ ExprApp (ExprVar (mk_ident "slice" $startpos $endpos), [e; lo; hi]) }
655+
| e = expr_postfix LBRACKET lo = expr COLON RBRACKET
656+
{ ExprApp (ExprVar (mk_ident "slice" $startpos $endpos),
657+
[e; lo; ExprApp (ExprVar (mk_ident "len" $startpos $endpos), [e])]) }
658+
| e = expr_postfix LBRACKET COLON hi = expr RBRACKET
659+
{ ExprApp (ExprVar (mk_ident "slice" $startpos $endpos),
660+
[e; ExprLit (LitInt (0, mk_span $startpos $endpos)); hi]) }
661+
| e = expr_postfix LBRACKET COLON RBRACKET
662+
{ ExprApp (ExprVar (mk_ident "slice" $startpos $endpos),
663+
[e; ExprLit (LitInt (0, mk_span $startpos $endpos));
664+
ExprApp (ExprVar (mk_ident "len" $startpos $endpos), [e])]) }
648665
| e = expr_postfix LPAREN args = separated_list(COMMA, expr) RPAREN { ExprApp (e, args) }
649666
| e = expr_postfix BACKSLASH field = field_name { ExprRowRestrict (e, field) }
650667
| e = expr_postfix QUESTION { ExprTry { et_body = { blk_stmts = []; blk_expr = Some e };

lib/resolve.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ let create_context () : context =
5656
(* Console I/O *)
5757
def "print"; def "println"; def "eprint"; def "eprintln";
5858
(* String / char builtins *)
59-
def "len"; def "string_get"; def "string_sub"; def "string_find";
59+
def "len"; def "slice"; def "string_get"; def "string_sub"; def "string_find";
6060
def "char_to_int"; def "int_to_char"; def "show";
6161
def "to_lowercase"; def "to_uppercase"; def "trim";
6262
def "int_to_string"; def "float_to_string"; def "string_length";

lib/typecheck.ml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,13 @@ let register_builtins (ctx : context) : unit =
12201220
sc_body = body_of tv }
12211221
in
12221222
bind_scheme ctx "len" (poly1 (fun a -> TArrow (a, QOmega, ty_int, EPure)));
1223+
(* slice : a -> Int -> Int -> a (issue #135 slice 2; arrays and strings,
1224+
mirroring `len`'s permissive `a`). Curried like other builtin schemes. *)
1225+
bind_scheme ctx "slice"
1226+
(poly1 (fun a ->
1227+
TArrow (a, QOmega,
1228+
TArrow (ty_int, QOmega,
1229+
TArrow (ty_int, QOmega, a, EPure), EPure), EPure)));
12231230
(* Honest string/char primitives underpinning stdlib/string.affine
12241231
(issue #122 v2.5). Concrete String/Char types; the Deno-ESM backend
12251232
lowers each to a JS intrinsic. char ::= TCon "Char". *)

test/test_e2e.ml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3318,6 +3318,19 @@ let test_fn_lambda_typed_block () =
33183318
(parse_check_passes
33193319
{|fn use_it() -> Int { let g = fn(x: Int) -> Int { x }; return g(2); }|})
33203320

3321+
(* Issue #135 slice 2: slice/range index `e[a:b]` / `e[a:]` / `e[:b]` / `e[:]`
3322+
desugars to the `slice` builtin. Used by option.affine (`list[1:]`) and
3323+
collections.affine. Plain `e[i]` indexing must be unaffected. *)
3324+
let test_slice_full_range () =
3325+
Alcotest.(check bool) "xs[1:3] / xs[1:] / xs[:2] / xs[:] parse + typecheck" true
3326+
(parse_check_passes
3327+
{|fn s(xs: [Int]) -> [Int] { let a = xs[1:3]; let b = xs[1:]; let c = xs[:2]; return xs[:]; }|})
3328+
3329+
let test_slice_index_not_regressed () =
3330+
Alcotest.(check bool) "plain xs[0] index still parses + typechecks" true
3331+
(parse_check_passes
3332+
{|fn idx(xs: [Int]) -> Int { return xs[0]; }|})
3333+
33213334
let test_multi_arg_arrow () =
33223335
Alcotest.(check bool) "(A, B) -> C parses + typechecks" true
33233336
(parse_check_passes
@@ -3368,6 +3381,8 @@ let type_syntax_sugar_tests = [
33683381
Alcotest.test_case "fn(x) => x (#135 fn-lambda expr)" `Quick test_fn_lambda_arrow_expr;
33693382
Alcotest.test_case "fn(x) => e as arg (#135 fn-lambda)" `Quick test_fn_lambda_higher_order;
33703383
Alcotest.test_case "fn(x:Int) -> Int { } (#135 fn-lambda)" `Quick test_fn_lambda_typed_block;
3384+
Alcotest.test_case "xs[a:b]/[a:]/[:b]/[:] (#135 slice 2)" `Quick test_slice_full_range;
3385+
Alcotest.test_case "xs[0] index non-regressed (#135 sl.2)" `Quick test_slice_index_not_regressed;
33713386
Alcotest.test_case "(A, B) -> C (multi-arg arrow)" `Quick test_multi_arg_arrow;
33723387
Alcotest.test_case "(A, B) without arrow remains tuple" `Quick test_tuple_type_still_works;
33733388
]

0 commit comments

Comments
 (0)