A pipe into a language word is refused, and a pipe the next operator or an as swallows is refused with the bracketed form as the fix.

This commit is contained in:
Joseph Ferano 2026-09-26 17:11:15 +07:00
parent 5a3d480edf
commit 0c7a53f526
3 changed files with 104 additions and 8 deletions

View File

@ -63,6 +63,16 @@ let binops =
their own. *)
("??", 0); ("|>", 0) ]
(* The words a statement, clause or declaration starts with, which name no
function and so are refused on the right of [|>]. *)
let pipe_words =
[ "fn"; "fn-"; "def"; "once"; "const"; "struct"; "union"; "data"; "enum";
"import"; "if"; "when"; "while"; "until"; "match"; "let"; "for"; "macro";
"class"; "generic"; "multi"; "method"; "type"; "return"; "break";
"continue"; "defer"; "handler-case"; "handler-bind"; "restart-case";
"quote"; "then"; "else"; "elif"; "as"; "in"; "where"; "and"; "or"; "on";
"restart" ]
let binop_level s = List.assoc_opt s binops
let is_binop s = binop_level s <> None
@ -1088,6 +1098,14 @@ and binary p lvl : Form.t * int =
side: x |> f(a)";
let i0 = p.i in
let t0 = p.toks.(i0) in
(match t0.tok with
| NAME w when List.mem w pipe_words ->
failk "pipe-keyword" t0.loc
"%s is a word of the language, not a function, so it cannot be \
the right side of |>. The right side is a function name or a \
call: x |> f(a)"
w
| _ -> ());
let rhs, rl = binary_ p 1 in
let head_at (h : Form.t) =
h.loc.Loc.line = t0.loc.Loc.line && h.loc.Loc.col = t0.loc.Loc.col
@ -1104,8 +1122,48 @@ and binary p lvl : Form.t * int =
Some (h, args)
| _ -> None
in
(* [a |> f ?? d] reads [a |> (f ?? d)]: the pipe is loosest. When the
leftmost operand of the right side is a target, the likely meaning
is the pipe's result under the operator, so the refusal says how
to write that. *)
(* An operator's form takes its first operand's location, so the
head's name is matched against the text too. *)
let target_at (f : Form.t) =
let starts s pre =
String.length s >= String.length pre
&& String.sub s 0 (String.length pre) = pre
in
match f.v, t0.tok with
| Form.Sym s, NAME n -> head_at f && s = n && text_of f = n
| Form.List (({ v = Form.Sym s; _ } as h) :: _), NAME n ->
head_at h && s = n && starts (text_of f) (n ^ "(")
| _ -> false
in
let rec leftmost (f : Form.t) =
if target_at f then Some f
else match f.v with
| Form.List (_ :: a :: _) -> leftmost a
| _ -> None
in
let bracketed () =
match leftmost rhs with
| None -> None
| Some f ->
let rt = text_of rhs and ft = text_of f in
let n = String.length ft in
if String.length rt > n && String.sub rt 0 n = ft then
Some (Printf.sprintf "(%s |> %s)%s" (text_of x) ft
(String.sub rt n (String.length rt - n)))
else None
in
(match call with
| Some (h, args) -> more (mk p l0 (Form.List (h :: x :: args)))
| None when bracketed () <> None ->
failk "pipe-target" rhs.loc
"|> takes the whole expression on its right, so this is %s |> (%s), \
and %s is not a function name or a call. To use the pipe's \
result there, put the pipe in parentheses:\n\n %s"
(text_of x) (text_of rhs) (text_of rhs) (Option.get (bracketed ()))
| None ->
failk "pipe-target" rhs.loc
"the right side of |> is a function name or a call, and %s is \
@ -1500,9 +1558,27 @@ and as_head p (c : Form.t) =
in
Some (Form.make (Form.Vec [ g; e ]) c.loc)
| _ ->
(* [a |> f? as g] would test [f] alone: a compound test is bracketed. *)
let t = text_of c in
let compound =
let depth = ref 0 and quoted = ref false and hit = ref false in
String.iteri
(fun i ch ->
if !quoted then
(if ch = '"' && (i = 0 || t.[i - 1] <> '\\') then quoted := false)
else
match ch with
| '"' -> quoted := true
| '(' | '[' | '{' -> incr depth
| ')' | ']' | '}' -> decr depth
| ' ' when !depth = 0 -> hit := true
| _ -> ())
t;
!hit
in
failk "as-test" at.loc
"as names what a test found, and %s is not one. Write %s? as name"
(text_of c) (text_of c))
t (if compound then "(" ^ t ^ ")" else t))
| _ -> None
(* The if an [if let] head was read into, rewritten to (if-let [P v] then

View File

@ -176,11 +176,16 @@ Each item: the proposal, then the reason in one line.
It is the loosest operator, so each side is a whole expression:
`a + 1 |> f()` is `f(a + 1)`, `a ?? b |> f()` is `f(a ?? b)`,
`a == b |> f()` is `f(a == b)`, `not x |> f()` is `f(not x)` and
`a or b |> f()` is `f(a or b)`. The left side is evaluated once, before
the call's other arguments, as the first argument of any call is. The
`a or b |> f()` is `f(a or b)`. When the right side names a function, the
left side is evaluated once, before the call's other arguments. A macro
receives the left side unevaluated as its first argument, as it would in
the written call: `x |> set(5)` is `set(x, 5)`, which assigns to `x`. The
right side is a name or a call to one; anything else — `x |> 3`,
`x |> a + b`, `x |> a.b`, `x |> f(a).x`, `x |> f(a)(b)` — is refused. A
pipe line under a statement is indented past it:
`x |> a + b`, `x |> a.b`, `x |> f(a).x`, `x |> f(a)(b)`, a word such as
`if`, `let` or `fn` — is refused. Since the pipe binds loosest,
`a |> f ?? d` is `a |> (f ?? d)` and is refused with the bracketed form
`(a |> f) ?? d` as the fix; likewise a test, `(a |> f)? as v`. A pipe line
under a statement is indented past it:
```
let cell = get(grid, r, c)

View File

@ -1441,12 +1441,27 @@ let () =
reads "|> into a call with a block" "xs |> each(1):\n print(2)" "(each xs 1 (print 2))";
reads "|>( is a call" "y = |>(a, b)" "(set y (|> a b))";
refuses "|> into a number" "y = x |> 3" "indent/pipe-target" "3 is neither";
refuses "|> into arithmetic" "y = x |> a + b" "indent/pipe-target" "a + b is neither";
refuses "|> into a field" "y = x |> a.b" "indent/pipe-target" "a.b is neither";
refuses "|> into arithmetic" "y = x |> a + b" "indent/pipe-target" "\n (x |> a) + b";
refuses "|> into a field" "y = x |> a.b" "indent/pipe-target" "(x |> a).b";
refuses "|> into a field of a call" "y = x |> f(a).x" "indent/pipe-target" "f(a).x";
refuses "|> into a call's result" "y = x |> f(a)(b)" "indent/pipe-target" "f(a)(b)";
refuses "|> into an index" "y = x |> a[1]" "indent/pipe-target" "a[1]";
refuses "|> into a lambda" "y = x |> fn(a) => g(a)" "indent/pipe-target" "is neither";
refuses "|> into a lambda" "y = x |> fn(a) => g(a)" "indent/pipe-keyword" "fn is a word";
refuses "|> into if" "y = 1 |> if" "indent/pipe-keyword" "if is a word";
refuses "|> into let" "y = 1 |> let" "indent/pipe-keyword" "let is a word";
refuses "|> into fn" "y = 1 |> fn" "indent/pipe-keyword" "fn is a word";
reads "|> into not" "y = x |> not" "(set y (not x))";
refuses "|> before ??" "y = a |> f ?? d" "indent/pipe-target" "\n (a |> f) ?? d";
refuses "|> before or" "y = a |> f or c" "indent/pipe-target" "\n (a |> f) or c";
refuses "|> before a test" "y = a |> f?" "indent/pipe-target" "\n (a |> f)?";
refuses "|> chained before ==" "y = a |> b |> f(1) == 2" "indent/pipe-target"
"\n (a |> b |> f(1)) == 2";
refuses "|> before as" "if a |> f(1) as v\n v" "indent/as-test" "Write (a |> f(1))? as name";
refuses "|> before as over a pattern" "if a |> f as Some(v)\n v" "indent/as-test"
"Write (a |> f)? as name";
refuses "a call before as is not bracketed" "if f(x) as v\n v" "indent/as-test"
"Write f(x)? as name";
reads "|> into a macro passes the place" "y = x |> set(5)" "(set y (set x 5))";
refuses "|> into parentheses" "y = x |> (f)" "indent/pipe-target" "(f) is neither";
refuses "|> unspaced on the right" "y = x |>[f]" "indent/unspaced-operator" "x |> f(a)";
refuses "|> at a statement's column" "f(x)\n|> g()" "indent/continuation" "starts with the operator |>";