(** The indented reader: [.fln] text to exactly the [Form.t] tree the paren reader ([Reader]) makes. Nothing after the reader knows which syntax a form came from. spec-syntax.md is the grammar; this comment is only the shape. Three passes. [lex] turns text into tokens, reusing [Reader]'s own string, character, number and quoted-datum readers so the atoms mean exactly what they mean in a [.flan] file. [layout] adds NEWLINE, INDENT and DEDENT at bracket depth zero from an indent stack of columns. The parser is a statement parser (soft keywords at the start of a line) over a precedence climber for expressions. Locations are spans, as [Reader] makes them: a form starts at its first token and ends where its last one does. A form this reader invents — the [dyn] of an untyped parameter, the [do] around a block, the [set] of an assignment — takes the location of the text that asked for it. *) type tok = | NAME of string (* a name run, after field splitting *) | KW of string | ATOM of Form.value (* number, string, character *) | DATUM of Form.t (* 'x and '(a b), read by the paren reader *) | LP | RP | LB | RB | LC | RC | COMMA | COLON (* x: T, and the trailing : of a call's block *) | UNQ | SPLICE (* ~ and ~@ *) | BNOT (* ~~, bit-not; a nested unquote is ~(~x) *) | NEG (* the - glued to the front of a name *) | QUEST (* T? and a?.b: a ? glued after a name or a closer *) | BANG (* x!: a ! glued after a value *) | NEWLINE | INDENT | DEDENT | EOF type token = { tok : tok; loc : Loc.t; sp : bool (* whitespace before it *) } let failk ?notes kind loc fmt = Loc.failk ?notes ("indent/" ^ kind) loc fmt let show = function | NAME s -> s | KW s -> ":" ^ s | ATOM v -> Form.to_source (Form.make v Loc.unknown) | DATUM f -> Form.to_source f | LP -> "(" | RP -> ")" | LB -> "[" | RB -> "]" | LC -> "{" | RC -> "}" | COMMA -> "," | COLON -> ":" | UNQ -> "~" | SPLICE -> "~@" | BNOT -> "~~" | NEG -> "-" | QUEST -> "?" | BANG -> "!" | NEWLINE -> "the end of the line" | INDENT -> "an indented line" | DEDENT -> "the end of the block" | EOF -> "the end of the file" (* ── Names ─────────────────────────────────────────────────────────── *) (* Binary operators and their levels, low to high (spec §2 "Precedence"). [not] sits at 3 and the prefix [-] and [~~] at 11; neither is binary. The bit operators sit between the comparisons and the shifts, Python's and Rust's order, so [x && mask == 0] is [(x && mask) == 0]. *) let binops = [ ("or", 1); ("and", 2); ("==", 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) ] let binop_level s = List.assoc_opt s binops let is_binop s = binop_level s <> None (* [==] is Flan's [=], and the bit operators are the words the Lisp side writes; every other operator is its own name. *) let op_sym = function | "==" -> "=" | "&&" -> "bit-and" | "||" -> "bit-or" | "^^" -> "bit-xor" | s -> s (* Words that are operators rather than names wherever a value is read. Alone before a comma or a closer they are the symbol itself, [reduce(+, 0, xs)]; glued to a parenthesis they are a call, [+(a, b, c)]. *) let is_op_word s = is_binop s || s = "not" || s = "=" let assign_ops = [ ("+=", "+"); ("-=", "-"); ("*=", "*"); ("/=", "/") ] (* A place whose parts are all names and literals reads the same however often it is evaluated, so [x += v] over one is [(set x (+ x v))], the form the Lisp side writes. Any other place — an index that is a call — reads [(update p + v)], which evaluates each part of the place once. The printer asks the same question, so the round trip is exact either way. *) let rec simple_place (f : Form.t) = let atom (x : Form.t) = match x.v with | Form.Sym _ | Form.Int _ | Form.Kw _ | Form.Byte _ -> true | _ -> false in match f.v with | Form.Sym _ -> true | Form.List [ { v = Form.Sym h; _ }; t ] when (String.length h > 1 && h.[0] = '.') || h = "deref" -> simple_place t | Form.List ({ v = Form.Sym "at"; _ } :: t :: (_ :: _ as idx)) -> simple_place t && List.for_all atom idx | _ -> false let compound (at : Loc.t) op (e : Form.t) (v : Form.t) span = if simple_place e then Form.List [ Form.make (Form.Sym "set") at; e; Form.make (Form.List [ Form.make (Form.Sym op) at; e; v ]) span ] else Form.List [ Form.make (Form.Sym "update") at; e; Form.make (Form.Sym op) at; v ] (* A comparison chain that mixes [<] with [<=], or [>] with [>=], is the [and] of its neighbouring pairs: [0 <= r < rows] is [(and (<= 0 r) (< r rows))]. It is evaluated as [(< a b c)] is: every operand once, left to right, before any test, with no short-circuit. When an operand is more than a name or a literal, every operand but a literal is bound first, in order, to a fresh [~cmp] name, which no reader can produce: a name too, since a call to its right may change it. The printer rebuilds a candidate with this same function and prints the chain only when the two agree. *) let cmp_dir = function | "<" | "<=" -> Some `Up | ">" | ">=" -> Some `Down | _ -> None let cmp_chain ~fresh (l : Loc.t) (xs : Form.t list) (ops : string list) = let mkf v = Form.make v l in let s x = mkf (Form.Sym x) in let literal (x : Form.t) = match x.v with | Form.Int _ | Form.UInt _ | Form.Float _ | Form.Str _ | Form.Byte _ | Form.Kw _ | Form.Sym ("true" | "false" | "nil") -> true | _ -> false in let simple (x : Form.t) = literal x || (match x.v with Form.Sym _ -> true | _ -> false) in let keep = if List.for_all simple xs then simple else literal in let bound = List.map (fun x -> if keep x then (None, x) else let t = s (fresh ()) in (Some (t, x), t)) xs in let refs = List.map snd bound in let rec tests = function | a :: (b :: _ as rest), op :: ops -> mkf (Form.List [ s op; a; b ]) :: tests (rest, ops) | _ -> [] in let body = mkf (Form.List (s "and" :: tests (refs, ops))) in match List.concat_map (function (Some (t, x), _) -> [ t; x ] | _ -> []) bound with | [] -> body | bs -> mkf (Form.List [ s "let"; mkf (Form.Vec bs); body ]) (* The reader's fresh names for [cmp_chain], counted per [read_all]. *) let cmp_n = ref 0 let cmp_fresh () = incr cmp_n; Printf.sprintf "~cmp%d" !cmp_n (* The reader's fresh names for an optional chain's payload, per [read_all]. *) let opt_n = ref 0 (* Whether a type is being read, set for [ty]'s extent: there a [?] after any name is [Option], [grain?] included. *) let in_type = ref false (* A [-] glued to one of these starts a negation: [-x] is [(- x)]. Anything else keeps the Lisp reading, so [--], [->] and [-=] stay names. *) let is_neg_char c = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c = '$' || c = '_' || c = '*' (* The segment a dot splits after, checked for a capital: [Shape.Rect] and [tree/Node.Branch] are one qualified name, [camera.target.x] is two field accesses. The part after a package's [/] is what is checked. *) let capitalised seg = let base = match String.rindex_opt seg '/' with | Some i -> String.sub seg (i + 1) (String.length seg - i - 1) | None -> seg in base <> "" && base.[0] >= 'A' && base.[0] <= 'Z' let split_fields text = if text = "" || text.[0] = '.' then [ text ] else let segs = String.split_on_char '.' text in if List.length segs < 2 || List.mem "" segs || capitalised (List.hd segs) then [ text ] else segs (* ── Names without ? or ! ──────────────────────────────────────────── *) (* What a [?] may follow to mean [Option]: a type's name. A capital after any [pkg/], a type variable, or a primitive. *) let type_like s = let base = match String.rindex_opt s '/' with | Some i -> String.sub s (i + 1) (String.length s - i - 1) | None -> s in base <> "" && ((base.[0] >= 'A' && base.[0] <= 'Z') || base.[0] = '$' || List.mem base ("char" :: Types.primitive_names)) (* The name a question is spelled with: [is-] in front, unless it already starts with a verb. The table holds the prelude's and raylib's names that do not follow that rule, so the fix for one of them is the real name. *) let question_fix name = let pkg, base = match String.rindex_opt name '/' with | Some i -> (String.sub name 0 (i + 1), String.sub name (i + 1) (String.length name - i - 1)) | None -> ("", name) in let starts p = String.starts_with ~prefix:p base in let fixed = match base with | "starts-with" -> "has-prefix" | "ends-with" -> "has-suffix" | "file-exists" | "window-should-close" -> base | "bytes<" -> "is-bytes-less" | "into-maps" -> "has-map-step" | "form-sym" -> "is-form-named" | "form-is-sym" -> "is-form-sym" | _ when starts "is-" || starts "has-" || starts "can-" -> base | _ when starts "collision-" -> "check-" ^ base | _ when String.ends_with ~suffix:"=" base -> "is-" ^ String.sub base 0 (String.length base - 1) ^ "-equal" | _ -> "is-" ^ base in pkg ^ fixed let name_refused ?(typed = false) loc whole = let drop c s = String.concat "" (String.split_on_char c s) in let n = String.length whole in if n > 1 && whole.[n - 1] = '?' && not (String.contains (String.sub whole 0 (n - 1)) '?') && not (String.contains whole '!') then let base = String.sub whole 0 (n - 1) in Loc.failk "indent/question-name" loc "%s is not a name: a name cannot contain ?.\n\n\ A name for a yes-or-no question starts with is- or has- instead: %s%s" whole (question_fix base) (if typed then Printf.sprintf "\n\nIf %s is a type, %s is Option(%s), written where a \ type goes, after : or ->" base whole base else "") else let c = if String.contains whole '!' then '!' else '?' in Loc.failk "indent/mark-in-name" loc "%s is not a name: a name cannot contain %c.\n\nLeave it out: %s" whole c (drop '!' (drop '?' whole)) (* ── Lexing ────────────────────────────────────────────────────────── *) let lex ?(line = 1) ?(col = 1) ~file src : token list = let st = Reader.of_string ~file src in (* Text taken from the middle of a buffer starts where it was written, so every location read from it is the buffer's own. *) st.Reader.line <- line; st.Reader.col <- col; let out = ref [] in let sp = ref true in let line_start = ref true in let tab = ref None in let emit tok loc = out := { tok; loc; sp = !sp } :: !out; sp := false in let piece line col len = { (Loc.make file line col) with Loc.eline = line; ecol = col + len } in let name_run () = let l0 = Reader.here st in let text = Reader.take_while st (fun c -> not (Reader.is_delimiter c)) in let n = String.length text in let line = l0.Loc.line and col = l0.Loc.col in if n = 0 then failk "unexpected-character" l0 "unexpected character %C" (Reader.peek st); if text = ":" then emit COLON (piece line col 1) else if text.[0] = ':' then emit (KW (String.sub text 1 (n - 1))) (piece line col n) else begin let body, colon = if text.[n - 1] = ':' then (String.sub text 0 (n - 1), true) else (text, false) in let plain col body = let bn = String.length body in let bcol, body = if bn > 1 && body.[0] = '-' && is_neg_char body.[1] then begin emit NEG (piece line col 1); (col + 1, String.sub body 1 (bn - 1)) end else (col, body) in let off = ref 0 in List.iteri (fun i seg -> let s = if i = 0 then seg else "." ^ seg in emit (NAME s) (piece line (bcol + !off) (String.length s)); off := !off + String.length s) (split_fields body) in (* [.b.c] after a [?] or a [!]: one field access per segment. *) let fields col s = let segs = String.split_on_char '.' (String.sub s 1 (String.length s - 1)) in if List.mem "" segs then emit (NAME s) (piece line col (String.length s)) else List.fold_left (fun c seg -> emit (NAME ("." ^ seg)) (piece line c (String.length seg + 1)); c + String.length seg + 1) col segs |> ignore in (* Before the first mark a leading dot is the name's own, [.field] as an accessor; after one it starts a field access. *) let some_part ~after col s = if s = "" then () else if after && s.[0] = '.' then fields col s else plain col s in (* The name a [?] or a [!] at [i] of [s] belongs to: back to the dot or the start before it, on to the dot or the end after it. *) let word s i = let a = match String.rindex_from_opt s i '.' with Some d -> d + 1 | None -> 0 in let b = match String.index_from_opt s i '.' with Some d -> d | None -> String.length s in (a, String.sub s a (b - a)) in let next = if Reader.at_end st then ' ' else Reader.peek st in (* A [?] ends a type's name, [T?], or starts a chain, [a?.b] and [a?[i]]; a [!] unwraps, [x!]. Anywhere else each is inside a name, which is refused (spec-syntax.md, "Names"). *) let rec marks ?(after = false) col s = match String.index_opt s '?', String.index_opt s '!' with | None, None -> some_part ~after col s | q, b -> let i = match q, b with | Some q, Some b -> min q b | Some q, None -> q | None, Some b -> b | None, None -> assert false in let pre = String.sub s 0 i and ch = s.[i] in let rest = String.sub s (i + 1) (String.length s - i - 1) in let at = col + i in let wa, whole = word s i in let last = match String.rindex_opt pre '.' with | Some d -> String.sub pre (d + 1) (String.length pre - d - 1) | None -> pre in (* The word after this run, for an operator written without its spaces: [x!= y] is [x != y]. *) let after_word r = if r <> "" then r else let j = ref st.Reader.pos in while !j < String.length src && src.[!j] = ' ' do incr j done; let k = ref !j in while !k < String.length src && not (Reader.is_delimiter src.[!k]) do incr k done; if !k > !j then String.sub src !j (!k - !j) else "b" in let tail r = String.sub r 1 (String.length r - 1) in if ch = '?' then begin if rest = "?" && pre <> "" && type_like last && next <> '(' then failk "nested-option" (piece line (col + wa) (String.length whole)) "%s is not read: ?? is the default operator. An Option of an \ Option is written Option(%s?)" whole last; if rest <> "" && rest.[0] = '?' && pre <> "" then failk "unspaced-operator" (piece line at 2) "?? is an operator here, and a binary operator has a space on \ each side: %s ?? %s" pre (after_word (tail rest)); (* A [?] at the end of a name is a type's, [T?], or one the parser refuses as part of a name; which, only the parser knows. Before [(] or inside a name it is a name's. *) let ends = rest = "" && next <> '(' in let chain = (rest <> "" && rest.[0] = '.') || (rest = "" && next = '[') in if ends || chain then begin some_part ~after col pre; emit QUEST (piece line at 1); marks ~after:true (at + 1) rest end else name_refused (piece line (col + wa) (String.length whole)) whole end else begin if rest <> "" && rest.[0] = '=' then failk "unspaced-operator" (piece line at 2) "!= is an operator here, and a binary operator has a space on \ each side: %s != %s" pre (after_word (tail rest)); if rest <> "" && rest.[0] = '!' then failk "double-unwrap" (piece line at 2) "!! is not read. To unwrap an Option of an Option, unwrap \ each level: (%s!)!" (if pre = "" then "x" else pre); if (rest = "" && next <> '(') || (rest <> "" && rest.[0] = '.') then begin some_part ~after col pre; emit BANG (piece line at 1); marks ~after:true (at + 1) rest end else name_refused (piece line (col + wa) (String.length whole)) whole end in let wordy = String.exists (fun c -> Reader.is_digit c || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) body in if wordy || body = "?" || body = "!" || body = "!!" then marks col body else plain col body; if colon then emit COLON (piece line (col + n - 1) 1) end in let token c = let l0 = Reader.here st in let simple t = Reader.advance st; emit t (Loc.upto l0 (Reader.here st)) in match c with | '(' -> simple LP | ')' -> simple RP | '[' -> simple LB | ']' -> simple RB | '{' -> simple LC | '}' -> simple RC | ',' -> simple COMMA | '"' -> let f = Reader.read_string st in emit (ATOM f.v) f.loc | '\\' -> let f = Reader.read_byte st in emit (ATOM f.v) f.loc (* The paren reader reads the quoted datum whole, so ['(a (b c))] is the Lisp list it always was and nothing here re-invents it. *) | '\'' -> let f = Reader.read_form st in emit (DATUM f) f.loc | '`' -> failk "backquote" l0 "` is not read in a .fln file. A quasiquote is quote followed by an \ indented block, or quasiquote(x) on one line" | '~' -> Reader.advance st; if Reader.peek st = '~' then begin Reader.advance st; emit BNOT (Loc.upto l0 (Reader.here st)) end else if Reader.peek st = '@' then begin Reader.advance st; emit SPLICE (Loc.upto l0 (Reader.here st)) end else emit UNQ (Loc.upto l0 (Reader.here st)) | c when Reader.is_digit c || ((c = '-' || c = '+') && Reader.is_digit (Reader.peek2 st)) -> (* [while x < 3:] — the colon is a mistake the parser explains, and not part of the number, so the number is read without it. *) let rec run i = if i < String.length src && not (Reader.is_delimiter src.[i]) then run (i + 1) else i in let stop = run st.Reader.pos in if stop - st.Reader.pos > 1 && src.[stop - 1] = ':' then begin let text = String.sub src st.Reader.pos (stop - st.Reader.pos - 1) in let f = Reader.read_number (Reader.of_string ~file text) in let n = String.length text in for _ = 1 to n do Reader.advance st done; emit (ATOM f.v) (piece l0.Loc.line l0.Loc.col n); Reader.advance st; emit COLON (piece l0.Loc.line (l0.Loc.col + n) 1) end else begin (* [0..10]: a range from another language. *) let text = String.sub src st.Reader.pos (stop - st.Reader.pos) in (match String.index_opt text '.' with | Some i when i + 1 < String.length text && text.[i + 1] = '.' -> failk "dot-range" (piece l0.Loc.line l0.Loc.col (String.length text)) "%s is not a number. A range of numbers is written range(%s, %s), \ as in for i in range(%s, %s)" text (String.sub text 0 i) (String.sub text (i + 2) (String.length text - i - 2)) (String.sub text 0 i) (String.sub text (i + 2) (String.length text - i - 2)) | _ -> ()); let f = Reader.read_number st in emit (ATOM f.v) f.loc end | _ -> name_run () in let rec go () = if not (Reader.at_end st) then match Reader.peek st with | ' ' | '\r' -> Reader.advance st; sp := true; go () | '\t' -> if !line_start && !tab = None then tab := Some (Reader.here st); Reader.advance st; sp := true; go () | '\n' -> Reader.advance st; sp := true; line_start := true; tab := None; go () | ';' -> while (not (Reader.at_end st)) && Reader.peek st <> '\n' do Reader.advance st done; go () | c -> (match !tab with | Some l when !line_start -> failk "tab" l "this line is indented with a tab. Indentation in a .fln file is \ measured in columns, and a tab has no one width, so only spaces \ indent. Replace the tab with spaces" | _ -> ()); line_start := false; token c; go () in go (); List.rev !out (* ── Layout ────────────────────────────────────────────────────────── *) (* The text being read, so that a message quotes what the user wrote rather than the paren form it became. Set for the length of one [read_all]. *) let source : (string * string array) ref = ref ("", [||]) (* Line [l] of the text being read, trimmed, and the column its text starts at. *) let source_line l = let _, lines = !source in if l < 1 || l > Array.length lines then ("", 1) else let t = lines.(l - 1) in let n = String.length t in let rec first i = if i < n && t.[i] = ' ' then first (i + 1) else i in (String.trim t, first 0 + 1) (* A line under a let that is not at its first name's column [name_col]: the fix is the let's line and this one, lined up. *) let let_misaligned loc ~let_line ~name ~name_col = let lt, lc = source_line let_line in let bt, _ = source_line loc.Loc.line in failk "let-align" loc "this line starts at column %d, under the let on line %d, whose bindings \ line up with its first name, %s, at column %d. Move it to column %d:\n\n\ \ %s\n %s%s" loc.Loc.col let_line name name_col name_col lt (String.make (max 0 (name_col - lc)) ' ') bt (* A line at a let's first name's column, after a value that took the lines under the let: it is the value's, not one more binding. *) let let_after_block loc ~let_line ~name = let bt, _ = source_line loc.Loc.line in failk "let-after-block" loc "this line lines up as one more binding of the let on line %d, after %s, \ whose value is the block above it. A binding whose value is a block ends \ its let's bindings, so this one needs a let of its own, at that let's \ column:\n\n let %s" let_line name bt (* Whether the tokens from [i] start a line shaped as a binding: [name =], [name:], a [[...]], [{...}] or [(op)] pattern, or [~x]. The alignment advice is for such lines only; any other line under a let is some other mistake. *) let binding_shaped (arr : token array) i = let n = Array.length arr in let at k = if i + k < n then arr.(i + k).tok else EOF in match at 0, at 1 with | NAME s, (NAME "=" | COLON) -> s <> "" && s.[0] <> '.' | (LB | LC | LP | UNQ), _ -> true | _ -> false (* A tab between [let] and its first name: the bindings under the let line up with that name, and a tab has no one width to line up with. *) let let_gap_tab (t : token) (nm : token) = if t.loc.Loc.eline = nm.loc.Loc.line then begin let _, lines = !source in if nm.loc.Loc.line <= Array.length lines then let line = lines.(nm.loc.Loc.line - 1) in let a = t.loc.Loc.ecol - 1 and b = nm.loc.Loc.col - 1 in if a >= 0 && b <= String.length line && b > a && String.contains (String.sub line a (b - a)) '\t' then failk "tab" t.loc "there is a tab between let and %s. The bindings under a let line up \ with its first name, and a tab has no one width, so put a space \ there: let %s" (show nm.tok) (show nm.tok) end let point (l : Loc.t) = { l with Loc.line = l.Loc.eline; col = l.Loc.ecol } (* NEWLINE, INDENT and DEDENT, at bracket depth zero only: inside ( [ { a line break is whitespace. A line continues the one before it when either side of the break is a spaced binary operator (spec §2 "Continuation"). The one exception is a lambda's block. A [=>] that ends its line inside brackets opens a block there: the lines under it are laid out as they would be at depth zero, against a base of their own (the column the [=>] line starts at), until the bracket around the lambda closes. That closer ends the block, whether it ends the block's last line or has a line of its own. The block is the last thing in its brackets: a comma after it, or a line back at the header's column, is refused. *) type frame = { f_base : int; f_stack : int list; f_opens : token list; (* the brackets open around the lambda *) f_arrow : token; (* the [=>] that opened the block *) } let lambda_not_last (fr : frame) (t : token) = failk "lambda-block-last" t.loc "%s follows the block of the lambda on line %d, inside the same \ brackets. A lambda with a block is the last thing in its brackets, and \ its block ends where they close. Name the lambda with a let first and \ pass the name:\n\n\ \ let f = fn(a) =>\n ...\n g(f, x)" (if t.tok = COMMA then "a comma" else show t.tok) fr.f_arrow.loc.Loc.line let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token array = let arr = Array.of_list toks in let n = Array.length arr in (* A snippet from the editor starts wherever it was written, and its first line is its base: a later line may not go left of it. One cut from the middle of a line (see [indent]) has that line's start as its base, so a block under the line, a [let]'s [match] arms or a lambda's, reads as it does in the file. *) let base = ref (if snippet && n > 0 then match indent with | Some c -> min c arr.(0).loc.Loc.col | None -> arr.(0).loc.Loc.col else base) in let out = ref [] in let add tok loc = out := { tok; loc; sp = true } :: !out in let stack = ref [ !base ] in (* [indent] is the column of the statement a snippet was cut out of, when the snippet starts after that statement's first word (an elif's condition, an arm's value). Its first joined line continues as it does in the file: deeper than the statement, not than the cut. *) let first_line = ref true in (* The brackets open in the current layout, innermost first. A lambda's block starts with none, and [frames] holds what it interrupted. *) let opens = ref [] in let frames = ref [] in let binop t = match t.tok with NAME s -> is_binop s | _ -> false in let closer t = match t.tok with RP | RB | RC -> true | _ -> false in (* The column [i]'s line starts at. *) let line_col i = let rec go j = if j > 0 && arr.(j - 1).loc.Loc.eline = arr.(i).loc.Loc.line then go (j - 1) else j in arr.(go i).loc.Loc.col in for i = 0 to n - 1 do let t = arr.(i) in (if i = 0 then begin if t.loc.Loc.col <> !base && indent = None then failk "unexpected-indent" t.loc "the first line starts at column %d, and a file's top-level lines \ start at column %d. Remove the indentation" t.loc.Loc.col !base end else let p = arr.(i - 1) in (* The closer that ends a lambda's block takes the block's end with it, below: the line break before it is nothing. *) let ends_block = !frames <> [] && !opens = [] && closer t in if !opens = [] && t.loc.Loc.line > p.loc.Loc.eline && not ends_block then begin let spaced_after = i + 1 < n && arr.(i + 1).loc.Loc.line = t.loc.Loc.line && arr.(i + 1).sp in let continues = (binop p && p.sp) || (binop t && spaced_after) in let top = match indent with | Some c when !first_line && List.length !stack = 1 -> min c (List.hd !stack) | _ -> List.hd !stack in (* A continuation line sits deeper than the statement it continues. One at or left of that statement's column is not read as joining it: that would pull a line into a block it was written outside of, silently. *) if continues && t.loc.Loc.col <= top then failk "continuation" t.loc "%s" (if binop t then Printf.sprintf "this line starts with the operator %s, so it continues the \ line above, but it is not indented past the start of that \ line (column %d). Indent it further to continue the line, \ or give %s a value on its left" (show t.tok) top (show t.tok) else Printf.sprintf "the line above ends with the operator %s, so this line \ continues it, but it is not indented past the start of \ that line (column %d). Indent it further, or finish the \ line above" (show p.tok) top); if not continues then begin first_line := false; let at = point p.loc in let col = t.loc.Loc.col in (* Inside a lambda's brackets, a line at or left of the line its header is on would be a statement beside the lambda. *) (match !frames with (* Left of the block's own column, after the block: the next element of the brackets, which the block must end. *) | fr :: _ when List.length !stack > 1 && col < List.nth !stack (List.length !stack - 2) -> lambda_not_last fr t | fr :: _ when col <= !base -> if t.tok = COMMA then lambda_not_last fr t else failk "lambda-block-left" t.loc "this line starts at column %d and is still inside the \ brackets of the lambda on line %d, whose block is indented \ past column %d. Indent it into the block, or close the \ brackets at the end of the block's last line" col fr.f_arrow.loc.Loc.line !base | _ -> ()); add NEWLINE at; let top = List.hd !stack in if col > top then begin stack := col :: !stack; add INDENT at end else if col < top then begin if col < !base then failk "dedent" t.loc "%s" (if snippet then Printf.sprintf "this line starts at column %d, left of column %d where \ the code sent starts. Its first line sets its left \ edge, and no later line can go left of it: send the \ enclosing form, or line this up at column %d or right \ of it" col !base !base else Printf.sprintf "this line starts at column %d, left of the top level at \ column %d" col !base); let closed = ref top in let rec pop () = match !stack with | top :: (_ :: _ as rest) when col < top -> closed := top; stack := rest; add DEDENT at; pop () | _ -> () in pop (); (* Between a let's column and the block under it: a binding meant for that let, if the let owns the block. *) (if col <> List.hd !stack then let first_on_line j = j = 0 || arr.(j - 1).loc.Loc.eline < arr.(j).loc.Loc.line in let rec owner j = if j < 0 then None else if first_on_line j && arr.(j).loc.Loc.col <= List.hd !stack then Some j else owner (j - 1) in match owner (i - 1) with | Some j when arr.(j).tok = NAME "let" && j + 1 < n && arr.(j).loc.Loc.col = List.hd !stack && binding_shaped arr i -> let nm = arr.(j + 1) in let let_line = arr.(j).loc.Loc.line and name = show nm.tok in if col = nm.loc.Loc.col then let_after_block t.loc ~let_line ~name else let_misaligned t.loc ~let_line ~name ~name_col:nm.loc.Loc.col | _ -> ()); if col <> List.hd !stack then failk "dedent" t.loc "this line starts at column %d, between the block at column \ %d and the one at column %d it would close, so it belongs \ to neither. The enclosing blocks start at column%s %s: line \ it up with one of them" col (List.hd !stack) !closed (if List.length !stack > 1 then "s" else "") (String.concat ", " (List.rev_map string_of_int !stack)) end end end); (match !frames with (* A comma at the top of a lambda's block, on one of the block's lines. *) | fr :: _ when !opens = [] && t.tok = COMMA -> lambda_not_last fr t (* The closer of the brackets a lambda's block is in: the block ends. *) | fr :: rest when !opens = [] && closer t -> let at = point arr.(i - 1).loc in add NEWLINE at; List.iter (fun _ -> add DEDENT at) (List.tl !stack); base := fr.f_base; stack := fr.f_stack; opens := fr.f_opens; frames := rest | _ -> ()); out := t :: !out; (match t.tok with | LP | LB | LC -> opens := t :: !opens | RP | RB | RC -> (match !opens with _ :: r -> opens := r | [] -> ()) | NAME "=>" when !opens <> [] && i + 1 < n && arr.(i + 1).loc.Loc.line > t.loc.Loc.eline -> frames := { f_base = !base; f_stack = !stack; f_opens = !opens; f_arrow = t } :: !frames; (* A snippet cut from the middle of a line starts where its line does in the file, [indent], not where the cut does. *) base := (match indent with | Some c when t.loc.Loc.line = arr.(0).loc.Loc.line -> min c (line_col i) | _ -> line_col i); stack := [ !base ]; opens := [] | _ -> ()) done; (match !frames with | fr :: _ -> let o = match fr.f_opens with o :: _ -> o | [] -> fr.f_arrow in failk "unclosed" o.loc ~notes:[ Loc.note (point arr.(n - 1).loc) "the input ends here, still inside it" ] "unclosed %s: the block of the lambda on line %d ends where this \ bracket closes" (show o.tok) fr.f_arrow.loc.Loc.line | [] -> ()); (if n > 0 then let at = point arr.(n - 1).loc in add NEWLINE at; List.iter (fun _ -> add DEDENT at) (List.tl !stack)); let eof_loc = if n > 0 then point arr.(n - 1).loc else Loc.unknown in add EOF eof_loc; Array.of_list (List.rev !out) (* ── Parsing ───────────────────────────────────────────────────────── *) (* [closed] is where a lambda's block that ended its statement stopped: the block took the line's end with it, so a check for that end passes there. *) type p = { toks : token array; mutable i : int; mutable closed : 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) (* A block's statements, for a lambda's; set once the statement parser is. *) let block_of : (p -> Form.t list) 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 = let t = peek p in if t.tok <> EOF then p.i <- p.i + 1; t let last p = p.toks.(max 0 (p.i - 1)) (* From [l] to the end of the last token consumed. *) let span p (l : Loc.t) = let e = (last p).loc in if e.Loc.eline > l.Loc.line || (e.Loc.eline = l.Loc.line && e.Loc.ecol > l.Loc.col) then { l with Loc.eline = e.Loc.eline; ecol = e.Loc.ecol } else l let mk p l v = Form.make v (span p l) let sym l s = Form.make (Form.Sym s) l (* Where a stray token is, pointing at the real token after a layout one. *) let where_ p = let t = peek p in match t.tok with | NEWLINE | INDENT | DEDENT -> (peek_at p 1).loc | _ -> t.loc let starts_value = function | NAME _ | KW _ | ATOM _ | DATUM _ | LP | LB | LC | UNQ | SPLICE | BNOT | NEG -> true | _ -> false let ends_value = function | RP | RB | RC | COMMA | NEWLINE | EOF | INDENT | DEDENT -> true | _ -> false let negative_literal = function | ATOM (Form.Int i) -> Int64.compare i 0L < 0 | ATOM (Form.Float f) -> f < 0. | _ -> false (* Something followed a complete value where nothing may. The two shapes that get their own sentence are the ones a Lisp hand writes: [a -1] and [f (x)]. *) (* A [?] glued to the name just read, somewhere a type is not: the name is what was meant, [let ok? = 1] or [fn f(ok?: bool)]. *) let mark_after p = let t = peek p in match t.tok, (last p).tok with | QUEST, NAME n when (not t.sp) && p.i > 0 && n <> "" -> let l = (last p).loc in let n = if n.[0] = '.' then String.sub n 1 (String.length n - 1) else n in name_refused ~typed:(type_like n) { l with Loc.eline = t.loc.Loc.eline; ecol = t.loc.Loc.ecol } (n ^ "?") | _ -> () let stray p ~after = mark_after p; let t = peek p in match t.tok with | ATOM _ when t.sp && negative_literal t.tok -> let text = show t.tok in let digits = String.sub text 1 (String.length text - 1) in failk "glued-minus" t.loc "%s is read as the number %s, right after %s with nothing between them. \ To subtract, space the minus: %s - %s. For two values, separate them \ with a comma: %s, %s" text text after after digits after text | LP when t.sp -> failk "spaced-call" t.loc "there is a space before this (, so it does not call %s — a call has \ none. Write %s(...), or put a comma before the ( if it is a separate \ value" after after | LB when t.sp -> failk "spaced-index" t.loc "there is a space before this [, so it does not index %s — indexing has \ none. Write %s[i]" after after | NEWLINE | INDENT | DEDENT | EOF -> failk "unexpected-end" (where_ p) "the line ends after %s, which is not \ finished here" after | NAME "=" -> failk "assign-in-test" t.loc "this = follows %s, where it cannot assign: an assignment is a line \ of its own, with one =. To compare two values, write == instead" after | COLON -> failk "header-colon" t.loc "this line ends in a colon after %s. A header (if, elif, else, while, \ until, for, fn, match, ...) opens its block with no colon; only a call \ takes one, as in f(x):. Remove the colon" after | _ -> failk "unexpected-token" t.loc "%s follows %s, and two values cannot sit side by side here. Separate \ them with a comma, or join them with an operator" (show t.tok) after let expect p tok ~what = let t = peek p in if t.tok = tok then ignore (advance p) else let () = mark_after p in failk "expected" (where_ p) "expected %s here, and found %s" what (show t.tok) let expect_name p s ~what = match (peek p).tok with | NAME n when n = s -> ignore (advance p) | t -> mark_after p; failk "expected" (where_ p) "expected %s here, and found %s" what (show t) (* The lets whose first value is being read, innermost first: the column of the let's first name, the name, and the let's line. A line at that column under the value's block looks like one more binding and is not. *) let let_values : (int * string * int) list ref = ref [] (* The end of a line that is not followed by a block. *) let expect_eol p ~after = if p.i = p.closed then () else match (peek p).tok with | NEWLINE -> ignore (advance p); (match (peek p).tok, !let_values with | INDENT, (name_col, name, let_line) :: _ -> let t = peek_at p 1 in let shaped = match t.tok, (peek_at p 2).tok with | NAME _, (NAME "=" | COLON) | (LB | LC | LP | UNQ), _ -> true | _ -> false in if t.loc.Loc.col = name_col && shaped then let_after_block t.loc ~let_line ~name | _ -> ()); if (peek p).tok = INDENT then failk "stray-indent" (peek_at p 1).loc "this line is indented under %s, which takes no block. A call takes \ an indented block only with a trailing colon, as in %s:" after (* The call itself when [after] is one, [f(a, b)]; else an example. *) (match String.index_opt after '(', String.index_opt after ' ' with | Some i, Some j when i < j -> after | Some _, None when after.[String.length after - 1] = ')' -> after | _ -> "rl/with-drawing()") | EOF -> () | _ -> stray p ~after (* A target's first token, for a message: [(not)] is shown whole. *) let text_of_tok (t : token) = match t.tok with LP -> "the name in parentheses" | tk -> show tk let check_name (t : token) s = if String.contains s ':' then failk "colon-in-name" t.loc "%s has a colon inside it, and a name cannot. A type annotation puts a \ space after the colon: %s" s (match String.index_opt s ':' with | Some i -> String.sub s 0 (i + 1) ^ " " ^ String.sub s (i + 1) (String.length s - i - 1) | None -> s) (* A form's own text, for the "after" half of a message. *) let text_of (f : Form.t) = let file, lines = !source in let l = f.loc in let from_source = if l.Loc.file <> file || l.Loc.line < 1 || l.Loc.line > Array.length lines then None else let text = lines.(l.Loc.line - 1) in let a = l.Loc.col - 1 in let b = if l.Loc.eline = l.Loc.line then l.Loc.ecol - 1 else String.length text in if a < 0 || b > String.length text || b <= a then None else let t = String.trim (String.sub text a (b - a)) in Some (if l.Loc.eline > l.Loc.line then t ^ " ..." else t) in let s = match from_source with Some t -> t | None -> Form.to_source f in if String.length s > 40 then String.sub s 0 37 ^ "..." else s (* An assignment's place that is an optional chain or an unwrap: neither names storage, and the forms they read to would show the reader's names. *) let no_place (e : Form.t) = match e.v with | Form.List ({ v = Form.Sym "?."; _ } :: _) -> failk "chain-assign" e.loc "%s is an optional chain, and a chain cannot be assigned to: when it \ holds nothing there is no place to write. Unwrap it first with if let, \ then assign through the name it binds" (text_of e) | Form.List [ { v = Form.Sym "!!"; _ }; _ ] -> failk "chain-assign" e.loc "%s unwraps a value, and a value cannot be assigned to. Unwrap it with \ if let, then assign through the name it binds" (text_of e) | _ -> () let unclosed p c l0 = failk "unclosed" l0 ~notes:[ Loc.note (where_ p) "the input ends here, still inside it" ] "unclosed %C" c let refuse_ws ?(brace = false) loc e = failk "separate-elements" loc "%s has an operator in it and sits in a list separated by spaces, where \ only single values are. Separate the %s with commas: %s" (text_of e) (if brace then "entries" else "elements") (if brace then "{.x a + 1, .y 2}" else "[a - 1, b]") (* [loop] and [recur] are Lisp-syntax forms. A .fln loop is a [while], [until], [dotimes] or [for]; [read_all] refuses any that gets past the parser, in a [quote] or a quoted datum too. *) let no_loop loc word = failk "no-loop" loc "%s is not part of the indented syntax. A loop here is a while, until, \ dotimes or for, with let variables it changes:\n\n\ \ let i = 0\n let total = 0\n while i < 10\n total += i\n i += 1\n\n\ break leaves the loop early, and continue goes on to the next round." word (* A [when] has one branch; an else under one is an if's. *) let when_else p = failk "when-else" (peek p).loc "a when has no else — it answers Some of its value when the test holds \ and None when it does not. For two branches write if c then a else b, \ or an if with an else block" (* A refused chain written out as the [and] of all its tests. A middle operand that is more than a name or a literal is named by a [let] first, so the rewrite does not run it twice. *) let and_rewrite (xs : Form.t list) ops = let n = List.length xs in let lets = ref [] in let texts = List.mapi (fun i (x : Form.t) -> let plain = match x.v with Form.List _ | Form.Vec _ | Form.Map _ -> false | _ -> true in if plain || i = 0 || i = n - 1 then text_of x else begin let m = if !lets = [] then "mid" else Printf.sprintf "mid%d" (List.length !lets + 1) in lets := Printf.sprintf " let %s = %s\n" m (text_of x) :: !lets; m end) xs in let rec tests = function | a :: (b :: _ as rest), op :: ops -> Printf.sprintf "%s %s %s" a op b :: tests (rest, ops) | _ -> [] in String.concat "" (List.rev !lets) ^ " " ^ String.concat " and " (tests (texts, ops)) (* Expressions come back with their syntactic level: 13 an atom or a bracket, 12 a postfix chain, 11 a prefix [-] or [~~], 1-10 a binary operator's level, 3 a [not], 0 a one-line [if] or a lambda. Anything under 11 is "compound": it has an operator at its top, so it cannot sit in a list separated only by whitespace. *) let rec expr p : Form.t * int = binary p 1 and binary p lvl : Form.t * int = if lvl = 3 then not_ p else if lvl > 10 then unary p else let l0 = (peek p).loc in let ((first, _) as fst_) = operand p lvl in let close op operands = match List.rev operands with | [ x ] -> (x, lvl) | ops -> if op = "!=" && List.length ops > 2 then failk "chained-not-equal" l0 "a != b != c is not read. != with more than two values means all \ of them are distinct, which is not what the chain says, so it is \ written as a call: !=(a, b, c)"; (mk p l0 (Form.List (sym l0 (op_sym op) :: ops)), lvl) in (* An operator glued to a parenthesis is a call, [+(a, b)], and never the operator between two values. *) let binary_here s = binop_level s = Some lvl && not ((peek_at p 1).tok = LP && not (peek_at p 1).sp) in let operator s = let ot = advance p in if not (ot.sp && (peek p).sp) then failk "unspaced-operator" ot.loc "%s is an operator here, and a binary operator has a space on each \ side: a %s b. Without them a-b is one name" s s; let rhs, _ = operand p lvl in (ot, rhs) in let rec run op operands = match (peek p).tok with | NAME s when binary_here s -> let _, rhs = operator s in if s = op then run op (rhs :: operands) else let folded, _ = close op operands in run s [ rhs; folded ] | _ -> close op operands in (* A comparison chain is read whole, then judged: one operator throughout is the variadic call, one direction is [cmp_chain], anything else is refused at the first operator that breaks it. *) let rec chain acc = match (peek p).tok with | NAME s when binary_here s -> let ot, rhs = operator s in chain ((s, ot, rhs) :: acc) | _ -> List.rev acc in let comparison () = let links = chain [] in let ops = List.map (fun (s, _, _) -> s) links in let xs = first :: List.map (fun (_, _, x) -> x) links in let op1 = List.hd ops in if List.for_all (( = ) op1) ops then close op1 (List.rev xs) else let d = cmp_dir op1 in Array.iteri (fun i (op, (ot : token), _) -> if i > 0 && (d = None || cmp_dir op <> d) then begin let prev, _, _ = List.nth links (i - 1) in failk "mixed-comparison" ot.loc "%s follows %s in one chain. A chain may repeat one operator, \ or mix < with <=, or > with >=, as in 0 <= i < n. Write this \ one as tests joined with and:\n\n%s" op prev (and_rewrite xs ops) end) (Array.of_list links); (cmp_chain ~fresh:cmp_fresh (span p l0) xs ops, lvl) in (* [run] folds a different operator at the same level into the left operand, so the first operator here only starts the first run. *) match (peek p).tok with | NAME s when binary_here s && (cmp_dir s <> None || s = "==" || s = "!=") -> comparison () | NAME s when binary_here s -> run s [ first ] | _ -> fst_ (* An operand of level [lvl]'s operators. [??] sits between the comparisons (4) and the bit operators (5), Swift's place for it: [a ?? b == c] is [(a ?? b) == c] and [a ?? b + 1] is [a ?? (b + 1)]. A chain is one variadic form, [(?? a b c)], which the checker reads from the right. *) and operand p lvl = if lvl <> 4 then binary p (lvl + 1) else let l0 = (peek p).loc in let ((first, _) as fp) = binary p 5 in let rec more acc = 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: a ?? b"; let rhs, _ = binary p 5 in more (rhs :: acc) | _ -> List.rev acc in match more [] with | [] -> fp | rest -> (mk p l0 (Form.List (sym l0 "??" :: first :: rest)), 4) and not_ p = let t = peek p in match t.tok with | NAME "not" when (peek_at p 1).sp && starts_value (peek_at p 1).tok -> ignore (advance p); let x, _ = not_ p in (mk p t.loc (Form.List [ sym t.loc "not"; x ]), 3) | _ -> binary p 4 and unary p = let t = peek p in match t.tok with | NEG -> ignore (advance p); let x, _ = postfix p in (mk p t.loc (Form.List [ sym t.loc "-"; x ]), 11) | BNOT -> ignore (advance p); let x, _ = unary p in (mk p t.loc (Form.List [ sym t.loc "bit-not"; x ]), 11) | _ -> postfix p and postfix p = let l0 = (peek p).loc in let rec loop ((f, _) as fp) = let t = peek p in if t.sp then fp else match t.tok with | LP -> ignore (advance p); let args = items p RP t.loc ~what:"arguments" in loop (mk p l0 (Form.List (f :: args)), 12) | LB -> ignore (advance p); let idx = index_items p t.loc ~head:(text_of f) in loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 12) | NAME s when String.length s > 1 && s.[0] = '.' -> ignore (advance p); loop (mk p l0 (Form.List [ sym t.loc s; f ]), 12) | LC -> ignore (advance p); let m = map_items p t.loc in loop (mk p l0 (Form.List [ f; Form.make (Form.Map m) (span p t.loc) ]), 12) (* [a?.b.c(x)] and [a?[i]]: the rest of the chain is read over a fresh name, [~o1], bound to what [a] holds — [(?. [~o1 a] (.c ...))]. No reader can produce a [~] name, so it shadows nothing. A [?.] later in the rest nests, and the checker flattens it. *) | QUEST when (let n = peek_at p 1 in (not n.sp) && (match n.tok with | NAME s -> String.length s > 1 && s.[0] = '.' | LB -> true | _ -> false)) -> ignore (advance p); incr opt_n; let h = Printf.sprintf "~o%d" !opt_n in let rest, _ = loop (sym t.loc h, 12) in (mk p l0 (Form.List [ sym t.loc "?."; Form.make (Form.Vec [ sym t.loc h; f ]) f.loc; rest ]), 12) (* [T?] where a type is read, and after what can only be a type where a value is, [vec-new(i32?)]. On a value, [x?] tests that it holds one (decision 133): [(? x)]. *) | QUEST -> let typish = match f.v with | Form.Sym n -> type_like n | Form.Vec _ -> true | Form.List ({ v = Form.Sym h; _ } :: _) -> h <> "" && h.[0] >= 'A' && h.[0] <= 'Z' | _ -> false in ignore (advance p); if !in_type || typish then loop (mk p l0 (Form.List [ sym l0 "Option"; f ]), 12) else loop (mk p l0 (Form.List [ sym t.loc "?"; f ]), 12) | BANG -> ignore (advance p); loop (mk p l0 (Form.List [ sym t.loc "!!"; f ]), 12) | _ -> fp in loop (primary p) and primary p : Form.t * int = let t = peek p in let l0 = t.loc in match t.tok with | NAME s -> let nxt = peek_at p 1 in let glued_lp = nxt.tok = LP && not nxt.sp in if (s = "if" || s = "when") && nxt.sp && starts_value nxt.tok then if_expr p else if s = "fn" && glued_lp then fn_expr p (* Only the Lisp loop's spellings are refused here, for a message at the word: [loop x = a, ...], [loop([...]):], a bare [loop] over a block where a statement or a let's value starts, and [recur(...)]. Anywhere else [loop] and [recur] are names; [refuse_loops] catches the rest. *) else if glued_lp && (s = "loop" || s = "recur") then no_loop l0 s else if s = "loop" && ((nxt.sp && (match nxt.tok with NAME x -> not (is_op_word x) | _ -> false) && (match (peek_at p 2).tok with NAME "=" | COMMA -> true | _ -> false)) || (nxt.tok = NEWLINE && (peek_at p 2).tok = INDENT && (p.i = 0 || (match (last p).tok with | NEWLINE | INDENT | DEDENT | NAME "=" -> true | _ -> false)))) then no_loop l0 s else if is_op_word s then begin if glued_lp || ends_value nxt.tok then begin ignore (advance p); (sym l0 (op_sym s), 13) end else if s = "not" && nxt.sp && starts_value nxt.tok then begin (* [a == not b]: [not] binds looser than the operator before it, so it cannot start that operator's right side. The fix is the line with the [not] and its operand in parentheses. *) ignore (advance p); let x, _ = not_ p in let e = (last p).loc in let _, lines = !source in let fix = if e.Loc.eline <> l0.Loc.line || l0.Loc.line > Array.length lines then "(not " ^ text_of x ^ ")" else let line = lines.(l0.Loc.line - 1) in let a = l0.Loc.col - 1 and b = e.Loc.ecol - 1 in String.trim (String.sub line 0 a ^ "(" ^ String.sub line a (b - a) ^ ")" ^ String.sub line b (String.length line - b)) in failk "not-operand" l0 "not follows an operator here, and it binds looser than any \ operator but and and or, so it cannot start that operator's right \ side. Put it in parentheses with what it negates:\n\n %s" fix end else failk "operator-operand" l0 "%s is an operator, and nothing is on its left. As a value on its \ own it goes before a comma or a closing bracket, reduce(%s, xs); \ as a call it is glued to its parenthesis, %s(a, b)" s s s end else begin ignore (advance p); check_name t s; (sym l0 s, 13) end | KW k -> ignore (advance p); (Form.make (Form.Kw k) l0, 13) | ATOM v -> ignore (advance p); (Form.make v l0, if negative_literal t.tok then 11 else 13) | DATUM f -> ignore (advance p); (f, 13) | LP -> ignore (advance p); if (peek p).tok = RP then begin ignore (advance p); (mk p l0 (Form.List []), 13) end else let e, _ = expr p in (match (peek p).tok with | RP -> ignore (advance p) | EOF -> unclosed p '(' l0 | COMMA -> failk "tuple" (peek p).loc "parentheses group one value, and this comma starts a second. \ Several values in a list are written in brackets, [a, b]; \ arguments go glued to a name, f(a, b)" | _ -> stray p ~after:(text_of e)); (e, 13) | LB -> ignore (advance p); let xs = vec_items p l0 in (mk p l0 (Form.Vec xs), 13) | LC -> ignore (advance p); let xs = map_items p l0 in (mk p l0 (Form.Map xs), 13) | UNQ | SPLICE -> ignore (advance p); let x, _ = primary p in let name = if t.tok = UNQ then "unquote" else "unquote-splicing" in (mk p l0 (Form.List [ sym l0 name; x ]), 13) | NEG | BNOT -> unary p | tk -> failk "expected-value" (where_ p) "expected a value here, and found %s" (show tk) (* [if c then a else b]: the one-line form, for a value. [if let P = v then a else b] and [when c then a] too. *) and if_expr p = let t = advance p in let word = match t.tok with NAME w -> w | _ -> "if" in let letp = if word = "if" then if_let_head p else None in let c = match letp with Some m -> m | None -> fst (binary p 1) in let letp, c = match letp with | Some _ -> (letp, c) | None -> (match as_head p c with Some m when word = "if" -> (Some m, m) | _ -> (None, c)) in (match (peek p).tok with | NAME "then" -> ignore (advance p) | _ -> failk "if-then" (where_ p) "an %s inside a line is %s, and there is no then \ after %s. Write the then, or start the %s on its own line with its \ branches indented under it" word (if word = "when" then "when c then a" else "if c then a else b") (text_of c) word); let a = inline_stmt p in match (peek p).tok with | NAME ("else" | "elif") when word = "when" -> when_else p | NAME "else" -> ignore (advance p); let b = inline_stmt p in (if_let_wrap letp (mk p t.loc (Form.List [ sym t.loc "if"; c; a; b ])), 0) | 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" | _ -> (if_let_wrap letp (mk p t.loc (Form.List [ sym t.loc "when"; c; a ])), 0) (* [if let P = v]: after the [if], the pattern and the value, as the one form [[P v]] that stands where the test would. [None] when no [let] follows. *) and if_let_head p = match (peek p).tok, (peek_at p 1) with | NAME "let", n when n.sp -> let lt = advance p in let pat, _ = unary p in expect_name p "=" ~what:"= and the value the pattern is matched against"; let v, _ = binary p 1 in (match pat.v with | Form.Sym g when g <> "" && g.[0] >= 'a' && g.[0] <= 'z' && not (String.contains g '.') && g <> "true" && g <> "false" -> failk "if-let-name" pat.loc "if let %s = %s has no pattern to test. To test that %s holds a \ value, write if %s?, and in the block it is what it holds; to name \ what it holds, write if %s? as %s" g (text_of v) (text_of v) (text_of v) (text_of v) g | _ -> ()); Some (mk p lt.loc (Form.Vec [ pat; v ])) | _ -> None (* [e? as g]: after a test [e?], the name what [e] holds is bound to, as the head [[g e]] an [if let] over a plain name stands as (decision 133). *) and as_head p (c : Form.t) = match (peek p).tok with | NAME "as" -> let at = advance p in (match c.v with | Form.List [ { v = Form.Sym "?"; _ }; e ] -> let g = match (peek p).tok with | NAME g when g <> "" && g.[0] <> '.' -> let gt = advance p in check_name gt g; sym gt.loc g | tk -> failk "as-name" (where_ p) "as takes the name to bind, and found %s" (show tk) in Some (Form.make (Form.Vec [ g; e ]) c.loc) | _ -> 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)) | _ -> None (* The if an [if let] head was read into, rewritten to (if-let [P v] then else): [(if [P v] a b)], [(when [P v] body ...)] and an elif chain's [(cond [P v] a c2 b2 ...)], whose rest is the else. *) and if_let_wrap letp (f : Form.t) = match letp with | None -> f | Some m -> let il (h : Form.t) items = { f with Form.v = Form.List (sym h.Form.loc "if-let" :: m :: items) } in let body (h : Form.t) = function | [ x ] -> x | (x : Form.t) :: _ as xs -> Form.make (Form.List (sym x.Form.loc "do" :: xs)) x.Form.loc | [] -> Form.make (Form.List [ sym h.Form.loc "do" ]) h.Form.loc in match f.Form.v with | Form.List (({ v = Form.Sym "if"; _ } as h) :: c :: rest) when c == m -> il h rest | Form.List (({ v = Form.Sym "when"; _ } as h) :: c :: b) when c == m -> il h [ body h b ] | Form.List (({ v = Form.Sym "cond"; _ } as h) :: c :: b1 :: rest) when c == m -> (match rest with | [] -> il h [ b1 ] | [ { v = Form.Kw "else"; _ }; e ] -> il h [ b1; e ] | (c2 : Form.t) :: _ -> il h [ b1; Form.make (Form.List (sym c2.Form.loc "cond" :: rest)) c2.Form.loc ]) | _ -> f (* What a one-line slot takes — a match arm's value, a then or an else, the thing after defer: a value, or one of the statements that fit on a line, break, continue, return and an assignment. *) (* A bare [()] written where a body goes — a one-line slot, a function's [= ()] — is the empty statement, [(do)], as it is on a line of its own: "do nothing" is what it says there. [(())] stays a value. *) and unit_slot p i0 (t0 : token) (e : Form.t) = if p.i - i0 = 2 && t0.tok = LP && e.v = Form.List [] then Form.make (Form.List [ sym t0.loc "do" ]) e.loc else e and inline_stmt p : Form.t = let t = peek p in let glued = let n = peek_at p 1 in n.tok = LP && not n.sp in match t.tok with | NAME (("break" | "continue") as w) when not glued -> ignore (advance p); (match (peek p).tok with | KW k -> let kt = advance p in mk p t.loc (Form.List [ sym t.loc w; Form.make (Form.Kw k) kt.loc ]) | _ -> mk p t.loc (Form.List [ sym t.loc w ])) | NAME "return" when not glued -> ignore (advance p); let n = peek p in if starts_value n.tok && not (n.tok = NAME "else") then let v, _ = expr p in mk p t.loc (Form.List [ sym t.loc "return"; v ]) else mk p t.loc (Form.List [ sym t.loc "return" ]) | _ -> let i0 = p.i in let e, _ = expr p in match (peek p).tok with | NAME "=" -> no_place e; let eq = advance p in let v, _ = expr p in mk p t.loc (Form.List [ sym eq.loc "set"; e; v ]) | NAME op when List.mem_assoc op assign_ops -> no_place e; let eq = advance p in let v, _ = expr p in mk p t.loc (compound eq.loc (List.assoc op assign_ops) e v (span p e.loc)) | _ -> unit_slot p i0 t e (* [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 let rp = last p in let names = List.for_all (fun (a : Form.t) -> match a.v with Form.Sym _ -> true | _ -> false) args in let header () = "fn(" ^ String.concat ", " (List.map text_of args) ^ ")" in let n = peek p in match n.tok with | NAME "=>" -> ignore (advance p); let ps = lambda_params args in let body = lambda_body p ~header:(header ()) in (mk p t.loc (Form.List (sym t.loc "fn" :: Form.make (Form.Vec ps) (span_of_list lp.loc args) :: body)), 0) | NAME "=" when names -> lambda_equals p (header ()) | NAME w when names && glued_arrow w -> lambda_glued n (header ()) w | NEWLINE when names && (peek_at p 1).tok = INDENT -> lambda_arrow (peek_at p 2).loc (header ()) (* Inside brackets a line break is no token: the next line's first token is what follows. *) | tk when names && n.loc.Loc.line > rp.loc.Loc.eline && starts_value tk -> lambda_arrow n.loc (header ()) | _ -> (mk p t.loc (Form.List (sym t.loc "fn" :: args)), 12) (* What follows a lambda's [=>]: a value on the line, or the indented block under it. [header] is the lambda's header as written, for a message. *) and lambda_body p ~header = match (peek p).tok, (peek_at p 1).tok with | NEWLINE, INDENT -> ignore (advance p); let body = !block_of p in p.closed <- p.i; body | (NEWLINE | EOF | DEDENT), _ -> failk "lambda-body" (where_ p) "the line ends after %s =>, and the lambda's body is not under it. Put \ the body after the =>, or on the lines under it, indented:\n\n\ \ %s =>\n ..." header header | _ -> let i0 = p.i and t0 = peek p in let body, _ = expr p in [ unit_slot p i0 t0 body ] (* [fn(a) = x]: a lambda written with a named function's [=]. *) and lambda_equals : 'a. p -> string -> 'a = fun p header -> let eq = advance p in let body = match expr p with | b, _ -> text_of b | exception _ -> "..." in failk "lambda-equals" eq.loc "a lambda's body follows =>, and this one has =, which is how a named \ function is written. Write:\n\n %s => %s" header body (* [fn(a) =>x]: the body glued to the arrow reads as one name. *) and glued_arrow w = String.length w > 2 && String.sub w 0 2 = "=>" and lambda_glued : 'a. token -> string -> string -> 'a = fun t header w -> failk "lambda-arrow-space" t.loc "%s is one name, with nothing between => and the body. Put a space \ after the arrow: %s => %s" w header (String.sub w 2 (String.length w - 2)) (* A lambda header with lines under it and no [=>]. *) and lambda_arrow : 'a. Loc.t -> string -> 'a = fun at header -> failk "lambda-arrow" at "the lines under %s are a lambda's body only after =>. End the header \ with it:\n\n %s =>\n ..." header header (* 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 | (x : Form.t) :: _ -> { l with Loc.eline = x.loc.Loc.eline; ecol = x.loc.Loc.ecol } and lambda_params args = List.map (fun (a : Form.t) -> match a.v with | Form.Sym _ -> a | _ -> failk "lambda-param" a.loc "a lambda's parameter is a name, and this is %s. Take the value \ under a name and destructure it in the body" (text_of a)) 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. *) and items p closer open_loc ~what = let opener = if closer = RB then '[' else '(' in let rec go acc = let t = peek p in if t.tok = closer then (ignore (advance p); List.rev acc) else if t.tok = EOF then unclosed p opener open_loc else match t.tok, peek_at p 1 with | NAME "const", n when n.sp && starts_value n.tok -> ignore (advance p); go (sym t.loc "const" :: acc) | _ -> let e, _ = expr p in (match (peek p).tok with | COMMA -> ignore (advance p); go (e :: acc) | tk when tk = closer -> ignore (advance p); List.rev (e :: acc) | EOF -> unclosed p opener open_loc | _ -> let n = peek p in if starts_value n.tok && n.sp && not (negative_literal n.tok) && n.loc.Loc.line > e.loc.Loc.eline then (* Most often the bracket was never closed: the next statement has been read as one more argument. *) failk "missing-comma" n.loc "%s on a new line follows %s with no comma between them. If the \ %c on line %d was meant to close before this line, close it; \ otherwise separate %s with commas" (show n.tok) (text_of e) opener open_loc.Loc.line 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: f(a, b)" (show n.tok) (text_of e) what 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 >= 11) 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 < 11) 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 comma was meant to part is a guess. *) let commas = ref false and spaces = ref false in let mixed at = failk "mixed-separators" at "this bracket separates some elements with commas and some with only \ spaces. Use one: [1, 2, 3] or [1 2 3]" in let rec go acc prev_ws = 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 if lvl < 11 && prev_ws then refuse_ws t.loc e; (match (peek p).tok with | COMMA -> if !spaces then mixed (peek p).loc; commas := true; ignore (advance p); go (e :: acc) false | RB -> ignore (advance p); List.rev (e :: acc) | EOF -> unclosed p '[' open_loc | tk when starts_value tk && (peek p).sp -> if lvl < 11 then refuse_ws t.loc e; if !commas then mixed (peek p).loc; spaces := true; go (e :: acc) true | _ -> stray p ~after:(text_of e)) in go [] false (* Braces pair a key with a value, so a value may be any expression; after one that has an operator in it, the next entry needs a comma. *) and map_items p open_loc = let rec go acc = let t = peek p in match t.tok with | RC -> ignore (advance p); List.rev acc | EOF -> unclosed p '{' open_loc | _ -> let e, lvl = expr p in (match (peek p).tok with | COMMA -> ignore (advance p); go (e :: acc) | RC -> ignore (advance p); List.rev (e :: acc) | EOF -> unclosed p '{' open_loc (* [P{x = 1}] or [P{x: 1}]: another language's field syntax. *) | (NAME "=" | COLON) as tk when (match e.v with Form.Sym n -> n <> "" && n.[0] <> '.' | _ -> false) -> let n = text_of e in failk "brace-field" (peek p).loc "a field in braces is written {.%s value}: a dot before the name, \ and no %s between it and the value" n (if tk = COLON then "colon" else "= sign") | tk when starts_value tk && (peek p).sp -> if lvl < 11 then refuse_ws ~brace:true t.loc e; go (e :: acc) | _ -> stray p ~after:(text_of e)) in go [] (* A type after [:] or [->]: a postfix term, plus the arrow of a function type, [Fn(A, B) -> R], which reads as [(Fn [A B] R)]. *) let rec ty p : Form.t = let l0 = (peek p).loc in let was = !in_type in in_type := true; let f, _ = Fun.protect ~finally:(fun () -> in_type := was) (fun () -> postfix p) in match f.v, (peek p).tok with | Form.List (({ v = Form.Sym ("Fn" | "CFn"); _ } as h) :: args), NAME "->" when (last p).tok = RP -> ignore (advance p); let r = ty p in mk p l0 (Form.List [ h; Form.make (Form.Vec args) h.loc; r ]) | _ -> f (* ── Statements ────────────────────────────────────────────────────── *) (* The let-statements this reader built, so that a [let] whose whole body is another one merges into one binding vector (spec §2), and a [let] written as a call does not. *) type st = { p : p; mutable lets : Form.t list } (* Whether the code line before [t] is a one-line [if c then a] with no else: an else under it reads as written for that if, and is not. *) let one_line_if_above p (t : token) = let layout = function NEWLINE | INDENT | DEDENT -> true | _ -> false in let rec prev j = if j < 0 then None else let u = p.toks.(j) in if u.loc.Loc.line < t.loc.Loc.line && not (layout u.tok) then Some u.loc.Loc.line else prev (j - 1) in match prev (p.i - 1) with | None -> false | Some l -> let rec line j acc = if j < 0 || p.toks.(j).loc.Loc.line < l then acc else line (j - 1) (if layout p.toks.(j).tok || p.toks.(j).loc.Loc.line > l then acc else p.toks.(j).tok :: acc) in (match line (p.i - 1) [] with | NAME "if" :: rest -> List.mem (NAME "then") rest && not (List.mem (NAME "else") rest) | _ -> false) (* Whether the code line before [t] is [else if c then a]: that else took the one-line if as its value, and an else under it has no if left. *) let else_if_above p (t : token) = let layout = function NEWLINE | INDENT | DEDENT -> true | _ -> false in let rec prev j = if j < 0 then None else let u = p.toks.(j) in if u.loc.Loc.line < t.loc.Loc.line && not (layout u.tok) then Some u.loc.Loc.line else prev (j - 1) in match prev (p.i - 1) with | None -> false | Some l -> let rec first j = if j <= 0 || p.toks.(j - 1).loc.Loc.line < l then j else first (j - 1) in let j = first (p.i - 1) in let j = if layout p.toks.(j).tok then j + 1 else j in j + 1 < Array.length p.toks && p.toks.(j).tok = NAME "else" && p.toks.(j + 1).tok = NAME "if" (* A block of several lines is a [do] spanning its lines, from the first statement to the end of the last — not from the header above it, which is another form's. *) let blk (s : st) l (ss : Form.t list) = match ss with | [ x ] -> x | (first : Form.t) :: _ -> mk s.p first.loc (Form.List (sym first.loc "do" :: ss)) | [] -> mk s.p l (Form.List [ sym l "do" ]) (* The word at the head of the line is a variable being assigned, [data = 3] or [on += 1], whatever else it could start. *) let assigns p = let n = peek_at p 1 in n.sp && (match n.tok with NAME x -> x = "=" || List.mem_assoc x assign_ops | _ -> false) let header_follow p s = let n = peek_at p 1 in (not (assigns p)) && let plain_name = function | NAME x -> not (is_op_word x || x = "=" || List.mem_assoc x assign_ops) | _ -> false in match s with | "fn" | "fn-" | "def" | "once" | "const" | "struct" | "union" | "data" | "enum" | "import" -> n.sp && plain_name n.tok | "if" | "when" | "while" | "until" | "match" | "let" | "for" -> n.sp && starts_value n.tok && (match n.tok with | NAME x when x = "=" || List.mem_assoc x assign_ops -> false | NAME x when is_binop x -> let a = peek_at p 2 in a.tok = LP && not a.sp | _ -> true) (* [macro name(...)]: the name and its glued parenthesis. *) | "macro" -> n.sp && plain_name n.tok && (let a = peek_at p 2 in a.tok = LP && not a.sp) (* [class Lambda(...)] or [class Lambda] over its slot lines. *) | "class" -> n.sp && plain_name n.tok (* [generic describe(v)], [multi kind(v)], [method describe(f: C)]: the name and its glued parenthesis. *) | "generic" | "multi" | "method" -> n.sp && plain_name n.tok && (let a = peek_at p 2 in a.tok = LP && not a.sp) (* [type Row = Vec(i32)]: a name and its [=]. *) | "type" -> n.sp && plain_name n.tok && (peek_at p 2).tok = NAME "=" | "return" -> n.tok = NEWLINE || (n.sp && starts_value n.tok) | "break" | "continue" -> n.tok = NEWLINE || (n.sp && (match n.tok with KW _ -> true | _ -> false)) | "defer" -> (n.tok = NEWLINE && (peek_at p 2).tok = INDENT) || (n.sp && starts_value n.tok) | "handler-case" | "handler-bind" | "restart-case" -> n.tok = NEWLINE || (n.sp && starts_value n.tok) | "quote" -> (n.tok = NEWLINE && (peek_at p 2).tok = INDENT) || (n.sp && starts_value n.tok) | _ -> false let name_tok p ~what = let t = peek p in match t.tok with | NAME s when not (String.length s > 0 && s.[0] = '.') -> ignore (advance p); check_name t s; sym t.loc s | tk -> failk "expected-name" (where_ p) "expected %s here, and found %s" what (show tk) let glued_lp p ~what = let t = peek p in if t.tok = LP && not t.sp then advance p else failk "expected" (where_ p) "expected %s here, and found %s" what (show t.tok) (* [(a: i32, b)] as name/type pairs, [dyn] written out for the untyped: the reader never leaves a vector for [Check.pair_params] to guess at. *) let params p (lp : token) = let rec go acc = let t = peek p in match t.tok with | RP -> ignore (advance p); List.rev acc | EOF -> unclosed p '(' lp.loc | _ -> (match t.tok with | NAME "&" -> failk "rest-parameter" t.loc "a function's parameters are a fixed list of names, each with an \ optional : Type, and & (a rest parameter) is not one. Take the rest \ as one parameter, xs: [T]" | _ -> ()); let n = name_tok p ~what:"a parameter's name" in let typed = (peek p).tok = COLON in let tyf = match (peek p).tok with | COLON -> ignore (advance p); ty p | _ -> sym n.loc "dyn" in (match (peek p).tok with | COMMA -> ignore (advance p) | RP -> () | _ -> stray p ~after:(text_of (if typed then tyf else n))); go (tyf :: n :: acc) in go [] (* [(a, b: T)] as each name and its type when one is written. *) let named_params p (lp : token) = let rec go acc = let t = peek p in match t.tok with | RP -> ignore (advance p); List.rev acc | EOF -> unclosed p '(' lp.loc | _ -> let n = name_tok p ~what:"a parameter's name" in let tyf = match (peek p).tok with | COLON -> ignore (advance p); Some (ty p) | _ -> None in (match (peek p).tok with | COMMA -> ignore (advance p) | RP -> () | _ -> stray p ~after:(text_of (match tyf with Some t -> t | None -> n))); go ((n, tyf) :: acc) 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. *) 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 params_text () = 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 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" (params_text ()) in let header () = "fn(" ^ params_text () ^ ") -> " ^ text_of r 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 body = lambda_body p ~header:(header ()) in (wrap body, 0) | NAME "=" -> lambda_equals p (header ()) | NAME w when glued_arrow w -> lambda_glued (peek p) (header ()) w | NEWLINE when (peek_at p 1).tok = INDENT -> lambda_arrow (peek_at p 2).loc (header ()) (* Inside brackets a line break is no token: the next line's first token is what follows. *) | tk when (peek p).loc.Loc.line > (last p).loc.Loc.eline && tk <> EOF -> lambda_arrow (peek p).loc (header ()) | _ -> failk "lambda-body" (where_ p) "a lambda's body follows => on its line, or is the block under it: \ %s => value" (header ()) let rec stmts (s : st) : Form.t list = let p = s.p in match (peek p).tok with | DEDENT -> ignore (advance p); [] | EOF -> [] | NAME "let" when header_follow p "let" -> let_stmt s | _ -> let f = stmt s in f :: stmts s and block (s : st) ~after : Form.t list = let p = s.p in match (peek p).tok with | INDENT -> ignore (advance p); stmts s | _ -> failk "expected-block" (where_ p) "%s takes an indented block on the lines under it, and the next line is \ not indented" after (* The rest of a line read as a value, through its end: [= v], or [=] and an indented block that reduces to one form, or a lambda with a block body. *) and value_line ?(block_ok = false) (s : st) ~after : Form.t = let p = s.p in let l0 = where_ p in if (peek p).tok = NEWLINE && (peek_at p 1).tok = INDENT then begin ignore (advance p); blk s l0 (block s ~after) end else match (peek p).tok with (* [let r = match a] with its arms under it, and [let r = if c] with its branches: a header read as the value, block and all. *) | NAME (("match" | "handler-case" | "handler-bind" | "restart-case") as w) when header_follow p w -> header s w | NAME (("if" | "when") as w) when header_follow p w && not (then_on_line p) -> header s w | _ -> let e, _ = expr p in match (peek p).tok with (* [let v = with-foo(a):] and its block: the call takes the block, as it would on a line of its own. *) | COLON when (match e.v, (last p).tok with | Form.List (_ :: _), RP | Form.Sym _, NAME _ -> true | _ -> false) -> ignore (advance p); (match (peek p).tok with | NEWLINE -> ignore (advance p) | _ -> stray p ~after:":"); let body = block s ~after:(text_of e ^ ":") in (match e.v with | Form.List items -> mk p e.loc (Form.List (items @ body)) | _ -> mk p e.loc (Form.List (e :: body))) | COMMA -> let c = (peek p).loc in (* On a let's line, the fix is the let's group: the rest of the line under the first name. *) let group = let file, lines = !source in if c.Loc.file <> file || c.Loc.line > Array.length lines then None else let line = lines.(c.Loc.line - 1) in let head = String.trim (String.sub line 0 (c.Loc.col - 1)) in let rest = String.trim (String.sub line c.Loc.col (String.length line - c.Loc.col)) in if String.length head > 4 && String.sub head 0 4 = "let " && rest <> "" then Some (Printf.sprintf ":\n\n %s\n %s" head rest) else None in failk "one-binding" c "%s is followed by a comma, and one line binds one name. Put each \ binding on its own line%s" (text_of e) (match group with | Some g -> ", the ones after the first under its name" ^ g | None -> ", one after the other") | _ -> lambda_block ~block_ok s e ~after:(text_of e) (* Whether this line has a [then] at depth zero: a one-line if. *) and then_on_line p = let rec go k depth = let t = peek_at p k in match t.tok with | NEWLINE | EOF -> false | NAME "then" when depth = 0 -> true | LP | LB | LC -> go (k + 1) (depth + 1) | RP | RB | RC -> go (k + 1) (max 0 (depth - 1)) | _ -> go (k + 1) depth in go 1 0 (* The end of a statement's line, which a lambda's block may already have taken. *) and lambda_block ?(block_ok = false) (s : st) (e : Form.t) ~after = let p = s.p in if block_ok && p.i <> p.closed && (peek p).tok = NEWLINE then ignore (advance p) else expect_eol p ~after; e (* One binding of a let, [x = v], [x: T = v] or [{a .x} = p], through the end of its line (and the block its value takes). *) and binding (s : st) : Form.t * Form.t = let p = s.p in let target, _ = unary p in (* [let x: T = v] is [(let [x (the T v)])]: a let binding has no type slot of its own, and [the] is the form that says what a value is. *) let annot = match (peek p).tok with | COLON -> ignore (advance p); Some (ty p) | _ -> None in (match (peek p).tok with | NAME "=" -> ignore (advance p) | _ -> failk "let-equals" (where_ p) "a let is let name = value, and %s is not followed by =" (text_of target)); let v = value_line ~block_ok:true s ~after:("let " ^ text_of target) in let v = match annot with | Some tyf -> Form.make (Form.List [ sym tyf.loc "the"; tyf; v ]) (span p tyf.loc) | None -> v in (target, v) (* The lines indented under a let, each one more binding of it: [let a = 1] and under it [b = a + 1], lined up with [a]. Anything else there is refused; [first] is the let's first target, for the message. *) and binding_lines : 'a. ?global:bool -> st -> first:Form.t -> name_col:int -> one:(unit -> 'a) -> 'a list = fun ?(global = false) s ~first ~name_col ~one -> let p = s.p in if (peek p).tok <> INDENT then [] else begin ignore (advance p); let misaligned (t : token) = let_misaligned t.loc ~let_line:first.loc.Loc.line ~name:(text_of first) ~name_col in (* [block] is the binding before this line when its value ended in a block still open at its end — a trailing [fn(x) =>], [match], [if], [x =] or [f():] — which ends the let's bindings. A block lambda whose brackets close after its block does not. *) let rec go ?block acc = match (peek p).tok with | DEDENT -> ignore (advance p); List.rev acc | EOF -> List.rev acc | INDENT when binding_shaped p.toks (p.i + 1) -> misaligned (peek_at p 1) | INDENT -> ignore (advance p); not_binding () | _ when binding_line ~global p -> if (peek p).loc.Loc.col <> name_col then misaligned (peek p); (match block with | Some name -> let_after_block (peek p).loc ~let_line:first.loc.Loc.line ~name | None -> ()); let t0 = peek p in let x = one () in (* The last token the value took, past its line's end. *) let rec last_real k = if k > 0 && p.toks.(k).tok = NEWLINE then last_real (k - 1) else k in let open_block = p.toks.(last_real (p.i - 1)).tok = DEDENT in go ?block:(if open_block then Some (text_of_tok t0) else None) (x :: acc) | _ -> not_binding () and not_binding () = failk "let-block" (where_ p) "this line is indented under let %s, and the only lines that go \ there are more bindings of the let, lined up with its first name:\n\n\ \ let a = 1\n b = a + 1\n\n\ A let's names last to the end of the block the let is in, so any \ other line after it goes at the let's column" (text_of first) in go [] end (* Whether the line at point is a binding: a name, a [[...]] or [{...}] pattern, or an operator word in parentheses, [(not) = 3], then [=] or [: T =]. [x += 1] and [f(x)] are not. A [global]'s line may be [x: T] alone, as a top-level let's may. *) and binding_line ?(global = false) p = (* An [=] at the line's own depth, from token [k] on. *) let rec eq k depth = match (peek_at p k).tok with | EOF | NEWLINE | INDENT | DEDENT -> false | NAME "=" when depth = 0 -> true | LP | LB | LC -> eq (k + 1) (depth + 1) | RP | RB | RC -> eq (k + 1) (depth - 1) | _ -> eq (k + 1) depth in match (peek p).tok with | NAME s when s <> "" && s.[0] <> '.' && not (is_op_word s) -> (match (peek_at p 1).tok with | NAME "=" -> (peek_at p 1).sp | COLON -> global || eq 2 0 | _ -> false) (* [~g = a] in a template. *) | UNQ -> eq 1 0 | LB | LC | LP -> let rec close k depth = match (peek_at p k).tok with | EOF | NEWLINE -> false | LP | LB | LC -> close (k + 1) (depth + 1) | RP | RB | RC when depth = 1 -> (peek_at p (k + 1)).tok = NAME "=" | RP | RB | RC -> close (k + 1) (depth - 1) | _ -> close (k + 1) depth in close 0 0 | _ -> false and let_stmt (s : st) : Form.t list = let p = s.p in let t = advance p in let_gap_tab t (peek p); let name_col = (peek p).loc.Loc.col in let (target, v) = let_values := (name_col, text_of_tok (peek p), t.loc.Loc.line) :: !let_values; Fun.protect ~finally:(fun () -> let_values := List.tl !let_values) (fun () -> binding s) in let more = binding_lines s ~first:target ~name_col ~one:(fun () -> binding s) in let own = List.concat_map (fun (a, b) -> [ a; b ]) ((target, v) :: more) in let make bindings body = let f = mk p t.loc (Form.List (sym t.loc "let" :: Form.make (Form.Vec bindings) (span_of_list target.loc bindings) :: body)) in s.lets <- f :: s.lets; f in let merged body = match body with | [ ({ Form.v = Form.List (_ :: { v = Form.Vec bs; _ } :: body); _ } as inner) ] when List.memq inner s.lets -> make (own @ bs) body | _ -> make own body in [ merged (stmts s) ] and stmt (s : st) : Form.t = let p = s.p in let t = peek p in match t.tok with | NAME w when header_follow p w -> header s w | NAME _ when assigns p -> expr_stmt s | NAME (("else" | "elif") as w) when else_if_above p t -> failk "orphan-else" t.loc "the else above took the one-line if after it as its value, so this %s \ has no if to belong to. Write that line as elif:\n\n\ \ if a then x\n elif b then y\n else z" w | NAME (("else" | "elif") as w) when one_line_if_above p t -> failk "orphan-else" t.loc "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 "%s is not under an if at this column. It goes at the same column as \ the if it belongs to, right after that if's block" w | _ -> expr_stmt s and expr_stmt (s : st) : Form.t = let p = s.p in let i0 = p.i in let t0 = peek p in let e, _ = expr p in match (peek p).tok with | NAME "=" -> no_place e; let eq = advance p in let v = value_line s ~after:(text_of e ^ " =") in mk p t0.loc (Form.List [ sym eq.loc "set"; e; v ]) | NAME op when List.mem_assoc op assign_ops -> no_place e; let eq = advance p in let v = value_line s ~after:(text_of e ^ " " ^ op) in let o = List.assoc op assign_ops in mk p t0.loc (compound eq.loc o e v (span p e.loc)) | COLON -> let before = (last p).tok in let c = advance p in (* [f(x):] and, with no arguments, [comment:] — a bare name — open a block; anything else has no call to hang it on. *) (match e.v, before with | Form.List (_ :: _), RP -> () | Form.Sym _, NAME _ -> () | _ -> failk "colon-block" c.loc "a trailing colon gives a call an indented block, and %s is not a \ call. Write it as one, as in f(x): or comment:" (text_of e)); (match (peek p).tok with | NEWLINE -> ignore (advance p) | _ -> stray p ~after:":"); let body = block s ~after:(text_of e ^ ":") in (match e.v with | Form.List items -> mk p t0.loc (Form.List (items @ body)) | _ -> mk p t0.loc (Form.List (e :: body))) | _ -> (* [()] alone on a line is the empty statement, spec §2 "Unit". *) let e = if p.i - i0 = 2 && t0.tok = LP && e.v = Form.List [] then Form.make (Form.List [ sym t0.loc "do" ]) e.loc else e in lambda_block s e ~after:(text_of e) and header (s : st) w : Form.t = let p = s.p in let t = advance p in let l0 = t.loc in let form items = mk p l0 (Form.List (sym l0 w :: items)) in let named head items = mk p l0 (Form.List (sym l0 head :: items)) in match w with | "fn" | "fn-" -> let name = name_tok p ~what:"the function's name" in let lp = glued_lp p ~what:"the parameters, in parentheses glued to the name" in let ps = params p lp in let rp = last p in (* No arrow reads the return type off the body: the paren syntax's [_] (spec-syntax.md §3.5). *) let ret, ret_text = match (peek p).tok with | NAME "->" -> ignore (advance p); let r = ty p in (r, text_of r) | _ -> (sym rp.loc "_", ")") in let where_clause = match (peek p).tok with | NAME "where" -> let wt = advance p in let rec preds acc = let e, _ = expr p in (* [and] is how a condition joins tests, so it is what gets written for several predicates; the clause separates them with commas. *) (match e.Form.v with | Form.List ({ Form.v = Form.Sym "and"; _ } :: (_ :: _ as ps)) -> let spell (q : Form.t) = match q.Form.v with | Form.List [ { Form.v = Form.Sym n; _ }; { Form.v = Form.Sym v; _ } ] -> Printf.sprintf "%s(%s)" n v | _ -> Form.to_string q in failk "where-and" e.Form.loc "a where clause separates its predicates with commas, not and \ — write where %s" (String.concat ", " (List.map spell ps)) | _ -> ()); match (peek p).tok with | COMMA -> ignore (advance p); preds (e :: acc) | _ -> List.rev (e :: acc) in let es = preds [] in let v = match es with | [ e ] -> e | _ -> Form.make (Form.Vec es) (span p wt.loc) in [ mk p wt.loc (Form.Map [ Form.make (Form.Kw "where") wt.loc; v ]) ] | _ -> [] in let body = match (peek p).tok with | NAME "=" -> ignore (advance p); if (peek p).tok = NEWLINE && (peek_at p 1).tok = INDENT then begin ignore (advance p); block s ~after:"fn" end else begin let i0 = p.i and t0 = peek p in let v = value_line s ~after:"=" in let bare = t0.tok = LP && p.toks.(i0 + 1).tok = RP && v.v = Form.List [] && (match p.toks.(i0 + 2).tok with NEWLINE | EOF | DEDENT -> true | _ -> false) in [ (if bare then Form.make (Form.List [ sym t0.loc "do" ]) v.loc else v) ] end | NEWLINE -> ignore (advance p); if (peek p).tok = INDENT then block s ~after:"fn" else [] | _ -> stray p ~after:ret_text in named (if w = "fn" then "defn" else "defn-") (name :: Form.make (Form.Vec ps) lp.loc :: ret :: (where_clause @ body)) | "def" | "once" | "const" -> def_form s w t l0 | "struct" | "union" -> let name = name_tok p ~what:"the type's name" in (* [struct Pt(x: i32, y: i32)]: the fields on the header's line, as a data case writes them, with no block under it. *) let inline = match (peek p).tok with | LP when not (peek p).sp -> let lp = advance p in Some (params p lp) | _ -> None in (* [struct DiskFull :parent IoError]: a condition's parent, before the fields as in the paren form. *) let parent = match (peek p).tok with | KW "parent" when w = "struct" -> let kt = advance p in let pt = ty p in Some (Form.make (Form.Kw "parent") kt.loc, pt) | _ -> None in let after = match parent, inline with | Some (_, pt), _ -> text_of pt | None, Some _ -> ")" | None, None -> w ^ " " ^ text_of name in let fields = match inline with | Some fs -> expect_eol p ~after; fs | None -> expect_eol_block p ~after; lines s (fun () -> let f = name_tok p ~what:"a field's name" in let tf = match (peek p).tok with | COLON -> ignore (advance p); ty p | _ -> sym f.loc "dyn" in expect_eol p ~after:(text_of tf); [ f; tf ]) in let fv = Form.make (Form.Vec fields) (span p name.loc) in (* No field lines under a parent is the category form, which has no field vector. *) named (if w = "struct" then "defstruct" else "defunion") (match parent with | None -> [ name; fv ] | Some (k, pt) -> name :: k :: pt :: (if fields = [] then [] else [ fv ])) | "class" -> let name = name_tok p ~what:"the class's name" in let ps = match (peek p).tok with | LP when not (peek p).sp -> let lp = advance p in let ps = named_params p lp in expect_eol p ~after:")"; ps | _ -> expect_eol_block p ~after:("class " ^ text_of name); let acc = ref [] in ignore (lines s (fun () -> let f = name_tok p ~what:"a slot's name" in let t = match (peek p).tok with | COLON -> ignore (advance p); Some (ty p) | _ -> None in expect_eol p ~after:(match t with Some t -> text_of t | None -> text_of f); acc := (f, t) :: !acc; [])); List.rev !acc in (* Each slot's name, and its type after it when one is written: the paren form's [(defclass c [a b n i32])], whose untyped slots are dyn. *) let slots = List.concat_map (fun (n, t) -> match t with Some t -> [ n; t ] | None -> [ n ]) ps in named "defclass" [ name; Form.make (Form.Vec slots) (span p name.loc) ] | "generic" | "multi" | "method" -> let name = name_tok p ~what:(Printf.sprintf "the %s's name" w) in let lp = advance p in let ps = named_params p lp in (* A method's first parameter may name the class it answers for; every other parameter of these is dyn, so it takes no type. *) let names = String.concat ", " (List.map (fun ((n : Form.t), _) -> text_of n) ps) in let first = match ps with (n, _) :: _ -> text_of n | [] -> "v" in let first_class = ref None in List.iteri (fun k ((n : Form.t), t) -> match t with | Some (tf : Form.t) when k = 0 && w = "method" -> first_class := Some tf | Some tf -> failk "dyn-parameter" tf.loc "every parameter of a %s is dyn, so %s takes no type: write %s %s(%s)%s" w (text_of n) w (text_of name) (String.concat ", " (List.mapi (fun k ((n : Form.t), t) -> match t with | Some t when k = 0 && w = "method" -> text_of n ^ ": " ^ text_of t | _ -> text_of n) ps)) (if w = "method" then "" else " -> dyn") | None -> ()) ps; let pv = Form.make (Form.Vec (List.map fst ps)) (span p lp.loc) in let ret () = match (peek p).tok with | NAME "->" -> ignore (advance p); ty p | _ -> (* At the end of the header's line, where the arrow goes. *) let e = (last p).loc in failk "generic-return" { e with Loc.line = e.Loc.eline; col = e.Loc.ecol } "a %s states the type every method returns: %s %s(%s) -> dyn" w w (text_of name) names in let body ~after ~prev = match (peek p).tok with | NAME "=" -> ignore (advance p); [ value_line s ~after:"=" ] | NEWLINE -> ignore (advance p); block s ~after | _ -> stray p ~after:prev in (match w with | "generic" -> let r = ret () in expect_eol p ~after:(text_of r); named "defgeneric" [ name; pv; r ] | "multi" -> let r = ret () in named "defmulti" (name :: pv :: r :: body ~after:("multi " ^ text_of name ^ "(...)") ~prev:(text_of r)) | _ -> let key = match (peek p).tok, !first_class with | NAME "when", Some tf -> failk "method-key" (peek p).loc "this method already answers for %s, its first parameter's type. \ Write the type or the when, not both" (text_of tf) | NAME "when", None -> ignore (advance p); fst (unary p) | _, Some tf -> tf | _, None -> failk "method-key" (where_ p) "a method says what it answers for: a class as its first \ parameter's type, method %s(%s: point), or a value after when, \ method %s(%s) when :int" (text_of name) first (text_of name) names in named "defmethod" (name :: key :: pv :: body ~after:("method " ^ text_of name ^ "(...)") ~prev:(if !first_class = None then text_of key else ")"))) | "type" -> let name = name_tok p ~what:"the alias's name" in expect_name p "=" ~what:"= and the type it names"; let t = ty p in expect_eol p ~after:(text_of t); named "defalias" [ name; t ] | "macro" -> let name = name_tok p ~what:"the macro's name" in let lp = glued_lp p ~what:"the parameters, in parentheses glued to the name" in let rec go acc = let t = peek p in match t.tok with | RP -> ignore (advance p); List.rev acc | EOF -> unclosed p '(' lp.loc | _ -> let one = match t.tok with | NAME "&" -> ignore (advance p); [ name_tok p ~what:"the rest parameter's name after &"; sym t.loc "&" ] | LB -> [ fst (primary p) ] | _ -> [ name_tok p ~what:"a parameter's name" ] in (match (peek p).tok with | COMMA -> ignore (advance p) | RP -> () | _ -> stray p ~after:(text_of (List.hd one))); go (one @ acc) in let ps = go [] in let n = List.length ps in List.iteri (fun k (a : Form.t) -> if a.v = Form.Sym "&" && k < n - 2 then begin let r = List.nth ps (k + 1) in let others = List.filteri (fun j _ -> j <> k && j <> k + 1) ps in failk "macro-rest-last" a.loc "& %s takes the arguments left over, so it comes last: macro %s(%s)" (text_of r) (text_of name) (String.concat ", " (List.map text_of others @ [ "& " ^ text_of r ])) end) ps; let pv = Form.make (Form.Vec ps) (span p lp.loc) in expect_line_end p ~after:")"; let body = block s ~after:("macro " ^ text_of name ^ "(...)") in named "defmacro" (name :: pv :: body) | "data" -> let name = name_tok p ~what:"the type's name" in expect_eol_block p ~after:("data " ^ text_of name); let cases = lines s (fun () -> let c = name_tok p ~what:"a case's name" in let f = match (peek p).tok with | LP when not (peek p).sp -> let lp = advance p in let ps = params p lp in mk p c.loc (Form.List [ c; Form.make (Form.Vec ps) lp.loc ]) | _ -> c in expect_eol p ~after:(text_of f); [ f ]) in named "defdata" [ name; Form.make (Form.Vec cases) (span p name.loc) ] | "enum" -> let name = name_tok p ~what:"the enum's name" in expect_eol_block p ~after:("enum " ^ text_of name); let members = lines s (fun () -> let m = name_tok p ~what:"a member's name" in match (peek p).tok with | NAME "=" -> ignore (advance p); let v, _ = unary p in expect_eol p ~after:(text_of v); [ m; v ] | _ -> expect_eol p ~after:(text_of m); [ m ]) in named "defenum" [ name; Form.make (Form.Vec members) (span p name.loc) ] | "import" -> let alias = name_tok p ~what:"the package's alias" in let path = match (peek p).tok with | ATOM (Form.Str _ as v) -> let pt = advance p in Form.make v pt.loc | tk -> failk "import-path" (where_ p) "an import is import alias \"collection:path\", and found %s where \ the path goes" (show tk) in expect_eol p ~after:(text_of path); form [ alias; path ] | "if" | "when" -> let letp = if w = "if" then if_let_head p else None in let c = match letp with Some m -> m | None -> fst (binary p 1) in let letp, c = match letp with | Some _ -> (letp, c) | None -> (match as_head p c with Some m when w = "if" -> (Some m, m) | _ -> (None, c)) 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 = (match (peek p).tok with | NAME ("else" | "elif") when w = "when" && not (assigns p) -> when_else p | _ -> ()); (* The [elif let P = v] heads, as the [[P v]] each stands as. *) let elif_lets = ref [] in let rec elifs acc = match (peek p).tok with | NAME "elif" when not (assigns p) -> ignore (advance p); let c = match if_let_head p with | Some m -> elif_lets := m :: !elif_lets; m | None -> let c = fst (binary p 1) in (match as_head p c with | Some m -> elif_lets := m :: !elif_lets; m | None -> c) 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" when not (assigns p) -> 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) "after an if with a block, another test at this level is \ written elif c, with its own block" (* [else x] on one line, after a one-line if or a block. *) | _ -> let x = inline_stmt p in expect_eol p ~after:(text_of x); Some (et.loc, [ x ])) | _ -> None in match els_, else_ with | _ when !elif_lets <> [] -> (* An [elif let] makes the rest of the chain the else of an if-let: each clause nests in the one before it, [if] or [if-let] as its head is, and a chain with no else ends in a [when]. *) let is_let c = List.memq c !elif_lets || Some c == letp in let rec build = function | [] -> Option.map (fun (el, e) -> blk s el e) else_ | ((c : Form.t), b) :: rest -> let at = c.Form.loc in let f items = Form.make (Form.List items) at in let r = build rest in Some (if is_let c then f (sym at "if-let" :: c :: blk s at b :: Option.to_list r) else match r with | None -> f (sym at "when" :: c :: b) | Some r -> f [ sym at "if"; c; blk s at b; r ]) in Option.get (build ((c, body) :: els_)) | [], None -> named "when" (c :: body) | [], Some (el, e) -> named "if" [ 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 if_let_wrap letp @@ (match (peek p).tok with | NAME "then" -> ignore (advance p); let a = inline_stmt p in (match (peek p).tok with | NAME ("else" | "elif") when w = "when" -> when_else p | NAME "else" -> ignore (advance p); let b = inline_stmt p in let f = named "if" [ 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" | _ -> (* An else or elif indented under the one-line if: it continues that if only at the if's own column. *) (match (peek p).tok, (peek_at p 1).tok, (peek_at p 2).tok with | NEWLINE, INDENT, NAME (("else" | "elif") as w) -> failk "else-column" (peek_at p 2).loc "this %s is indented deeper than the one-line if it continues. \ Put it at the if's column:\n\n\ \ if c then a\n %s ..." w w | _ -> ()); 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 clauses ~oneline:false body) | "while" | "until" -> let label = match (peek p).tok, (peek_at p 1).tok with | KW k, n when n <> NEWLINE -> let kt = advance p in [ Form.make (Form.Kw k) kt.loc ] | _ -> [] in let c, _ = expr p in (match (if w = "while" then as_head p c else None) with (* [while e? as g]: [(while true (if-let [g e] (do body) (break)))]. A break or continue in the body is this loop's. *) | Some m -> expect_line_end p ~after:(w ^ " " ^ text_of c ^ " as ..."); let body = block s ~after:w in let at = c.Form.loc in let f items = Form.make (Form.List items) at in form (label @ [ sym at "true"; f [ sym at "if-let"; m; f (sym at "do" :: body); f [ sym at "break" ] ] ]) | None -> expect_line_end p ~after:(w ^ " " ^ text_of c); let body = block s ~after:w in form (label @ (c :: body))) | "for" -> let label = match (peek p).tok with | KW k -> let kt = advance p in [ Form.make (Form.Kw k) kt.loc ] | _ -> [] in (* In a macro template the variable may be an unquote, [for ~i in ...]. *) let v = match (peek p).tok with | UNQ -> fst (primary p) | _ -> name_tok p ~what:"the loop variable" in expect_name p "in" ~what:"in, as in for i in range(n)"; let rt = peek p in expect_name p "range" ~what:"range(n), range(a, b) or range(a, b, step)"; let lp = glued_lp p ~what:"range's bounds in parentheses" in let bs = items p RP lp.loc ~what:"bounds" in if bs = [] || List.length bs > 3 then failk "range-arity" rt.loc "range takes one, two or three bounds: range(stop), range(start, stop) \ or range(start, stop, step)"; expect_line_end p ~after:"range(...)"; let body = block s ~after:"for" in named "dotimes" (label @ (Form.make (Form.Vec (v :: bs)) (span_of_list v.loc bs) :: body)) | "return" -> (match (peek p).tok with | NEWLINE -> expect_eol p ~after:"return"; form [] | _ -> let e, _ = expr p in expect_eol p ~after:(text_of e); form [ e ]) | "break" | "continue" -> (match (peek p).tok with | KW k -> let kt = advance p in expect_eol p ~after:(":" ^ k); form [ Form.make (Form.Kw k) kt.loc ] | _ -> expect_eol p ~after:w; form []) | "defer" -> (match (peek p).tok with | NEWLINE -> ignore (advance p); form (block s ~after:"defer") | _ -> let e = inline_stmt p in expect_eol p ~after:(text_of e); form [ e ]) | "match" -> let scrut, _ = expr p in expect_eol_block p ~after:("match " ^ text_of scrut); let arms = lines s (fun () -> let pat, _ = unary p in expect_name p "->" ~what:"-> and the arm's value"; let body = if (peek p).tok = NEWLINE && (peek_at p 1).tok = INDENT then begin let nl = advance p in blk s nl.loc (block s ~after:"->") end else begin let e = inline_stmt p in expect_eol p ~after:(text_of e); e end in [ pat; body ]) in form (scrut :: arms) | "handler-case" | "handler-bind" -> clause_header_end p w; let body = block s ~after:w in let rec clauses acc = match (peek p).tok, (peek_at p 1) with | NAME "on", n when n.sp && not (assigns p) -> let ot = advance p in let head, _ = postfix p in let ty, var = match head.v with | Form.List [ ty; ({ v = Form.Sym _; _ } as var) ] -> (ty, var) | _ -> failk "on-clause" head.loc "a handler clause is on Type(name), naming the condition type \ and the name it is bound to, as in on FileError(c)" in clause_end p ("on " ^ text_of ty ^ "(" ^ text_of var ^ ")"); let b = block s ~after:"on" in let c = mk p ot.loc (Form.List (ty :: Form.make (Form.Vec [ var ]) var.loc :: b)) in clauses (c :: acc) | _ -> List.rev acc in let cs = clauses [] in let vec = Form.make (Form.Vec cs) (span p l0) in if w = "handler-case" then form [ blk s l0 body; vec ] else form (vec :: body) | "restart-case" -> clause_header_end p w; let body = block s ~after:w in let rec clauses acc = match (peek p).tok, (peek_at p 1) with | NAME "restart", n when n.sp && not (assigns p) -> ignore (advance p); 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 :: (report @ b))) in clauses (c :: acc) | _ -> List.rev acc in let cs = clauses [] in form (blk s l0 body :: cs) | "quote" -> (* One line, [quote ~x + 1], is the quasiquote of that expression. *) (match (peek p).tok with | NEWLINE -> ignore (advance p); let body = block s ~after:"quote" in named "quasiquote" [ blk s l0 body ] | _ -> let e, _ = expr p in expect_eol p ~after:(text_of e); named "quasiquote" [ e ]) | _ -> assert false (* [let x = v] at the top level, [once x = v] or [const x = v], from the name on: [t] is the word, and [l0] where the form starts. A top-level let leaves the lines indented under it to [read_all], which reads each as one more global. *) and def_form (s : st) w (t : token) l0 : Form.t = let p = s.p in let named head items = mk p l0 (Form.List (sym l0 head :: items)) in (* A top-level [let] is read here too, as [def]: [w] is then "def" and [t] the let. *) let shown = match t.tok with NAME "let" -> "let" | _ -> w in let name = name_tok p ~what:"the name being defined" in let tyf = match (peek p).tok with | COLON -> ignore (advance p); Some (ty p) | _ -> None in let v = match (peek p).tok with | NAME "=" -> ignore (advance p); Some (value_line ~block_ok:(shown = "let") s ~after:(shown ^ " " ^ text_of name ^ " =")) (* [let a: i32] with more globals under it. *) | NEWLINE when shown = "let" && tyf <> None && (peek_at p 1).tok = INDENT -> ignore (advance p); None | _ -> expect_eol p ~after:(match tyf with Some f -> text_of f | None -> text_of name); None in let head = match w with "def" -> "def" | "once" -> "defonce" | _ -> "defconst" in if shown = "def" then failk "def-is-let" l0 "a global is written with let, at the file's top level:\n\n let %s%s%s" (text_of name) (match tyf, v with | Some t, _ -> ": " ^ text_of t | None, None -> ": i32" | None, Some _ -> "") (match v, tyf with | Some v, _ -> " = " ^ text_of v | None, None -> " = 0" | None, Some _ -> ""); let items = match w, tyf, v with | "const", None, Some v -> [ name; v ] | "const", Some t, Some v -> [ name; t; v ] | "const", _, None -> failk "const-value" l0 "a const needs its value: const %s = 3" (text_of name) | _, None, Some v -> [ name; sym name.loc "dyn"; v ] | _, Some t, None -> [ name; t ] | _, Some t, Some v -> [ name; t; v ] | _, None, None -> failk "def-empty" l0 "%s %s names neither a type nor a value. Give it one or both: %s %s: \ i32 = 0" shown (text_of name) shown (text_of name) in named head items (* handler-case, handler-bind and restart-case take nothing on their own line. *) and clause_header_end p w = match (peek p).tok with | NEWLINE -> ignore (advance p) | _ -> failk "clause-header" (peek p).loc "%s takes its body on the indented lines under it, and its %s clauses \ at its own column after that, each with its block under it:\n\ %s\n body\n%s" w (if w = "restart-case" then "restart" else "on") w (if w = "restart-case" then "restart name()\n value" else "on Type(c)\n value") and clause_end p head = match (peek p).tok with | NEWLINE -> ignore (advance p) | _ -> failk "clause-body" (peek p).loc "the body of %s goes on the indented lines under it, not on its line. \ Move it to the next line, indented" head (* The end of a header line whose block must follow. *) and expect_line_end p ~after = if p.i = p.closed then () else match (peek p).tok with | NEWLINE -> ignore (advance p) | _ -> stray p ~after and expect_eol_block p ~after = expect_line_end p ~after (* An indented run of one-line entries — a struct's fields, a match's arms. None at all is allowed for the declarations and is refused later, by the form, where it matters. *) and lines (s : st) (one : unit -> Form.t list) : Form.t list = let p = s.p in if (peek p).tok <> INDENT then [] else begin ignore (advance p); let rec go acc = match (peek p).tok with | DEDENT -> ignore (advance p); List.rev acc | EOF -> List.rev acc | _ -> go (List.rev_append (one ()) acc) in go [] end let () = block_of := fun p -> block { p; lets = [] } ~after:"=>" (* Every [(loop ...)] and [(recur ...)] in [fs], at any depth and inside quoted code too, in source order. The indented syntax has neither: its loops are [while], [until], [dotimes] and [for]. The printer asks the same question before it converts a .flan file. *) let loop_forms (fs : Form.t list) = let out = ref [] in let rec walk (f : Form.t) = match f.v with | Form.List ({ v = Form.Sym ("loop" | "recur"); _ } :: _) -> out := f :: !out; (match f.v with Form.List l -> List.iter walk l | _ -> ()) | Form.List l | Form.Vec l | Form.Map l -> List.iter walk l | _ -> () in List.iter walk fs; List.rev !out let refuse_loops fs = match loop_forms fs with | [] -> () | (f : Form.t) :: _ -> no_loop f.loc (match f.v with Form.List ({ v = Form.Sym w; _ } :: _) -> w | _ -> "loop") (** All top-level forms in a [.fln] source string. [col] is the column the text's top level starts at, 1 for a file. *) let read_all ?(line = 1) ?col ?indent ?(global_let = true) ~file src = let snippet = col <> None in let col = Option.value col ~default:1 in let saved = !source in let saved_n = !cmp_n and saved_o = !opt_n in cmp_n := 0; opt_n := 0; (* The quoted text is indexed by the buffer's lines, so a snippet that starts on line 40 is padded to start there. *) source := (file, Array.of_list (String.split_on_char '\n' (String.make (line - 1) '\n' ^ String.make (col - 1) ' ' ^ src))); Fun.protect ~finally:(fun () -> source := saved; cmp_n := saved_n; opt_n := saved_o) (fun () -> let toks = layout ~snippet ~base:col ?indent (lex ~line ~col ~file src) in let s = { p = { toks; i = 0; closed = -1 }; lets = [] } in (* At the top level, a [let] is a global, [(def x dyn v)]: a let there has no block to be local to. Not in an expression the editor sends, where a let is the statement it is in a body. *) let rec top () = match (peek s.p).tok with | EOF -> [] | DEDENT -> ignore (advance s.p); [] | NAME "let" when header_follow s.p "let" -> let t = peek s.p in let_gap_tab t (peek_at s.p 1); let name_col = (peek_at s.p 1).loc.Loc.col in let f = let_values := (name_col, text_of_tok (peek_at s.p 1), t.loc.Loc.line) :: !let_values; Fun.protect ~finally:(fun () -> let_values := List.tl !let_values) (fun () -> header s "def") in (* The lines indented under it are more globals, one each: a global is one name, so a pattern there is refused. *) let first = match f.v with Form.List (_ :: n :: _) -> n | _ -> f in let more = binding_lines ~global:true s ~first ~name_col ~one:(fun () -> let l = peek s.p in (match l.tok with | LP -> failk "global-pattern" l.loc "a global's name is a plain name, and this line under let %s \ names an operator word in parentheses. Give the global \ another name" (text_of first) | LB | LC -> failk "global-pattern" l.loc "a global binds one name, and this line under let %s is a \ pattern. Bind the value to a name, and take it apart inside \ the function that uses it" (text_of first) | _ -> ()); def_form s "def" t l.loc) in f :: more @ top () | _ -> let f = stmt s in f :: top () in let fs = if global_let then top () else stmts s in (match (peek s.p).tok with | EOF -> () | tk -> failk "unexpected-token" (where_ s.p) "unexpected %s" (show tk)); refuse_loops fs; fs) let read_file path = let ic = open_in_bin path in Fun.protect ~finally:(fun () -> close_in ic) (fun () -> let n = in_channel_length ic in read_all ~file:path (really_input_string ic n))