A one-line if takes its else on the next line, lambdas state their types, a restart's report sits on its header, Dir.north names an enum member, messages spell types in the code's syntax, adjacent globals convert adjacent, and a bad map key is one error
This commit is contained in:
parent
dbddc61b30
commit
fd1a7f2b36
53
TODO.org
53
TODO.org
@ -628,51 +628,32 @@ awaiting confirmation, and the build order. Rules out Parinfer, wisp and
|
||||
sweet-expressions, and a simplified in-paren syntax — all thin the parens
|
||||
without removing them.
|
||||
|
||||
** TODO A one-line if cannot take its else on the next line
|
||||
=if c then a= then =else b= under it is refused. Proposal: an =else=/=elif= at
|
||||
the if's column continues a one-line if, as F#'s does.
|
||||
|
||||
** TODO A lambda with a block cannot be a call's argument
|
||||
=sort-by(xs, fn(a, b)= plus a block is refused; the lambda has to be bound first,
|
||||
with its type written. Proposal: a call ending in =fn(...)= and a trailing =:=
|
||||
** WAIT A lambda with a block cannot be a call's argument
|
||||
On the author's decision. =sort-by(xs, fn(a, b)= plus a block is refused; the
|
||||
lambda has to be bound first with =let=. Proposal: a call ending in =fn(...)= and a trailing =:=
|
||||
hands the block to that lambda: =sort-by(xs, fn(a, b)):=.
|
||||
|
||||
** TODO A lambda's parameters cannot be typed
|
||||
=fn(a, b)= takes bare names, so a lambda bound by =let= needs
|
||||
=let f: Fn(C, C) -> bool = fn(a, b)=. Proposal: =fn(a: C, b: C)= as in a
|
||||
=fn= definition.
|
||||
|
||||
** TODO A condition struct with a parent has no sugar
|
||||
=defstruct(DiskFull, :parent, IoError, [free i64])= is the fallback, with a
|
||||
** WAIT A condition struct with a parent has no sugar
|
||||
On the author's decision. =defstruct(DiskFull, :parent, IoError, [free i64])= is the fallback, with a
|
||||
paren field vector. Proposal: =struct DiskFull :parent IoError= plus field lines.
|
||||
|
||||
** TODO defmacro has no sugar
|
||||
=defmacro(repeat, [i n & body]):= with a space-separated parameter vector.
|
||||
** WAIT defmacro has no sugar
|
||||
On the author's decision. =defmacro(repeat, [i n & body]):= with a space-separated parameter vector.
|
||||
Proposal: =macro repeat(i, n, & body)= plus a block.
|
||||
|
||||
** TODO loop/recur has no sugar
|
||||
=loop([x a y b]):=. Proposal: =loop x = a, y = b= plus a block; =recur(...)=
|
||||
** WAIT loop/recur has no sugar
|
||||
On the author's decision. =loop([x a y b]):=. Proposal: =loop x = a, y = b= plus a block; =recur(...)=
|
||||
stays a call.
|
||||
|
||||
** TODO A restart's report string is a body line
|
||||
=:report= and its string sit as two statements under =restart name()=.
|
||||
Proposal: =restart name() "report text"= on the header line.
|
||||
** TODO Hard-coded code in messages is still paren syntax in a .fln file
|
||||
Types follow the code's syntax now (=Types.spell=). Hints written into a message's
|
||||
text — =(Ptr %s)=, =(clone v)=, =(the T x)= in most of =check.ml= and =parse.ml=, the
|
||||
runtime's =(get 0 :body)= and =(/ x 0)= — still print parens; each is fixed per
|
||||
message as it is met.
|
||||
|
||||
** TODO An enum member has only a keyword spelling
|
||||
=:north= works, =Dir.north= is an unknown name, while a data case is
|
||||
=Shape.Rect=. Proposal: accept =Dir.north= as the member.
|
||||
|
||||
** TODO Checker and runtime messages print types and fixes in parens
|
||||
In a .fln file, =(Fn [A] R)=, =(Option i32)=, =(get 0 :body)=, =(/ x 0)= and most
|
||||
usage hints still print paren syntax; a handful now print .fln spellings.
|
||||
Proposal: =Types.to_string= and the hints take the syntax of the location's file.
|
||||
|
||||
** TODO flan convert separates adjacent one-line globals with blank lines
|
||||
=once a: i32= on consecutive lines come back one blank line apart.
|
||||
|
||||
** TODO A bad map key type is followed by unknown-name errors for its parts
|
||||
=map-new([const u8], i32)= reports the key, then "unknown name const" and
|
||||
"unknown name i32" — in both syntaxes.
|
||||
** CANCELLED Sugar for defclass, defgeneric, defmulti and defmethod
|
||||
CLOSED: [2026-09-26]
|
||||
The fallback, =defmethod(area, point, [p]):=, reads well enough.
|
||||
|
||||
** TODO The shims in sand.flan can go
|
||||
=sand.flan= defines =dyn->f64= and =dyn->u32=, one-line functions whose only job
|
||||
|
||||
387
lib/check.ml
387
lib/check.ml
File diff suppressed because it is too large
Load Diff
@ -363,7 +363,7 @@ and head_text (h : Form.t) =
|
||||
| Form.Sym s -> fst (sym h s)
|
||||
| _ -> at 9 h
|
||||
|
||||
and list _f h args =
|
||||
and list f h args =
|
||||
let call () = (head_text h ^ "(" ^ commas args ^ ")", 9) in
|
||||
match h.v, args with
|
||||
| Form.Sym "quote", [ x ] -> ("'" ^ Form.to_source x, 10)
|
||||
@ -418,6 +418,10 @@ and list _f h args =
|
||||
if glued then (tt ^ s, 9) else call ()
|
||||
| Form.Sym s, [ ({ v = Form.Map _; _ } as m) ] when name_ok s && R.capitalised s ->
|
||||
(s ^ fst (expr m), 9)
|
||||
| Form.Sym "the", _ when (match typed_lambda f with Some (_, [ _ ]) -> true | _ -> false) ->
|
||||
(match typed_lambda f with
|
||||
| Some (head, [ body ]) -> (head ^ " = " ^ unit_text body, 0)
|
||||
| _ -> assert false)
|
||||
| Form.Sym "fn", [ { v = Form.Vec ps; _ }; body ] when List.for_all sym_param ps ->
|
||||
("fn(" ^ commas ps ^ ") = " ^ unit_text body, 0)
|
||||
| Form.Sym "if", [ c; a; b ] ->
|
||||
@ -463,6 +467,26 @@ and assign_text ?(lvl = 0) t v =
|
||||
and sym_param (p : Form.t) =
|
||||
match p.v with Form.Sym s -> name_ok s | _ -> false
|
||||
|
||||
(* [(the (Fn [C dyn] R) (fn [a b] ...))] as [fn(a: C, b) -> R], the typed
|
||||
lambda it reads from; [None] for any other shape. *)
|
||||
and typed_lambda (f : Form.t) =
|
||||
let rec tyt (t : Form.t) =
|
||||
match t.v with
|
||||
| Form.List [ { v = Form.Sym (("Fn" | "CFn") as h); _ }; { v = Form.Vec ps; _ }; r ] ->
|
||||
h ^ "(" ^ String.concat ", " (List.map tyt ps) ^ ") -> " ^ tyt r
|
||||
| _ -> at 9 t
|
||||
in
|
||||
match f.v with
|
||||
| Form.List [ { v = Form.Sym "the"; _ };
|
||||
{ v = Form.List [ { v = Form.Sym "Fn"; _ }; { v = Form.Vec ts; _ }; r ]; _ };
|
||||
{ v = Form.List ({ v = Form.Sym "fn"; _ } :: { v = Form.Vec ps; _ } :: body); _ } ]
|
||||
when List.length ts = List.length ps && List.for_all sym_param ps && body <> [] ->
|
||||
let one (n : Form.t) (t : Form.t) =
|
||||
if is_sym "dyn" t then fst (expr n) else fst (expr n) ^ ": " ^ tyt t
|
||||
in
|
||||
Some ("fn(" ^ String.concat ", " (List.map2 one ps ts) ^ ") -> " ^ tyt r, body)
|
||||
| _ -> None
|
||||
|
||||
(* A type after [:] or [->]: the function-type arrow at the top, a postfix
|
||||
term below it. *)
|
||||
let rec ty (f : Form.t) =
|
||||
@ -750,6 +774,9 @@ and value_lines n prefix (v : Form.t) =
|
||||
else if n + String.length inline <= width then [ ind n ^ inline ]
|
||||
else
|
||||
match v.v with
|
||||
| _ when typed_lambda v <> None ->
|
||||
let head, body = Option.get (typed_lambda v) in
|
||||
[ ind n ^ prefix ^ " = " ^ head ] @ block (n + 2) body
|
||||
| Form.List ({ v = Form.Sym "fn"; _ } :: { v = Form.Vec ps; _ } :: (_ :: _ as body))
|
||||
when List.for_all sym_param ps ->
|
||||
[ ind n ^ prefix ^ " = fn(" ^ commas ps ^ ")" ] @ block (n + 2) body
|
||||
@ -894,8 +921,14 @@ and sugar n (f : Form.t) : string list option =
|
||||
match c.v with
|
||||
| Form.List ({ v = Form.Sym r; _ } :: { v = Form.Vec ps; _ } :: (_ :: _ as b))
|
||||
when def_name r ->
|
||||
let report, b =
|
||||
match b with
|
||||
| { v = Form.Kw "report"; _ } :: ({ v = Form.Str _; _ } as t) :: (_ :: _ as rest) ->
|
||||
(" " ^ fst (expr t), rest)
|
||||
| _ -> ("", b)
|
||||
in
|
||||
Option.map
|
||||
(fun pt -> (i ^ "restart " ^ r ^ "(" ^ pt ^ ")") :: block (n + 2) b)
|
||||
(fun pt -> (i ^ "restart " ^ r ^ "(" ^ pt ^ ")" ^ report) :: block (n + 2) b)
|
||||
(params_text ps)
|
||||
| _ -> None
|
||||
in
|
||||
@ -1026,7 +1059,8 @@ and let_lines n prs body =
|
||||
(* [(let [x (the T v)])] is [let x: T = v]. *)
|
||||
let bind ((t : Form.t), (v : Form.t)) =
|
||||
match t.v, v.v with
|
||||
| Form.Sym x, Form.List [ { v = Form.Sym "the"; _ }; ty_; w ] when def_name x ->
|
||||
| Form.Sym x, Form.List [ { v = Form.Sym "the"; _ }; ty_; w ]
|
||||
when def_name x && typed_lambda v = None ->
|
||||
("let " ^ x ^ ": " ^ ty ty_, w)
|
||||
| _ -> ("let " ^ guard (at 8 t), v)
|
||||
in
|
||||
@ -1063,11 +1097,11 @@ let program ?source ?macros:m (fs : Form.t list) : string =
|
||||
in
|
||||
let rec go = function
|
||||
| [] -> []
|
||||
| [ x ] -> [ top x ]
|
||||
| x :: rest -> top (if let_sugar x then in_do x else x) :: go rest
|
||||
| [ x ] -> [ (x, top x) ]
|
||||
| x :: rest -> (x, top (if let_sugar x then in_do x else x)) :: go rest
|
||||
in
|
||||
let text =
|
||||
try String.concat "\n\n" (go fs) ^ "\n"
|
||||
try Source_text.join_top (go fs) ^ "\n"
|
||||
with e -> spelling := (fun _ -> None); inside := (fun _ -> false); raise e
|
||||
in
|
||||
spelling := (fun _ -> None);
|
||||
|
||||
@ -386,6 +386,9 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar
|
||||
|
||||
type p = { toks : token array; mutable i : int }
|
||||
|
||||
(* Set below [params] and [ty], which the expression parser comes before. *)
|
||||
let typed_fn_expr : (p -> Form.t * int) ref = ref (fun _ -> assert false)
|
||||
|
||||
let peek p = p.toks.(p.i)
|
||||
let peek_at p k = p.toks.(min (p.i + k) (Array.length p.toks - 1))
|
||||
let advance p =
|
||||
@ -785,6 +788,7 @@ and inline_stmt p : Form.t =
|
||||
(* [fn(a, b) = body] is a lambda; [fn(...)] followed by anything else is the
|
||||
fallback call spelling of [(fn ...)]. *)
|
||||
and fn_expr p =
|
||||
if typed_lambda p then !typed_fn_expr p else
|
||||
let t = advance p in
|
||||
let lp = advance p in
|
||||
let args = items p RP lp.loc ~what:"parameters" in
|
||||
@ -801,6 +805,21 @@ and fn_expr p =
|
||||
0)
|
||||
| _ -> (mk p t.loc (Form.List (sym t.loc "fn" :: args)), 9)
|
||||
|
||||
(* Whether the [fn(] at point has a [:] among its parameters or a [->]
|
||||
after them: a lambda that states its types. *)
|
||||
and typed_lambda p =
|
||||
let rec go k depth =
|
||||
let t = peek_at p k in
|
||||
match t.tok with
|
||||
| EOF -> false
|
||||
| COLON when depth = 1 -> true
|
||||
| LP | LB | LC -> go (k + 1) (depth + 1)
|
||||
| RP when depth = 1 -> (peek_at p (k + 1)).tok = NAME "->"
|
||||
| RP | RB | RC -> go (k + 1) (depth - 1)
|
||||
| _ -> go (k + 1) depth
|
||||
in
|
||||
go 1 0
|
||||
|
||||
and span_of_list l args =
|
||||
match List.rev args with
|
||||
| [] -> l
|
||||
@ -1068,6 +1087,49 @@ let params p (lp : token) =
|
||||
in
|
||||
go []
|
||||
|
||||
(* [fn(a: C, b) -> R = body] is [(the (Fn [C dyn] R) (fn [a b] body))]: the
|
||||
paren [fn] takes its parameters' types from where it is written, and [the]
|
||||
is the form that says what a value is, as in [let x: T = v]. An untyped
|
||||
parameter is dyn, as in a definition, and the return type is required. A
|
||||
block body is added by [lambda_block]. *)
|
||||
let () = typed_fn_expr := fun p ->
|
||||
let t = advance p in
|
||||
let lp = advance p in
|
||||
let ps = params p lp in
|
||||
let rec split = function
|
||||
| n :: ty :: rest -> let ns, ts = split rest in (n :: ns, ty :: ts)
|
||||
| _ -> ([], [])
|
||||
in
|
||||
let names, tys = split ps in
|
||||
let r =
|
||||
match (peek p).tok with
|
||||
| NAME "->" -> ignore (advance p); ty p
|
||||
| _ ->
|
||||
failk "lambda-return" (where_ p)
|
||||
"a lambda that states its parameters' types states its return type \
|
||||
too: fn(%s) -> R = value"
|
||||
(String.concat ", "
|
||||
(List.map2 (fun n (ty : Form.t) ->
|
||||
if ty.v = Form.Sym "dyn" then text_of n else text_of n ^ ": " ^ text_of ty)
|
||||
names tys))
|
||||
in
|
||||
let fty = mk p lp.loc (Form.List [ sym t.loc "Fn"; Form.make (Form.Vec tys) lp.loc; r ]) in
|
||||
let vec = Form.make (Form.Vec names) lp.loc in
|
||||
let wrap body =
|
||||
mk p t.loc (Form.List [ sym t.loc "the"; fty;
|
||||
mk p t.loc (Form.List (sym t.loc "fn" :: vec :: body)) ])
|
||||
in
|
||||
match (peek p).tok with
|
||||
| NAME "=" ->
|
||||
ignore (advance p);
|
||||
let i0 = p.i and t0 = peek p in
|
||||
let body, _ = expr p in
|
||||
(wrap [ unit_slot p i0 t0 body ], 0)
|
||||
| NEWLINE when (peek_at p 1).tok = INDENT -> (wrap [], 0)
|
||||
| _ ->
|
||||
failk "lambda-body" (where_ p)
|
||||
"a lambda's body follows = on its line, or is the block under it"
|
||||
|
||||
let rec stmts (s : st) : Form.t list =
|
||||
let p = s.p in
|
||||
match (peek p).tok with
|
||||
@ -1143,6 +1205,15 @@ and then_on_line p =
|
||||
|
||||
and lambda_block ?(block_ok = false) (s : st) (e : Form.t) ~after =
|
||||
let p = s.p in
|
||||
match e.v with
|
||||
(* A typed lambda waiting for its block, from [typed_fn_expr]. *)
|
||||
| Form.List [ ({ v = Form.Sym "the"; _ } as th); fty;
|
||||
({ v = Form.List [ ({ v = Form.Sym "fn"; _ } as fh); ({ v = Form.Vec _; _ } as vec) ]; _ } as fn_) ]
|
||||
when (peek p).tok = NEWLINE && (peek_at p 1).tok = INDENT ->
|
||||
ignore (advance p);
|
||||
let body = block s ~after in
|
||||
mk p e.loc (Form.List [ th; fty; { fn_ with v = Form.List (fh :: vec :: body) } ])
|
||||
| _ ->
|
||||
if is_lambda_candidate e && (last p).tok = RP && (peek p).tok = NEWLINE
|
||||
&& (peek_at p 1).tok = INDENT
|
||||
then begin
|
||||
@ -1216,11 +1287,9 @@ and stmt (s : st) : Form.t =
|
||||
| NAME w when header_follow p w -> header s w
|
||||
| NAME (("else" | "elif") as w) when one_line_if_above p t ->
|
||||
failk "orphan-else" t.loc
|
||||
"the if above is a one-line if, which ends with its line, so this %s \
|
||||
has no if to belong to. Keep the one-line form on one line:\n\n\
|
||||
\ if c then a else b\n\n\
|
||||
or give each branch a block:\n\n\
|
||||
\ if c\n a\n else\n b"
|
||||
"this %s is not at the column of the one-line if above it. An else or \
|
||||
elif that continues a one-line if goes at the if's column:\n\n\
|
||||
\ if c then a\n else b"
|
||||
w
|
||||
| NAME (("else" | "elif") as w) ->
|
||||
failk "orphan-else" t.loc
|
||||
@ -1455,72 +1524,88 @@ and header (s : st) w : Form.t =
|
||||
form [ alias; path ]
|
||||
| "if" ->
|
||||
let c, _ = binary p 1 in
|
||||
(* The elif and else clauses at the if's column, then the whole form.
|
||||
[oneline] when the if was [if c then a]: its clauses may then be
|
||||
one-line too, [elif c then x] and [else y], or take blocks. *)
|
||||
let clauses ~oneline body =
|
||||
let rec elifs acc =
|
||||
match (peek p).tok with
|
||||
| NAME "elif" ->
|
||||
ignore (advance p);
|
||||
let c, _ = binary p 1 in
|
||||
(match (peek p).tok with
|
||||
| NAME "then" when oneline ->
|
||||
ignore (advance p);
|
||||
let x = inline_stmt p in
|
||||
expect_eol p ~after:(text_of x);
|
||||
elifs ((c, [ x ]) :: acc)
|
||||
| NAME "then" ->
|
||||
failk "elif-then" (peek p).loc
|
||||
"elif takes its block on the indented lines under it, with no \
|
||||
then. Put the branch on the next line, indented"
|
||||
| _ ->
|
||||
expect_line_end p ~after:("elif " ^ text_of c);
|
||||
let b = block s ~after:"elif" in
|
||||
elifs ((c, b) :: acc))
|
||||
| _ -> List.rev acc
|
||||
in
|
||||
let els_ = elifs [] in
|
||||
let else_ =
|
||||
match (peek p).tok with
|
||||
| NAME "else" ->
|
||||
let et = advance p in
|
||||
(match (peek p).tok with
|
||||
| NEWLINE -> ignore (advance p); Some (et.loc, block s ~after:"else")
|
||||
| NAME "if" when not oneline ->
|
||||
failk "else-if" (where_ p)
|
||||
"else takes its block on the lines under it. For another test \
|
||||
at this level, write elif c"
|
||||
| _ when oneline ->
|
||||
let x = inline_stmt p in
|
||||
expect_eol p ~after:(text_of x);
|
||||
Some (et.loc, [ x ])
|
||||
| _ -> stray p ~after:"else")
|
||||
| _ -> None
|
||||
in
|
||||
match els_, else_ with
|
||||
| [], None -> named "when" (c :: body)
|
||||
| [], Some (el, e) -> form [ c; blk s l0 body; blk s el e ]
|
||||
| _ ->
|
||||
let pairs =
|
||||
List.concat_map (fun (c, b) -> [ c; blk s c.Form.loc b ]) ((c, body) :: els_)
|
||||
in
|
||||
let tail =
|
||||
match else_ with
|
||||
| Some (el, e) -> [ Form.make (Form.Kw "else") el; blk s el e ]
|
||||
| None -> []
|
||||
in
|
||||
named "cond" (pairs @ tail)
|
||||
in
|
||||
(match (peek p).tok with
|
||||
| NAME "then" ->
|
||||
ignore (advance p);
|
||||
let a = inline_stmt p in
|
||||
let f =
|
||||
match (peek p).tok with
|
||||
| NAME "else" ->
|
||||
ignore (advance p);
|
||||
let b = inline_stmt p in
|
||||
form [ c; a; b ]
|
||||
| NAME "elif" ->
|
||||
failk "one-line-elif" (peek p).loc
|
||||
"a one-line if has then and else and no elif. Chain another if \
|
||||
after the else — if a then x else if b then y else z — or write \
|
||||
the if over several lines, where elif goes"
|
||||
| _ -> named "when" [ c; a ]
|
||||
in
|
||||
expect_eol p ~after:(text_of f);
|
||||
f
|
||||
(match (peek p).tok with
|
||||
| NAME "else" ->
|
||||
ignore (advance p);
|
||||
let b = inline_stmt p in
|
||||
let f = form [ c; a; b ] in
|
||||
expect_eol p ~after:(text_of f);
|
||||
f
|
||||
| NAME "elif" ->
|
||||
failk "one-line-elif" (peek p).loc
|
||||
"a one-line if has then and else and no elif. Chain another if \
|
||||
after the else — if a then x else if b then y else z — or write \
|
||||
the if over several lines, where elif goes"
|
||||
| _ ->
|
||||
expect_eol p ~after:(text_of (named "when" [ c; a ]));
|
||||
(* An else or elif on the next line, at the if's column,
|
||||
continues it. *)
|
||||
clauses ~oneline:true [ a ])
|
||||
| _ ->
|
||||
expect_line_end p ~after:("if " ^ text_of c);
|
||||
let body = block s ~after:("if " ^ text_of c) in
|
||||
let rec elifs acc =
|
||||
match (peek p).tok with
|
||||
| NAME "elif" ->
|
||||
ignore (advance p);
|
||||
let c, _ = binary p 1 in
|
||||
(match (peek p).tok with
|
||||
| NAME "then" ->
|
||||
failk "elif-then" (peek p).loc
|
||||
"elif takes its block on the indented lines under it, with no \
|
||||
then. Put the branch on the next line, indented"
|
||||
| _ -> ());
|
||||
expect_line_end p ~after:("elif " ^ text_of c);
|
||||
let b = block s ~after:"elif" in
|
||||
elifs ((c, b) :: acc)
|
||||
| _ -> List.rev acc
|
||||
in
|
||||
let els_ = elifs [] in
|
||||
let else_ =
|
||||
match (peek p).tok with
|
||||
| NAME "else" ->
|
||||
let et = advance p in
|
||||
(match (peek p).tok with
|
||||
| NEWLINE -> ignore (advance p)
|
||||
| NAME "if" ->
|
||||
failk "else-if" (where_ p)
|
||||
"else takes its block on the lines under it. For another test \
|
||||
at this level, write elif c"
|
||||
| _ -> stray p ~after:"else");
|
||||
Some (et.loc, block s ~after:"else")
|
||||
| _ -> None
|
||||
in
|
||||
(match els_, else_ with
|
||||
| [], None -> named "when" (c :: body)
|
||||
| [], Some (el, e) -> form [ c; blk s l0 body; blk s el e ]
|
||||
| _ ->
|
||||
let pairs =
|
||||
List.concat_map (fun (c, b) -> [ c; blk s c.Form.loc b ]) ((c, body) :: els_)
|
||||
in
|
||||
let tail =
|
||||
match else_ with
|
||||
| Some (el, e) -> [ Form.make (Form.Kw "else") el; blk s el e ]
|
||||
| None -> []
|
||||
in
|
||||
named "cond" (pairs @ tail)))
|
||||
clauses ~oneline:false body)
|
||||
| "while" | "until" ->
|
||||
let label =
|
||||
match (peek p).tok, (peek_at p 1).tok with
|
||||
@ -1639,10 +1724,19 @@ and header (s : st) w : Form.t =
|
||||
let name = name_tok p ~what:"the restart's name" in
|
||||
let lp = glued_lp p ~what:"the restart's parameters in parentheses" in
|
||||
let ps = params p lp in
|
||||
(* [restart name() "text"]: the report the break loop shows,
|
||||
[:report "text"] in the clause. *)
|
||||
let report =
|
||||
match (peek p).tok with
|
||||
| ATOM (Form.Str _ as v) ->
|
||||
let st = advance p in
|
||||
[ Form.make (Form.Kw "report") st.loc; Form.make v st.loc ]
|
||||
| _ -> []
|
||||
in
|
||||
clause_end p ("restart " ^ text_of name ^ "(...)");
|
||||
let b = block s ~after:"restart" in
|
||||
let c =
|
||||
mk p name.loc (Form.List (name :: Form.make (Form.Vec ps) lp.loc :: b))
|
||||
mk p name.loc (Form.List (name :: Form.make (Form.Vec ps) lp.loc :: (report @ b)))
|
||||
in
|
||||
clauses (c :: acc)
|
||||
| _ -> List.rev acc
|
||||
|
||||
@ -327,13 +327,13 @@ let program ?source (fs : Form.t list) : string =
|
||||
cs
|
||||
in
|
||||
let text =
|
||||
String.concat "\n\n"
|
||||
Source_text.join_top
|
||||
(List.map
|
||||
(fun (f : Form.t) ->
|
||||
String.concat "\n"
|
||||
(match layout ~inside spell 0 f with
|
||||
| first :: rest -> Source_text.tag f.loc.Loc.line first :: rest
|
||||
| [] -> []))
|
||||
(f, String.concat "\n"
|
||||
(match layout ~inside spell 0 f with
|
||||
| first :: rest -> Source_text.tag f.loc.Loc.line first :: rest
|
||||
| [] -> [])))
|
||||
fs)
|
||||
^ "\n"
|
||||
in
|
||||
|
||||
@ -206,3 +206,23 @@ let weave ?(starts = []) (cs : comment list) (text : string) : string =
|
||||
else s
|
||||
in
|
||||
trim s
|
||||
|
||||
(** Top-level forms' printed texts joined with a blank line between them,
|
||||
except that one-line globals written on adjacent lines stay adjacent. *)
|
||||
let join_top (items : (Form.t * string) list) : string =
|
||||
let global (f : Form.t) =
|
||||
match f.v with
|
||||
| Form.List ({ v = Form.Sym ("def" | "defonce" | "defconst"); _ } :: _) -> true
|
||||
| _ -> false
|
||||
in
|
||||
let rec go = function
|
||||
| [] -> []
|
||||
| [ (_, t) ] -> [ t ]
|
||||
| ((a : Form.t), ta) :: (((b : Form.t), tb) :: _ as rest) ->
|
||||
let tight =
|
||||
global a && global b && (not (String.contains ta '\n'))
|
||||
&& (not (String.contains tb '\n')) && b.loc.Loc.line = a.loc.Loc.eline + 1
|
||||
in
|
||||
ta :: (if tight then "\n" else "\n\n") :: go rest
|
||||
in
|
||||
String.concat "" (go items)
|
||||
|
||||
32
lib/types.ml
32
lib/types.ml
@ -229,6 +229,10 @@ let rec equal a b =
|
||||
and then only in how a message spells it. *)
|
||||
let display : (string, string) Hashtbl.t = Hashtbl.create 16
|
||||
|
||||
(* The same copies as the template and its arguments, for [spell] to write
|
||||
in either syntax. *)
|
||||
let display_app : (string, string * t list) Hashtbl.t = Hashtbl.create 16
|
||||
|
||||
(* A struct's name as a printed value's head: its own name, or for a generic
|
||||
struct's copy the template and its arguments, [Pair i32] — so a value
|
||||
prints as [(Pair i32 {.a 1 .b 2})], the way its type is written. *)
|
||||
@ -238,7 +242,33 @@ let struct_head n =
|
||||
String.sub d 1 (String.length d - 2)
|
||||
| _ -> n
|
||||
|
||||
let rec to_string = function
|
||||
(* A type as the code it is written in spells it: [(Fn [i32] bool)] in a
|
||||
.flan file, [Fn(i32) -> bool] in a .fln one. Messages use it; a spelling
|
||||
that is a key, a symbol or a runtime string stays [to_string]'s. *)
|
||||
let rec spell ~indented t =
|
||||
if not indented then to_string t
|
||||
else
|
||||
let sp = spell ~indented in
|
||||
let call h args = h ^ "(" ^ String.concat ", " args ^ ")" in
|
||||
match t with
|
||||
| Named n ->
|
||||
(match Hashtbl.find_opt display_app n with
|
||||
| Some (g, args) -> call g (List.map sp args)
|
||||
| None -> to_string t)
|
||||
| Slice (Mut, t) -> "[" ^ sp t ^ "]"
|
||||
| Slice (Const, t) -> "[const " ^ sp t ^ "]"
|
||||
| Array (n, t) -> Printf.sprintf "[%Ld %s]" n (sp t)
|
||||
| LArray (n, t) -> Printf.sprintf "[$%s %s]" n (sp t)
|
||||
| Map (k, v) -> call "Map" [ sp k; sp v ]
|
||||
| Ptr (Mut, t) -> call "Ptr" [ sp t ]
|
||||
| Ptr (Const, t) -> call "Ptr" [ "const " ^ sp t ]
|
||||
| Vec t -> call "Vec" [ sp t ]
|
||||
| Option t -> call "Option" [ sp t ]
|
||||
| Fn (ps, r) -> call "Fn" (List.map sp ps) ^ " -> " ^ sp r
|
||||
| CFn (ps, r) -> call "CFn" (List.map sp ps) ^ " -> " ^ sp r
|
||||
| _ -> to_string t
|
||||
|
||||
and to_string = function
|
||||
| Int k -> ikind_name k
|
||||
| Float k -> fkind_name k
|
||||
| Bool -> "bool"
|
||||
|
||||
@ -88,9 +88,10 @@ reads as `(rl/with-drawing (rl/clear-background rl/black) (game-draw))`.
|
||||
Replacing macros with built-in constructs is **not** part of this work.
|
||||
|
||||
**Diagnostics may print paren syntax** during the test drive. `Form.to_string`,
|
||||
`Types.to_string`, the usage strings in `parse.ml` and `check.ml`, and
|
||||
`Render` all print parens today (inventory in section 5). Fixing that waits on
|
||||
the author deciding to switch.
|
||||
the usage strings in `parse.ml` and `check.ml`, and `Render` print parens
|
||||
today (inventory in section 5). Types in `check.ml`'s messages do not: they
|
||||
are spelled by `Types.spell`, in the syntax of the code the message is about
|
||||
(section 3, item 9).
|
||||
|
||||
## 2. Proposed (confirm before building the piece it governs)
|
||||
|
||||
@ -201,7 +202,10 @@ Each item: the proposal, then the reason in one line.
|
||||
- **`if`/`elif`/`else`.** `else` and `elif` sit at the `if`'s column. No `elif`
|
||||
reads as `if` (with else) or `when` (without); with `elif` it reads as `cond`.
|
||||
One-line form: `if c then a else b`, for use in a `let`. **Built** (a block
|
||||
of one line is that line; of more, `(do …)`).
|
||||
of one line is that line; of more, `(do …)`). An `else` or `elif` on the
|
||||
line after a one-line `if c then a`, at its column, continues it (section
|
||||
3, item 6); each such clause is one-line (`elif c then x`, `else y`) or
|
||||
takes a block.
|
||||
- **`while c`, `until c`**, optional label first: `while :outer c`. **Built.**
|
||||
- **`for i in range(n)`**, `range(a, b)`, `range(a, b, step)` read as
|
||||
`dotimes`. `range` here is syntax, not a function. `..` is avoided because
|
||||
@ -253,14 +257,17 @@ Each item: the proposal, then the reason in one line.
|
||||
v * 2
|
||||
```
|
||||
`handler-bind` takes the same `on` clauses; the reader moves them in front of
|
||||
the body, where the form wants them. **Built.**
|
||||
the body, where the form wants them. **Built.** A restart's report text goes
|
||||
on its header, `restart retry() "Try the load again"`, and reads
|
||||
`(retry [] :report "Try the load again" …)`.
|
||||
- **Unit:** `()` as a statement reads `(do)`; in a type it is `()`. **Built**;
|
||||
inside an expression `()` stays `()`, and the printer writes a lone `()`
|
||||
statement as `(())`. A bare `()` in a one-line body slot (`fn f() -> () = ()`,
|
||||
`_ -> ()`, `fn() = ()`, `then ()`) is a statement too, and reads `(do)`.
|
||||
- **Lambda:** `fn(i, j) = i * 10 + j`, or `fn(i, j)` plus a block. **Built**;
|
||||
its parameters are bare names, as `(fn [i j] …)` wants, with no `dyn`.
|
||||
`fn(…)` followed by anything else is the fallback call.
|
||||
`fn(…)` followed by anything else is the fallback call. A lambda may state
|
||||
its types, `fn(a: C, b) -> bool = …` or plus a block (section 3, item 7).
|
||||
|
||||
### Definitions
|
||||
|
||||
@ -274,10 +281,14 @@ Each item: the proposal, then the reason in one line.
|
||||
- `struct Cell` with a `name: Type` line per field. `data Shape` with a line per
|
||||
case: `Circle(r: f32)`, `Empty`. `enum K` with `lo = -1`, `mid`. `union U` like
|
||||
`struct`. **Built** (an untyped field is `dyn`; `Empty()` is `(Empty [])`).
|
||||
A member is `:mid` or `K.mid`, in a value and in a match arm, in both
|
||||
syntaxes (section 3, item 8).
|
||||
- `import rl "vendor:raylib"`. **Built.**
|
||||
- **Every other form uses the fallback** (next item) until someone asks for
|
||||
sugar: `defclass`, `defgeneric`, `defmulti`, `defmethod`, `declare`,
|
||||
`declare-c`, `defalias`, `defmacro`, `loop`/`recur`, `array-fill`. **Built.**
|
||||
The class forms keep the fallback for good (2026-09-26):
|
||||
`defmethod(area, point, [p]):` reads well enough.
|
||||
|
||||
### The fallback
|
||||
|
||||
@ -341,6 +352,27 @@ after it, so `~name(x)` is `((unquote name) x)`, and `~(f(x))` unquotes a call.
|
||||
omitted. `_` in type position means "fill this in" in Rust and OCaml too,
|
||||
and no type can be named `_`.
|
||||
|
||||
Settled 2026-09-26, after writing programs by hand (`test/syntax/handwritten/`):
|
||||
|
||||
6. **A one-line if continues on the next line.** `if c then a` followed by
|
||||
`else b` (or `elif c2 then d`, or either with a block) at the if's column
|
||||
is one if. An `else` left of that column is refused.
|
||||
7. **Typed lambdas.** `fn(a: C, b) -> R = body`, or plus a block, reads
|
||||
`(the (Fn [C dyn] R) (fn [a b] body))`: the paren `fn` has no typed
|
||||
parameters, and `the` is how a value states its type, as in
|
||||
`let x: T = v`. An untyped parameter is `dyn`; the return type is
|
||||
required. Where a `CFn` of the same signature is wanted, the literal is
|
||||
that `CFn`. The printer writes that form back as the typed lambda.
|
||||
8. **`Dir.north` is the enum member `:north`**, in a value and in a match
|
||||
pattern, in both syntaxes. `:north` stays.
|
||||
9. **Types in messages follow the code's syntax.** `Types.spell ~indented` is
|
||||
the one printer, `Fn(A) -> R`, `Option(i32)`, `Small(4, i32)` for a .fln
|
||||
location and `(Fn [A] R)` for a .flan one; `Types.to_string` stays the
|
||||
spelling for keys, symbols and runtime strings. Hard-coded code in a hint
|
||||
is written per message.
|
||||
10. **`flan convert` keeps adjacent one-line globals adjacent**, in both
|
||||
directions.
|
||||
|
||||
## 4. Build order
|
||||
|
||||
Each step lands on its own, with `dune test --root .` green.
|
||||
|
||||
@ -26,7 +26,7 @@ fn flush(field: Ptr(Vec(u8)), row: Ptr(Vec(string))) -> ()
|
||||
fn parse-line(line: [const u8]) -> Vec(string)
|
||||
let row = vec-new(string)
|
||||
let field = vec-new(u8)
|
||||
let state: State = :start
|
||||
let state = State.start
|
||||
for i in range(length(line))
|
||||
let c = line[i]
|
||||
match state
|
||||
@ -38,7 +38,7 @@ fn parse-line(line: [const u8]) -> Vec(string)
|
||||
else
|
||||
push(field, c)
|
||||
state = :bare
|
||||
:bare ->
|
||||
State.bare ->
|
||||
if c == separator
|
||||
flush(addr(field), addr(row))
|
||||
state = :start
|
||||
|
||||
@ -18,10 +18,8 @@ const red-time = 3
|
||||
fn next(l: Light, pressed: bool) -> Light
|
||||
match l
|
||||
Green(left) ->
|
||||
if left > 1 and not pressed
|
||||
Light.Green{.left left - 1}
|
||||
else
|
||||
Light.Yellow{.left yellow-time, .walk pressed}
|
||||
if left > 1 and not pressed then Light.Green{.left left - 1}
|
||||
else Light.Yellow{.left yellow-time, .walk pressed}
|
||||
Yellow(left, walk) ->
|
||||
if left > 1
|
||||
Light.Yellow{.left left - 1, .walk walk}
|
||||
@ -53,9 +51,7 @@ fn run(ticks: i32, presses: [const i32], fault-at: i32) -> ()
|
||||
l = restart-case
|
||||
error(Fault{.tick t})
|
||||
l
|
||||
restart reset()
|
||||
:report
|
||||
"Put the light back to red and carry on"
|
||||
restart reset() "Put the light back to red and carry on"
|
||||
Light.Red{.left red-time, .walk false}
|
||||
restart flash()
|
||||
Light.Flashing{}
|
||||
|
||||
@ -58,7 +58,7 @@ fn main() -> i32
|
||||
let text = "The cat saw the dog. The dog didn't see the cat, but the bird saw both!"
|
||||
let ws = words(bytes-view(text))
|
||||
let counts = tally(slice(ws))
|
||||
let by-count: Fn(Count, Count) -> bool = fn(a, b)
|
||||
let by-count = fn(a: Count, b: Count) -> bool
|
||||
if a.n != b.n
|
||||
return a.n > b.n
|
||||
bytes<?(a.word, b.word)
|
||||
|
||||
@ -592,8 +592,21 @@ let () =
|
||||
"indent/lambda-block-in-brackets" "let f = fn(a, b)";
|
||||
refuses "an unclosed call swallows the next line" "fn f() -> ()\n push(v, 1\n g()"
|
||||
"indent/missing-comma" "If the ( on line 2 was meant to close";
|
||||
refuses "else under a one-line if" "if a then b\nelse c"
|
||||
"indent/orphan-else" "one-line if";
|
||||
reads "else on the line after a one-line if" "if a then b\nelse c" "(if a b c)";
|
||||
reads "elif and else continuing a one-line if"
|
||||
"if a then b\nelif c then d\nelif e\n f()\n g()\nelse\n h()"
|
||||
"(cond a b c d e (do (f) (g)) :else (h))";
|
||||
refuses "else left of a one-line if" "while x\n if a then b\nelse c"
|
||||
"indent/orphan-else" "goes at the if's column";
|
||||
reads "a typed lambda" "f = fn(a: C, b) -> bool = a.n < b"
|
||||
"(set f (the (Fn [C dyn] bool) (fn [a b] (< (.n a) b))))";
|
||||
reads "a typed lambda with a block" "let f = fn(x: i32) -> i32\n let y = x + 1\n y\ng(f)"
|
||||
"(let [f (the (Fn [i32] i32) (fn [x] (let [y (+ x 1)] y)))] (g f))";
|
||||
refuses "a typed lambda states its return type" "f = fn(a: C) = a"
|
||||
"indent/lambda-return" "fn(a: C) -> R = value";
|
||||
reads "a restart's report on its header"
|
||||
"restart-case\n go()\nrestart retry(n: i32) \"Try again\"\n n"
|
||||
"(restart-case (go) (retry [n i32] :report \"Try again\" n))";
|
||||
reads "a bare () in a body slot does nothing"
|
||||
"fn f() -> () = ()\nfn g(x) -> ()\n match x\n 1 -> h()\n _ -> ()\n k = fn() = ()"
|
||||
"(defn f [] () (do))\n(defn g [x dyn] () (match x 1 (h) _ (do)) (set k (fn [] (do))))";
|
||||
@ -763,6 +776,18 @@ let () =
|
||||
"(defn f [] () (if (> a 1) (let [k 2] (g k))))" " if(a > 1):\n let k = 2";
|
||||
prints "and inside or keeps its parentheses"
|
||||
"(defn f [a bool b bool c bool] bool (or (and a b) c))" "= (a and b) or c";
|
||||
prints "a typed lambda prints as one"
|
||||
"(defn f [] () (let [g (the (Fn [C] bool) (fn [c] (> (.n c) 3)))] (h g)))"
|
||||
"let g = fn(c: C) -> bool = c.n > 3";
|
||||
prints "a restart's report goes on its header"
|
||||
"(defn f [] i32 (restart-case (go) (retry [] :report \"Try again\" 7)))"
|
||||
"restart retry() \"Try again\"\n 7";
|
||||
prints "adjacent one-line globals stay adjacent"
|
||||
"(defonce a i32)\n(def b i32 2)\n\n(defconst c 3)\n"
|
||||
"once a: i32\ndef b: i32 = 2\n\nconst c = 3";
|
||||
back "adjacent one-line globals stay adjacent in parens"
|
||||
"once a: i32\ndef b: i32 = 2\n\nconst c = 3\n"
|
||||
"(defonce a i32)\n(def b i32 2)\n\n(defconst c 3)";
|
||||
prints "a field of a field chains" "(defn f [] () (g (.count (.x w))))" "g(w.x.count)";
|
||||
prints "an else-if chain on one line"
|
||||
"(defn f [r] dyn (if (> r 7) :rich (if (> r 4) :fair :poor)))"
|
||||
@ -983,6 +1008,31 @@ let () =
|
||||
[ "write ++(x) or x += 1" ];
|
||||
refused "plusplus-global.fln" "once g = 0\n\nfn main() -> i32\n g--\n 0\n"
|
||||
[ "write --(g) or g -= 1" ];
|
||||
(* Types in a message are in the syntax of the code it is about. *)
|
||||
refused "types.fln" "fn g(x: Option(i32)) -> i32 = 0\n\nfn main() -> i32\n let v = vec-new(i32)\n g(v)\n"
|
||||
[ "expected Option(i32), found Vec(i32)" ];
|
||||
refused "types.flan" "(defn g [x (Option i32)] i32 0)\n(defn main [] i32 (let [v (vec-new i32)] (g v)))\n"
|
||||
[ "expected (Option i32), found (Vec i32)" ];
|
||||
refused "fn-field.fln" "struct R\n f: Fn(i32) -> bool\n\nfn main() -> i32 = 0\n"
|
||||
[ "cannot be Fn(i32) -> bool"; "store a CFn(i32) -> bool" ];
|
||||
refused "generic-struct.fln"
|
||||
"struct Small\n items: [$n $t]\n\nfn main() -> i32\n let s: Small(4, i32) = zeroed()\n let q: i32 = s\n 0\n"
|
||||
[ "found Small(4, i32)" ];
|
||||
(* Dir.north is the member :north. *)
|
||||
checks "enum-qualified.fln"
|
||||
("enum Dir\n north\n south\n\nfn name(d: Dir) -> i32\n match d\n Dir.north -> 1\n :south -> 2\n\n"
|
||||
^ "fn main() -> i32\n let d = Dir.north\n let e: Dir = Dir.south\n name(d) + name(e)\n");
|
||||
refused "enum-qualified-miss.fln"
|
||||
"enum Dir\n north\n\nfn f(d: Dir) -> i32\n match d\n Dir.west -> 1\n _ -> 0\n\nfn main() -> i32 = 0\n"
|
||||
[ "Dir has no member west — it has Dir.north" ];
|
||||
(* A bad key type is one error, not three. *)
|
||||
refused "map-key.fln" "fn main() -> i32\n let m = map-new([const u8], i32)\n 0\n"
|
||||
[ "[const u8] is not a map key" ];
|
||||
(match Front.checked (Filename.concat scratch "map-key.fln") with
|
||||
| exception Loc.Errors (_ :: _ :: _ as ds) ->
|
||||
fail "map-key.fln: %d errors, wanted one" (List.length ds)
|
||||
| exception _ -> ()
|
||||
| _ -> ());
|
||||
refused "defvar.fln" "defvar(x, 1)\n\nfn main() -> i32 = 0\n"
|
||||
[ "once x = 1 initialises once"; "def x = 1 re-initialises" ]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user