From 0c7a53f5263effd49789653169d229fcb4646ac9 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 17:11:15 +0700 Subject: [PATCH] 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. --- lib/indent_reader.ml | 78 +++++++++++++++++++++++++++++++++++++++++++- spec-syntax.md | 13 +++++--- test/test_syntax.ml | 21 ++++++++++-- 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index b7dfc7e7..092df315 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -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 diff --git a/spec-syntax.md b/spec-syntax.md index 6b9dab1a..4299e0e1 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -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) diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 3f9711c8..4b69b8c3 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -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 |>";