.fln has a pipe, x |> f(a), that passes x as the first argument.

This commit is contained in:
Joseph Ferano 2026-09-26 17:15:09 +07:00
commit 21c2e78808
9 changed files with 324 additions and 9 deletions

View File

@ -15,6 +15,11 @@ Found porting the prelude (2026-09-26): =declare= has no statement form; a quasi
in the middle of an expression needs =quasiquote(...)=; =if not x= plus a block reads as
=when=, so a template wanting =(if c (do …))= keeps call syntax; a spliced let binding
list needs =let([…]):=. Each needs a .fln spelling now that .fln is the only syntax.
** DONE x |> f(a) is f(x, a)
CLOSED: [2026-09-26]
Decided (137): the pipe feeds the first argument, as Elixir's does, since Flan puts the
thing operated on first. Rules out F#'s last-argument pipe. Below =or=; the right side is
a name or a call to one, and the reader writes the plain call.
** CANCELLED rand-int returns i64
Decided 2026-09-26 (134): rand-int stays u64; write rand-int-range(lo, hi) for a signed
range.

View File

@ -77,7 +77,7 @@ fine here. Brackets and strings are still paired."
;; that starts with one, or follows a line that ends with one, continues the
;; line above.
(defconst flan-fln--binops
'("or" "and" "==" "!=" "??" "<" "<=" ">" ">=" "||" "^^" "&&" "<<" ">>" "+" "-"
'("|>" "or" "and" "==" "!=" "??" "<" "<=" ">" ">=" "||" "^^" "&&" "<<" ">>" "+" "-"
"*" "/" "%"))
(defconst flan-fln--binop-re (regexp-opt flan-fln--binops))
@ -1492,12 +1492,36 @@ lambda's block: it ends that block, and goes to the opening line's column."
((save-excursion (back-to-indentation) (looking-at flan-fln--clause-re))
(or (flan-fln--clause-columns (match-string-no-properties 1) (point))
(flan-fln--block-levels (point))))
((flan-fln--starts-with-pipe-p (point))
(flan-fln--pipe-columns prev))
((or (flan-fln--starts-with-op-p (point))
(flan-fln--ends-in-op-p prev))
(list (+ (flan-fln--indent-at (flan-fln--logical-start prev))
flan-fln-indent-offset)))
(t (flan-fln--block-levels (point))))))))))
(defun flan-fln--starts-with-pipe-p (pos)
"Non-nil if POS's line starts with the pipe `|>'."
(save-excursion
(goto-char pos)
(back-to-indentation)
(looking-at "|>\\(?:[ \t]\\|$\\)")))
(defun flan-fln--pipe-columns (prev)
"Columns for a line starting with `|>' whose code line above is PREV.
Under the pipe above it when PREV starts with one. Else under the value
PREV's statement binds -- `let t = x', then `|> f()' under the `x' -- and
one level in from the statement."
(if (flan-fln--starts-with-pipe-p prev)
(list (flan-fln--indent-at prev))
(let* ((start (flan-fln--logical-start prev))
(in (+ (flan-fln--indent-at start) flan-fln-indent-offset))
(v (flan-fln--value-start start))
(vcol (and v
(< v (save-excursion (goto-char start) (line-end-position)))
(save-excursion (goto-char v) (current-column)))))
(if (and vcol (> vcol in)) (list vcol in) (list in)))))
(defun flan-fln-indent-line ()
"Indent the line to a block column.
A line with text at a valid column stays there: its column is its meaning,
@ -1891,6 +1915,8 @@ lambda or a `Fn(...)' type, and not after a match arm's."
;; The optional marks: `x ?? d', the unwrap of `x!' and the `?' of a
;; chain, `a?.b' and `a?[i]'. A type's `T?' stays the type's.
("[ \t]\\(\\?\\?\\)[ \t]" 1 font-lock-keyword-face)
;; The pipe, `x |> f(a)', inside a line or starting one.
("\\(?:^[ \t]*\\|[ \t]\\)\\(|>\\)\\(?:[ \t]\\|$\\)" 1 font-lock-keyword-face)
("[])[:alnum:]_]\\(!\\)\\(?:[^=]\\|$\\)" 1 font-lock-keyword-face)
("[])[:alnum:]_]\\(\\?\\)[.[]" 1 font-lock-keyword-face)
(,(concat "\\_<" (regexp-opt flan--constants t) "\\_>")

View File

@ -950,6 +950,31 @@ defconst(k, 3)
(forward-line 2)
(test-flan--check "a line starting with ?? continues"
(flan-fln--continuation-p (point))))
;; The pipe: a line starting with `|>' goes under the value the statement
;; binds, then under the pipe above it. The first `|' of each text is point.
(test-flan-fln--is "a first |> goes under the let's value"
(test-flan-fln--tabs "let total = get(g)\n||> f()" 1) 12)
(test-flan-fln--is "and a second TAB one level in from the let"
(test-flan-fln--tabs "let total = get(g)\n||> f()" 2) 2)
(with-temp-buffer
(insert "let total = get(g)\n |> f()\n|> g()")
(flan-fln-mode)
(flan-fln-indent-line)
(test-flan-fln--is "a |> under a |> lines up with it" (current-indentation) 12))
(test-flan-fln--is "a |> under a bare statement is one level in"
(test-flan-fln--tabs "fn f() -> ()\n get(g)\n||> f()" 1) 4)
(test-flan-fln--in "|let y = x |> f()\nz = a\n |> g\nw = b |>\n h\n"
(font-lock-ensure)
(test-flan-fln--is "a |> inside a line is marked"
(save-excursion (search-forward "x |>") (get-text-property (- (point) 2) 'face))
'font-lock-keyword-face)
(test-flan-fln--is "a |> starting a line is marked"
(save-excursion (search-forward " |>") (get-text-property (- (point) 2) 'face))
'font-lock-keyword-face)
(forward-line 2)
(test-flan--check "a line starting with |> continues" (flan-fln--continuation-p (point)))
(forward-line 2)
(test-flan--check "a line after a trailing |> continues it" (flan-fln--continuation-p (point))))
(test-flan-fln--is "else goes to its if's column, whatever the depth"
(test-flan-fln--tabs "if a\n if b\n c\n |else" 1) 2)
(test-flan-fln--is "and a second TAB to the outer if's"

View File

@ -479,7 +479,7 @@ and list f h args =
((if t <> "" && t.[0] = '~' then "~(" ^ t ^ ")" else "~" ^ t), 13)
| Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 13 x, 13)
| Form.Sym s, _ :: _ :: _
when R.is_binop (infix_op s) && s <> "==" && s <> "??"
when R.is_binop (infix_op s) && s <> "==" && s <> "??" && s <> "|>"
&& not (s = "!=" && List.length args > 2) ->
let op = infix_op s in
let lvl = Option.get (R.binop_level op) in

View File

@ -57,10 +57,21 @@ let binops =
("==", 4); ("!=", 4); ("<", 4); ("<=", 4); (">", 4); (">=", 4);
("||", 5); ("^^", 6); ("&&", 7);
("<<", 8); (">>", 8); ("+", 9); ("-", 9); ("*", 10); ("/", 10); ("%", 10);
(* [??] is here for the line rules — a line ending in it, or one starting
with it, continues — and is read by [operand] between 4 and 5, so no
level of [binary]'s is its own. *)
("??", 0) ]
(* [??] and [|>] are here for the line rules — a line ending in one, or
starting with one, continues. [operand] reads [??] between 4 and 5,
and [binary] reads [|>] below [or]; no level of [binary_]'s climb is
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
@ -1066,7 +1077,107 @@ let and_rewrite (xs : Form.t list) ops =
separated only by whitespace. *)
let rec expr p : Form.t * int = binary p 1
(* [x |> f(a)] is [f(x, a)] and [x |> f] is [f(x)] (decision 137): the value
is the first argument, as Flan puts the thing operated on first. It is
below [or], so each side is a whole level-1 expression, and it reads to
the plain call, so nothing after the reader knows a pipe was written.
The right side is a name or a call to one and nothing else: those are the
only shapes the first-argument rewrite has one meaning for. *)
and binary p lvl : Form.t * int =
if lvl <> 1 then binary_ p lvl
else
let l0 = (peek p).loc in
let ((first, _) as fp) = binary_ p 1 in
let rec more (x : Form.t) =
match (peek p).tok with
| NAME "|>" when not ((peek_at p 1).tok = LP && not (peek_at p 1).sp) ->
let ot = advance p in
if not (ot.sp && (peek p).sp) then
failk "unspaced-operator" ot.loc
"|> is an operator here, and a binary operator has a space on each \
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
in
let glued_paren () =
i0 + 1 < Array.length p.toks
&& p.toks.(i0 + 1).tok = LP && not p.toks.(i0 + 1).sp
in
let call =
match t0.tok, rhs.v with
| NAME _, Form.Sym _ when rl = 13 && p.i = i0 + 1 -> Some (rhs, [])
| NAME _, Form.List (({ v = Form.Sym _; _ } as h) :: args)
when rl = 12 && head_at h && (last p).tok = RP && glued_paren () ->
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 \
neither. The value on the left becomes the first argument:\n\n\
\ x |> f(a, b) is f(x, a, b)\n\
\ x |> f is f(x)"
(if t0.tok = LP && rl = 13 then "(" ^ text_of rhs ^ ")"
else text_of rhs))
| _ -> x
in
let r = more first in
if r == first then fp else (r, 1)
and binary_ p lvl : Form.t * int =
if lvl = 3 then not_ p
else if lvl > 10 then unary p
else
@ -1447,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

@ -122,7 +122,7 @@ Each item: the proposal, then the reason in one line.
a lambda's block there, laid out as at the top level against the column its
line starts at, and the block ends where the enclosing bracket closes.*
- **Continuation outside brackets:** a line that starts with a spaced infix
operator (`+`, `and`, `==`, …) continues the previous line; so does a line
operator (`+`, `and`, `==`, `|>`, …) continues the previous line; so does a line
after one that ends in a spaced infix operator. (F# `LexFilter.fs` 360-380,
1850-1870, 2345-2360.) No `\` continuation. **Built** (`=` does not
continue: `let x =` plus a block is a block value). A continuation line must
@ -163,11 +163,38 @@ Each item: the proposal, then the reason in one line.
### Expressions
- **Precedence**, low to high: `or` < `and` < `not` < comparisons
- **Precedence**, low to high: `|>` < `or` < `and` < `not` < comparisons
(`== != < <= > >=`) < `??` < `||` < `^^` < `&&` < `<< >>` < `+ -` < `* / %` <
prefix `-` and `~~` < postfix (call, index, field, `!`, `?`, `?.`). **Built.** An operator
glued to `(` is always a call. The bit operators sit where Python and Rust
put them, so `x && mask == 0` is `(x && mask) == 0`.
- **The pipe** (decision 137): `x |> f(a, b)` reads `(f x a b)`; `x |> f()`
and `x |> f` read `(f x)`. The value on the left is the first argument. A
chain runs left to right: `get(grid, r, c) |> or-else(empty) |> is-empty-cell()`
reads `(is-empty-cell (or-else (get grid r c) empty))`. The name may be
qualified, `x |> m/f(a)`, and the call may take a block, `xs |> each():`.
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)`. 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)`, 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)
|> or-else(empty)
|> is-empty-cell()
```
The reader writes the plain call, so nothing after it knows a pipe was
written. `|>(a, b)`, glued, is a call to a function named `|>`. **Built.**
- **The bit operators** are `a && b`, `a || b`, `a ^^ b` and `~~a`, reading
`(bit-and a b)`, `(bit-or a b)`, `(bit-xor a b)` and `(bit-not a)`. They take
integers; `and`, `or` and `not` are the logical ones. `~~` is one token, so a

42
test/programs/pipe.fln Normal file
View File

@ -0,0 +1,42 @@
;; The pipe x |> f(a) is f(x, a): the value on the left is the first argument,
;; evaluated before the others. Typed, dyn, chained, multi-line and qualified.
import shape "pkgs/shape"
import area "pkgs/area"
fn add(a: i32, b: i32) -> i32 = a + b
fn scale(a: i32, k: i32) -> i32 = a * k
fn twice(a: i32) -> i32 = a * 2
fn sub3(a: i32, b: i32, c: i32) -> i32 = a - b - c
;; Prints its argument, to show the order the arguments run in.
fn note(n: i32) -> i32
println(n)
n
fn held-or(o: i32?, d: i32) -> i32 = o ?? d
fn bump(x) = x + 1
fn main()
println(3 |> add(4))
println(3 |> twice)
println(3 |> twice())
println(1 |> add(2) |> scale(10) |> twice)
;; Below or and the arithmetic: the whole left side is piped.
println(2 + 3 |> scale(2))
let n: i32? = None
println(n ?? 4 |> twice)
println(1 == 1 |> not)
let total = 10
|> add(5)
|> scale(2)
println(total)
let other = 10 |>
sub3(1, 2)
println(other)
println(Some(7) |> held-or(0), n |> held-or(-1))
println(shape/box(3, 2) |> area/of)
println(5 |> bump |> bump)
;; The left side runs first, then the other arguments in order: 1 2 3.
println(note(1) |> sub3(note(2), note(3)))

View File

@ -2276,6 +2276,12 @@ let () =
[ ("optionals.fln", optionals_out); ("optionals-dyn.fln", optionals_dyn_out);
(* x? tests and narrows, e? as g names what it found (decision 133). *)
("presence.fln", "true false true\n6\n-1\n3\n101 209 0\n11\n42\n2\nabsent\n6\nfalse true\n3\n6\n15\n"); ("presence-dyn.fln", "true false\n103 209 0\nno pet\nann\n3 2\n") ];
(* The pipe (decision 137): chains, multi-line, qualified, dyn, and the
left side run before the other arguments (the last 1 2 3). *)
(let want = "7\n6\n6\n60\n10\n8\nfalse\n30\n7\n7 -1\n6\n7\n1\n2\n3\n-4\n" in
outputs "pipe.fln" "programs/pipe.fln" want;
outputs ~opt:"-O0" "pipe.fln, -O0" "programs/pipe.fln" want;
outputs ~x86:true "pipe.fln, --x86" "programs/pipe.fln" want);
(* x! over nothing traps at its site and names the expression. *)
List.iter
(fun (x86, arg, want) ->

View File

@ -1417,6 +1417,61 @@ let () =
reads "while e? as g" "while pop(s)? as x\n f(x)"
"(while true (if-let [x (pop s)] (do (f x)) (break)))";
refuses "as after no test" "if x as y\n y" "indent/as-test" "Write x? as name";
(* Decision 137: x |> f(a) is f(x, a), below or. *)
reads "|> into a call" "y = x |> f(1, 2)" "(set y (f x 1 2))";
reads "|> into an empty call" "y = x |> f()" "(set y (f x))";
reads "|> into a name" "y = x |> f" "(set y (f x))";
reads "|> into a case" "y = x |> Some" "(set y (Some x))";
reads "|> chains left to right"
"y = get(grid, r, c) |> or-else(empty) |> is-empty-cell()"
"(set y (is-empty-cell (or-else (get grid r c) empty)))";
reads "|> into a qualified name" "y = x |> m/f(a) |> m/g" "(set y (m/g (m/f x a)))";
reads "|> is below arithmetic" "y = a + 1 |> f()" "(set y (f (+ a 1)))";
reads "|> is below ??" "y = a ?? b |> f()" "(set y (f (?? a b)))";
reads "|> is below comparisons" "y = a == b |> f()" "(set y (f (= a b)))";
reads "|> is below not" "y = not x |> f()" "(set y (f (not x)))";
reads "|> is below or" "y = a or b and c |> f()" "(set y (f (or a (and b c))))";
reads "|> in an argument" "g(x |> f, 2)" "(g (f x) 2)";
reads "|> in a condition" "if x |> is-empty() then 1 else 2" "(if (is-empty x) 1 2)";
reads "|> starting lines" "let y = get(g, r)\n |> or-else(0)\n |> f()\ny"
"(def y dyn (f (or-else (get g r) 0)))\ny";
reads "|> ending a line" "let y = get(g, r) |>\n or-else(0)\ny"
"(def y dyn (or-else (get g r) 0))\ny";
reads "|> under a statement" "x\n |> f(1)\n |> g" "(g (f x 1))";
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" "\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-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 |>";
refuses "|> in a list separated by spaces" "y = [x |> f() 2]" "indent/separate-elements" "commas";
(match read "y = |>(a, b)" with
| forms ->
let got = Indent_printer.program ~source:"y = |>(a, b)" forms in
if not (Test_support.contains got "|>(a, b)") then
fail "a call to |> prints as a call: %S" got
| exception e -> fail "a call to |> prints as a call: %s" (diag_text e));
refused "present-i32.fln" "fn main()\n let x = 5\n println(x?)\n"
[ "x is i32, which always holds a value, so x? has nothing to test";
"a yes-or-no name starts with is- or has-, as in is-x" ];