From f91ed8ad271e178b45381cb6dfab131da77cd87d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 06:19:59 +0700 Subject: [PATCH] Indices in .fln separate like a vector's elements, by spaces between single values or by commas --- TODO.org | 5 --- lib/indent_reader.ml | 75 +++++++++++++++++++++++++++++++++++++++----- spec-syntax.md | 4 +++ test/test_syntax.ml | 21 +++++++++++-- 4 files changed, 91 insertions(+), 14 deletions(-) diff --git a/TODO.org b/TODO.org index a9432e15..8633ec77 100644 --- a/TODO.org +++ b/TODO.org @@ -648,11 +648,6 @@ CLOSED: [2026-09-26] A block lambda inside brackets is the last thing in them, its block ending where they close: a comma after it (so a second block lambda, or one not last) is refused, and the fix names it with ~let~. -** NEXT Indices separate like vector elements -Decided 2026-09-26: ~grid[r c]~ and ~grid[(r + 1) (c - 1)]~ read like a vector's -space-separated single values; commas always work; an index with a bare operator -needs them, ~grid[r + 1, c]~. - ** WAIT Calls without parentheses Held 2026-09-26 by the author: F#-style ~f x y~ or Nim-style one-argument calls without parentheses. Collides with space-separated vector and index elements. diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index b703d7f4..c6c8d58d 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -743,7 +743,7 @@ and postfix p = loop (mk p l0 (Form.List (f :: args)), 9) | LB -> ignore (advance p); - let idx = items p RB t.loc ~what:"indices" ~head:(text_of f) in + let idx = index_items p t.loc ~head:(text_of f) in loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 9) | NAME s when String.length s > 1 && s.[0] = '.' -> ignore (advance p); @@ -1005,8 +1005,7 @@ and lambda_params args = (* Comma-separated values up to [closer]. [const T] is two elements without a comma, for [Ptr(const u8)]: const is a reserved word in a type and never a value. *) -(* [head] is the text of what is indexed, for [[ ]]'s message. *) -and items ?head p closer open_loc ~what = +and items p closer open_loc ~what = let opener = if closer = RB then '[' else '(' in let rec go acc = let t = peek p in @@ -1037,15 +1036,77 @@ and items ?head p closer open_loc ~what = else if starts_value n.tok && n.sp && not (negative_literal n.tok) then failk "missing-comma" n.loc "%s follows %s with no comma between them. Separate %s with \ - commas: %s" + commas: f(a, b)" (show n.tok) (text_of e) what - (match head with - | Some h -> Printf.sprintf "%s[%s, %s]" h (text_of e) (show n.tok) - | None -> "f(a, b)") else stray p ~after:(text_of e)) in go [] +(* An index's values, [grid[r c]] or [grid[r + 1, c]]: separated as a + vector's elements are, by commas or, between single values only, by + spaces. The whole list is read before a mistake is named, so the fix can + be the index as written, commas put in. [head] is the text of what is + indexed. *) +and index_items p open_loc ~head = + let rec go acc = + let t = peek p in + match t.tok with + | RB -> ignore (advance p); List.rev acc + | EOF -> unclosed p '[' open_loc + | _ -> + let e, lvl = expr p in + (* The element as written, parentheses and all. *) + let src = text_of (Form.make (Form.Sym "") (span p t.loc)) in + let n = peek p in + match n.tok with + | COMMA -> ignore (advance p); go ((e, src, lvl, t.loc, Some n.loc) :: acc) + | RB -> ignore (advance p); List.rev ((e, src, lvl, t.loc, None) :: acc) + | EOF -> unclosed p '[' open_loc + (* [grid[i -1]]: most likely [i - 1] with its minus glued. *) + | (ATOM _ | NEG) when n.sp && (negative_literal n.tok || n.tok = NEG) -> + let x, _ = unary p in + let digits = + let s = text_of x in String.sub s 1 (String.length s - 1) + in + failk "glued-minus" n.loc + "%s is read as the value %s, right after %s with nothing between \ + the minus and it. To subtract, space the minus: %s[%s - %s]. For \ + two indices, separate them with a comma: %s[%s, %s]" + (text_of x) (text_of x) src head src digits head src (text_of x) + | tk when starts_value tk && n.sp -> go ((e, src, lvl, t.loc, None) :: acc) + | _ -> stray p ~after:(text_of e) + in + let xs = go [] in + let commas = List.exists (fun (_, _, _, _, c) -> c <> None) xs in + let spaces = + List.exists (fun (_, _, _, _, c) -> c = None) (match List.rev xs with _ :: r -> r | [] -> []) + in + let with_commas () = + Printf.sprintf "%s[%s]" head + (String.concat ", " (List.map (fun (_, s, _, _, _) -> s) xs)) + in + if commas && spaces then begin + let at = match List.find_opt (fun (_, _, _, _, c) -> c <> None) xs with + | Some (_, _, _, _, Some l) -> l | _ -> open_loc + in + failk "mixed-separators" at + "these indices are separated some with commas and some with only \ + spaces. Use one: %s%s" + (with_commas ()) + (if List.for_all (fun (_, _, l, _, _) -> l >= 8) xs then + Printf.sprintf " or %s[%s]" head + (String.concat " " (List.map (fun (_, s, _, _, _) -> s) xs)) + else "") + end; + (match List.find_opt (fun (_, _, l, _, _) -> l < 8) xs with + | Some (_, src, _, at, _) when spaces -> + failk "separate-elements" at + "%s has an operator in it and sits among indices separated by spaces, \ + where only single values are. Separate the indices with commas: %s" + src (with_commas ()) + | _ -> ()); + List.map (fun (e, _, _, _, _) -> e) xs + (* [[a b c]] or [[a, b + 1]]: whitespace separates only single terms. *) and vec_items p open_loc = (* One separator per bracket: [1 2, 3] mixes them, and which elements the diff --git a/spec-syntax.md b/spec-syntax.md index 06b1bc75..f047f23a 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -145,6 +145,10 @@ Each item: the proposal, then the reason in one line. the Lisp look for data and is refusable by shape. **Built** (in braces a value may have an operator in it, `{.x a + 1, .y 2}`; the comma after it is what is required). +- **Indices separate the same way.** `grid[r c]` and `grid[(r + 1) (c - 1)]` + are two indices each; `grid[r + 1, c]` needs its comma, and `grid[r + 1 c]` + is refused with the commas put in as the fix. `grid[i -1]` is refused as a + glued minus. **Built**; the printer writes indices with commas. - **Struct literal:** `Vector2{.x 1, .y 2}` (brace glued to the name) reads `(Vector2 {.x 1 .y 2})`. A bare `{.x 1}` is today's bare literal. `{:a 1}` is a dyn map. **Built.** diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 3133f094..1d5c7607 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -693,8 +693,25 @@ let () = "indent/lambda-block-left" "Indent it into the block"; refuses "a block lambda's brackets left open" "f(fn(a) =>\n a\n" "indent/unclosed" "ends where this bracket closes"; - refuses "indices with no comma" "x = grid[row col].color-idx" - "indent/missing-comma" "Separate indices with commas: grid[row, col]"; + (* Indices separate as a vector's elements do. *) + reads "indices separated by spaces" "x = grid[row col].color-idx" + "(set x (.color-idx (at grid row col)))"; + reads "grouped indices separated by spaces" "x = grid[(r + 1) (c - 1)]" + "(set x (at grid (+ r 1) (- c 1)))"; + reads "indices separated by commas" "x = grid[r + 1, c]" "(set x (at grid (+ r 1) c))"; + reads "calls and fields as spaced indices" "x = grid[f(r) p.y]" "(set x (at grid (f r) (.y p)))"; + reads "a negative literal first" "x = grid[-1 c]" "(set x (at grid -1 c))"; + reads "a spaced index assigned" "grid[r c] = 1" "(set (at grid r c) 1)"; + refuses "an operator among spaced indices" "x = grid[r + 1 c]" + "indent/separate-elements" "Separate the indices with commas: grid[r + 1, c]"; + refuses "an operator last among spaced indices" "x = grid[r c - 1]" + "indent/separate-elements" "grid[r, c - 1]"; + refuses "mixed index separators" "x = grid[r c, d]" + "indent/mixed-separators" "Use one: grid[r, c, d] or grid[r c d]"; + refuses "a glued minus among indices" "x = grid[i -1]" + "indent/glued-minus" "grid[i - 1]"; + refuses "a glued negation among indices" "x = grid[i -j]" + "indent/glued-minus" "grid[i, -j]"; refuses "arguments with no comma" "x = f(a b)" "indent/missing-comma" "Separate arguments with commas: f(a, b)"; refuses "a body glued to =>" "x = fn(a) =>a"