diff --git a/TODO.org b/TODO.org index dcb65469..b1169f74 100644 --- a/TODO.org +++ b/TODO.org @@ -590,10 +590,11 @@ method to a running program is an ordinary redefinition. ** CANCELLED Class features deferred, each with its reason CLOSED: [2026-09-20] Inheritance, multi-argument dispatch, =:before=/=:after=/=:around= and -=call-next-method=, named-slot construction, unknown-slot checking, computed -dispatch values. With single dispatch on literal values there is no specificity -question, and inheritance or multiple dispatch would create one. Unknown-slot -checking needs class-typed tracking the dyn side deliberately does not have. +=call-next-method=, named-slot construction, compile-time unknown-slot checking, +computed dispatch values. With single dispatch on literal values there is no +specificity question, and inheritance or multiple dispatch would create one. +Compile-time unknown-slot checking needs class-typed tracking the dyn side +deliberately does not have; the runtime refuses an unknown slot instead. ** DONE update-instance-for-redefined-class, the user hook CLOSED: [2026-09-25] @@ -646,50 +647,34 @@ awaiting confirmation, and the build order. Rules out Parinfer, wisp and sweet-expressions, and a simplified in-paren syntax — all thin the parens without removing them. -** NEXT Lambdas are written with => -Decided 2026-09-26: a lambda's body follows ~=>~, and ~=>~ is its only spelling -(~fn(a) = x~ is refused for a lambda; named functions keep ~=~). A header ending in ~=>~ -takes an indented block even inside brackets, closing where the brackets close: -~sort-by(xs, fn(a, b) =>~ plus a block. +** DONE Lambdas are written with => +CLOSED: [2026-09-26] +A block lambda inside brackets is the last thing in them, its block ending where they close: a comma +after it (so a second block lambda, or one not last) is refused, and the fix names it with ~let~. -** NEXT A condition struct with a parent has no sugar -On the author's decision. =defstruct(DiskFull, :parent, IoError, [free i64])= is the fallback, with a -paren field vector. Proposal: =struct DiskFull :parent IoError= plus field lines. +** NEXT Indices separate like vector elements +Decided 2026-09-26: ~grid[r c]~ and ~grid[(r + 1) (c - 1)]~ read like a vector's +space-separated single values; commas always work; an index with a bare operator +needs them, ~grid[r + 1, c]~. -** NEXT A type alias is written type Row = Vec(i32) -Decided 2026-09-26: .fln reads ~type Name = T~ as ~(defalias Name T)~, and the printer -writes it back. +** WAIT Calls without parentheses +Held 2026-09-26 by the author: F#-style ~f x y~ or Nim-style one-argument calls +without parentheses. Collides with space-separated vector and index elements. -** TODO flan check prints every definition of a file that checks -A clean =flan check= lists the whole prelude (about 170 lines). Clean should print -nothing, or only the file's own definitions behind a flag. +** TODO RET inside an open call puts the closer at the statement's column +In a .fln buffer, ~if and(state.paused|)~ then RET leaves ~)~ at the ~if~'s column, so +the next argument cannot be typed where it belongs. A line inside open brackets, +closer-led or not, goes to the continuation column (aligned after the opening bracket). -** NEXT Classes and methods have .fln syntax -Decided 2026-09-26: ~class Lambda(param, body, env)~, ~generic describe(v) -> dyn~, -~method describe(f: Lambda)~ plus a block (the class as the parameter's type), -~multi kind(v) -> dyn = type-of(v)~, ~method kind(v) when :int~ plus a block. - -** NEXT A top-level let is a global -Decided 2026-09-26: in .fln ~let x = v~ at column 0 reads ~(def x v)~ and replaces -~def~, which is refused with that fix; ~once~ and ~const~ stay. +** NEXT not is a prefix word +Decided 2026-09-26: .fln writes ~not x~ (binding like F#'s ~not~, tighter than +~and~/~or~, looser than comparisons); ~not(x)~ keeps working as a call. ** NEXT A let takes several bindings on indented lines Decided 2026-09-26: lines indented under a ~let~ that are ~name = v~ or ~name: T = v~ are more bindings of the same let; anything else there stays refused. flan convert writes consecutive lets this way. -** NEXT A struct fits on one line -Decided 2026-09-26: ~struct Pt(x: i32, y: i32)~ beside the block form, like a data -case; union and a struct with a parent too. - -** NEXT defmacro has no sugar -On the author's decision. =defmacro(repeat, [i n & body]):= with a space-separated parameter vector. -Proposal: =macro repeat(i, n, & body)= plus a block. - -** NEXT loop/recur has no sugar -On the author's decision. =loop([x a y b]):=. Proposal: =loop x = a, y = b= plus a block; =recur(...)= -stays a call. - ** TODO Hard-coded code in messages is still paren syntax in a .fln file Types follow the code's syntax now (=Types.spell=). Hints written into a message's text — =(Ptr %s)=, =(clone v)=, =(the T x)= in most of =check.ml= and =parse.ml=, the @@ -733,10 +718,10 @@ of !=. * Checker -** NEXT A dyn value takes .field and [:key] -Decided 2026-09-26: on a dyn value, ~x.name~ / ~(.name x)~ reads ~(get x :name)~ and -assigning it is ~(put x :name v)~ — a class slot or a map key; ~m[:k]~ indexes a dyn -map as ~(get m :k)~, and assigning it puts. +** DONE A dyn value takes .field and [:key] +CLOSED: [2026-09-26] +Assigning ~x.name~ or ~m[:k]~ is ~put~: a plain map gains the key, and a class +instance refuses one its class does not declare, on read too, as ~get~ and ~put~ now do. ** DONE A slice from a C pointer, and a pointer cast CLOSED: [2026-09-25] @@ -1405,9 +1390,8 @@ CLOSED: [2026-09-20] CLHS 4.3.6. Nothing is enumerated and no heap is walked — the redefinition is constant time and each instance pays once, at its next touch. Neither printer migrates, so a stale instance shows its old slots to the editor -until something touches it. The registry is advisory: a key the class never -declared is dropped by the next migration, which is data loss with no enforcement -behind it. +until something touches it. An instance holds only declared slots — get, put and +set refuse any other key — so the migration's drop loses nothing a program wrote. ** WAIT A class registry keeps one slot list per class, not one per layout version Decided 2026-09-25: waits for a case name-matching migration to the current list gets wrong. diff --git a/bin/main.ml b/bin/main.ml index 0a6184cd..2da76c62 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -226,9 +226,14 @@ let no_gc_flag = "--no-gc" downstream that it ran. *) let warn_memory_flag = "--warn-memory" +(* [flan check --defs]: every definition the checked program holds, prelude + included. Off by default, so a file that checks prints nothing. *) +let defs_flag = "--defs" + let flags = [ no_checks_flag; dev_flag; debug_flag; sanitize_flag; two_process_flag; - x86_flag; llvm_flag; no_annotate_flag; no_gc_flag; warn_memory_flag ] + x86_flag; llvm_flag; no_annotate_flag; no_gc_flag; warn_memory_flag; + defs_flag ] (* The warnings, where errors go. Printed one to a location in the repo's standard [file:line:col:] shape, with the squiggle [Loc.entry] draws, so @@ -355,6 +360,7 @@ let () = files | _ :: "check" :: args when List.exists (fun a -> not (is_flag a)) args -> let warn_memory = List.mem warn_memory_flag args in + let defs = List.mem defs_flag args in let files = List.filter (fun a -> not (is_flag a)) args in List.iter (fun path -> @@ -366,26 +372,28 @@ let () = program that checked, which is what leaves the exit status alone. *) if warn_memory then print_memory_warnings ~file:path p; - List.iter - (fun (g : Flan.Tast.global) -> - Printf.printf "%s %s %s\n" - (* The three defining forms, told apart the way the compiler - tells them apart: [gconst] is the image, and [grerun] is - what a re-run does to the storage. A listing that called - both mutable forms one name could not answer the question - someone runs [flan check] on a dev file to ask. *) - (if g.gconst then "defconst" - else if g.grerun then "def" else "defonce") - g.gname (Flan.Types.to_string g.gty)) - p.globals; - List.iter - (fun (f : Flan.Tast.fn) -> - if not (Flan.Check.internal_name f.name) then - Printf.printf "defn %s : (Fn [%s] %s) %d slots\n" f.name - (String.concat " " - (List.map Flan.Types.to_string f.params)) - (Flan.Types.to_string f.ret) (Array.length f.slots)) - p.fns)) + if defs then begin + List.iter + (fun (g : Flan.Tast.global) -> + Printf.printf "%s %s %s\n" + (* The three defining forms, told apart the way the compiler + tells them apart: [gconst] is the image, and [grerun] is + what a re-run does to the storage. A listing that called + both mutable forms one name could not answer the question + someone runs [flan check] on a dev file to ask. *) + (if g.gconst then "defconst" + else if g.grerun then "def" else "defonce") + g.gname (Flan.Types.to_string g.gty)) + p.globals; + List.iter + (fun (f : Flan.Tast.fn) -> + if not (Flan.Check.internal_name f.name) then + Printf.printf "defn %s : (Fn [%s] %s) %d slots\n" f.name + (String.concat " " + (List.map Flan.Types.to_string f.params)) + (Flan.Types.to_string f.ret) (Array.length f.slots)) + p.fns + end)) files (* The generated C, for looking at. A wrong FFI binding is wrong in the wrapper, and the wrapper is not on disk anywhere — [Build] hands the text @@ -982,7 +990,7 @@ let () = | None -> code)) | _ -> prerr_endline - "usage: flan (read|parse|check|emit|shim) ...\n flan check ... [--warn-memory]\n flan emit [--x86] [--dev] [--debug] [--no-bounds-checks]\n\ + "usage: flan (read|parse|check|emit|shim) ...\n flan check ... [--warn-memory] [--defs]\n flan emit [--x86] [--dev] [--debug] [--no-bounds-checks]\n\ \ flan import-c [package.flan...] [clang flags...]\n\ \ flan generate-c \n\ \ flan build [-o out] [-O0|-O1|-O2|-O3] \ diff --git a/docs/SBCL-REDEFINITION-NOTES.md b/docs/SBCL-REDEFINITION-NOTES.md index d54b7533..7fb2acec 100644 --- a/docs/SBCL-REDEFINITION-NOTES.md +++ b/docs/SBCL-REDEFINITION-NOTES.md @@ -326,10 +326,9 @@ The CLOS answer would need, concretely: Flan spelling of that hook is a generic function, e.g. `(defmethod update-for-redefined point [p added discarded] ...)`, which fits the dispatch mechanism that already exists. -- A decision on whether `put` of an unknown slot stays legal. Today it is — a - class instance is an open map, and TODO.org, "Class features deferred, each with - its reason", already defers refusing an unknown slot at `(get p :z)`. If unknown slots stay legal, the registry's slot list is - advisory and the whole update protocol is advisory with it. +- A decision on whether `put` of an unknown slot stays legal. Decided + 2026-09-26: it does not; `get`, `put` and `set` refuse an unknown slot on an + instance at run time, so the registry's slot list is enforced. This is a real, SBCL/CLOS-precedented design that Flan's runtime can actually support. It is also a feature with no user yet, since redefinition on the dyn diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 0814dafc..6f044e41 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -1205,7 +1205,7 @@ Use `C-c C-g` if you need frames. | `M-a` / `M-e` | `(` / `)` | statement: start / end (`)`: start of the next) | | `C-M-u` | same | up to the enclosing bracket, or the line that owns the block | | `C-M-f` / `C-M-b` | same | brackets and terms, as everywhere | -| `TAB` | same | a line at a valid column stays; an empty or misplaced line goes deepest; each repeat steps out a level. One level deeper only after a line that opens a block: never after a `let`, unless its value goes on under it (`= match x`, `= if c`, a lambda header) | +| `TAB` | same | a line at a valid column stays; an empty or misplaced line goes deepest; each repeat steps out a level. One level deeper only after a line that opens a block: never after a `let`, unless its value goes on under it (`= match x`, `= if c`, `= loop i = 0`, a lambda header). After a line ending in `=>`, one level in from that line, inside brackets too; a line of that block keeps to the block's columns | | `DEL` in indentation | same | drop one level | | `C-c <` / `C-c >` | `<` / `>` | shift the region's lines a level | | `M-` / `M-` | same | move the statement past its neighbour | @@ -1225,7 +1225,7 @@ enclosing statement, top-level form. - **term**: a run with no space outside brackets — `f(a, b)`, `grid[r, c]`, `p.x`. - **group**: a bracket pair and what is inside it. -- **statement**: a line, the deeper lines under it, lines inside brackets it leaves open, lines an operator continues, and `else`/`elif`/`on`/`restart` at its column. Blank and comment lines inside never end it. +- **statement**: a line, the deeper lines under it, lines inside brackets it leaves open, lines an operator continues, and `else`/`elif`/`on`/`restart` at its column. Blank and comment lines inside never end it. A lambda's block inside brackets, `sort-by(xs, fn(a, b) =>` and the lines under it, is part of the call's statement, and each of its lines is a statement too: `C-c C-e` or `C-u C-c C-c` there takes that line, not the call, and never the call's closing `)` at the end of it. - **body**: a statement's own block, up to its first clause. - **clause**: one `else`/`elif`/`on`/`restart` line and its block, or the value on its line (`else x`). - **top-level form**: a column-0 line that is code, not a clause and not a continuation, through the last code line before the next one. diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 7e538382..96e0c01c 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -25,7 +25,8 @@ ;; leaves open, the lines an operator continues, and the ;; `else'/`elif'/`on'/`restart' clauses at its own column. Blank ;; and comment lines inside never end it; trailing ones are not -;; part of it. +;; part of it. A `=>' ending a line inside brackets opens a +;; lambda's block there, whose lines are statements again. ;; body a statement's own block: the deeper lines under its first line, ;; up to its first clause. ;; clause one `else'/`elif'/`on'/`restart' line and its block. @@ -83,8 +84,14 @@ fine here. Brackets and strings are still paired." (defconst flan-fln--clause-words '("else" "elif" "on" "restart") "Words that start a clause of the statement above, at its column.") +;; What follows a word that starts a clause or a header: a space and not an +;; assignment, or the end of the line. `on = 2' assigns a variable named on, +;; as `assigns' in lib/indent_reader.ml reads it. +(defconst flan-fln--word-end-re + "\\(?:[ \t]+\\(?:[^-+*/= \t\n]\\|[-+*/]\\(?:[^=]\\|$\\)\\)\\|[ \t]*$\\)") + (defconst flan-fln--clause-re - "\\(else\\|elif\\|on\\|restart\\)\\(?:[ \t]\\|$\\)") + (concat "\\(else\\|elif\\|on\\|restart\\)" flan-fln--word-end-re)) (defconst flan-fln--clause-headers '(("else" "if" "elif") ("elif" "if" "elif") @@ -97,7 +104,8 @@ fine here. Brackets and strings are still paired." '("fn" "fn-" "def" "once" "const" "struct" "union" "data" "enum" "import" "if" "elif" "else" "while" "until" "for" "match" "let" "return" "break" "continue" "defer" "handler-case" "handler-bind" "restart-case" "on" - "restart" "quote")) + "restart" "quote" "macro" "loop" "type" "class" "generic" "multi" + "method")) ;; The headers whose block follows on the lines under them. `defer' and ;; `quote' open one only when nothing follows them on the line; `fn' does not @@ -106,12 +114,15 @@ fine here. Brackets and strings are still paired." (defconst flan-fln--opener-words '("fn" "fn-" "struct" "union" "data" "enum" "if" "elif" "else" "while" "until" "for" "match" "defer" "handler-case" "handler-bind" - "restart-case" "on" "restart" "quote")) + "restart-case" "on" "restart" "quote" "macro" "loop" "class" "multi" + "method")) (defconst flan-fln--declaration-words '(("fn" . "defn") ("fn-" . "defn-") ("def" . "def") ("once" . "defonce") ("const" . "defconst") ("struct" . "defstruct") ("data" . "defdata") - ("enum" . "defenum") ("union" . "defunion") ("import" . "import")) + ("enum" . "defenum") ("union" . "defunion") ("import" . "import") + ("let" . "def") ("macro" . "defmacro") ("type" . "defalias") ("class" . "defclass") + ("generic" . "defgeneric") ("multi" . "defmulti") ("method" . "defmethod")) "Each declaration header word, and the paren head it reads as.") ;;; Syntax @@ -224,15 +235,63 @@ fine here. Brackets and strings are still paired." (setq hit (line-beginning-position))) hit))) +(defun flan-fln--closer-line-p (pos) + "Non-nil if POS's line starts inside a bracket with the bracket's closer." + (save-excursion + (let ((s (syntax-ppss (flan-fln--bol pos)))) + (and (nth 1 s) (not (nth 3 s)) + (progn (goto-char (flan-fln--bol pos)) + (skip-chars-forward " \t") + (looking-at "\\s)")))))) + +(defun flan-fln--lambda-arrow (pos) + "The `=>' whose block POS's line is in, when that block is inside brackets. +A `=>' ending a line inside a bracket opens a block there, which lasts until +the bracket closes (lib/indent_reader.ml's `layout'): a line inside the same +bracket after it is a statement of that block, not a continuation. A line +that starts with the bracket's closer is not in the block. +Each call scans the lines from the open bracket to POS, so walking a long +bracketed literal line by line costs the square of its length." + (save-excursion + (let* ((bol (flan-fln--bol pos)) + (s (syntax-ppss bol)) + (open (nth 1 s))) + (when (and open (not (nth 3 s)) (not (flan-fln--closer-line-p bol))) + (goto-char open) + (let (hit) + (while (and (not hit) (< (line-end-position) bol)) + (let ((end (flan-fln--code-end (point)))) + (goto-char end) + (when (and (looking-back "[ \t]=>" (line-beginning-position)) + (= (nth 1 (save-excursion (syntax-ppss (- end 2)))) open)) + (setq hit (- end 2)))) + (forward-line 1)) + hit))))) + +(defun flan-fln--bracketed-p (pos) + "Non-nil if POS's line is inside a bracket or a string, where a line break +is only a space: not in a lambda's block." + (and (flan-fln--in-open-p pos) (not (flan-fln--lambda-arrow pos)))) + (defun flan-fln--continuation-p (pos) "Non-nil if POS's line continues the line above it. Inside a bracket or a string, after a line that ends in a spaced operator, or -starting with one: the reader's three ways a line break is not a new line." - (or (flan-fln--in-open-p pos) +starting with one: the reader's three ways a line break is not a new line. +Not a line of a lambda's block inside brackets, which is a statement." + (or (flan-fln--bracketed-p pos) (flan-fln--starts-with-op-p pos) (let ((p (flan-fln--prev-code pos))) (and p (flan-fln--ends-in-op-p p))))) +(defun flan-fln--continues-p (pos start) + "Non-nil if POS's line continues the statement that starts at START. +A line that starts with the closer of a bracket opened before START closes +something START is inside, a lambda's block's call, and is not START's." + (and (flan-fln--continuation-p pos) + (not (and (flan-fln--closer-line-p pos) + (< (nth 1 (save-excursion (syntax-ppss (flan-fln--bol pos)))) + (flan-fln--bol start)))))) + (defun flan-fln--clause-line-p (pos) "Non-nil if POS's line starts a clause: else, elif, on or restart. A word, and only when a space or the end of the line follows it." @@ -245,16 +304,21 @@ A word, and only when a space or the end of the line follows it." (defun flan-fln--logical-start (pos) "The first line of the line POS is on, after continuation lines are joined." (let ((bol (flan-fln--bol pos)) p) - (while (and (flan-fln--continuation-p bol) - (setq p (flan-fln--prev-code bol))) - (setq bol p)) + (while (cond + ;; A closer's line belongs with the line its bracket opens on, + ;; not with a lambda's block just above it. + ((flan-fln--closer-line-p bol) + (setq bol (flan-fln--bol (nth 1 (save-excursion (syntax-ppss bol)))))) + ((and (flan-fln--continuation-p bol) + (setq p (flan-fln--prev-code bol))) + (setq bol p)))) bol)) (defun flan-fln--logical-end (pos) "The last line of the joined line whose first line is POS's." - (let ((bol (flan-fln--bol pos)) n) + (let* ((bol (flan-fln--bol pos)) (start bol) n) (while (and (setq n (flan-fln--next-code bol)) - (flan-fln--continuation-p n)) + (flan-fln--continues-p n start)) (setq bol n)) bol)) @@ -285,10 +349,18 @@ depth, outside strings and comments, or nil." (flan-fln--joined-end l)))) (and m (car m)))) +(defun flan-fln--value-end (l) + "Where a value written on the joined line L ends: at the line's code end, +or, when the line ends in `=>', at the end of the lambda's block under it." + (let ((last (flan-fln--logical-end l))) + (if (flan-fln--ends-in-arrow-p last) + (cdr (flan-fln--span l (flan-fln--statement-last l t))) + (flan-fln--code-end last)))) + (defun flan-fln--clause-value (l) "Bounds of the value on the clause line L itself: `x' of `else x' or of `elif c then x'; nil when the clause's value is its block." - (let ((end (flan-fln--joined-end l)) + (let ((end (flan-fln--value-end l)) (then (flan-fln--then l))) (save-excursion (goto-char (flan-fln--first-char l)) @@ -309,30 +381,36 @@ depth, outside strings and comments, or nil." (flan-fln--joined-end l)))) (and m (cdr m)))) +(defun flan-fln--ends-in-arrow-p (pos) + "Non-nil if POS's line ends in `=>': a lambda's header, its block under it." + (save-excursion + (let ((end (flan-fln--code-end pos))) + (goto-char end) + (and (not (nth 8 (syntax-ppss end))) + (looking-back "[ \t]=>" (line-beginning-position)))))) + (defun flan-fln--lambda-header-p (pos end) "Non-nil if a lambda with its body under it starts at POS and runs to END: -`fn(a, b)', or `fn(a: C, b) -> R', with no `= body' after it." +`fn(a, b) =>', or `fn(a: C, b) -> R =>', with nothing after the `=>'." (save-excursion (goto-char pos) (and (looking-at "fn(") (let ((close (ignore-errors (scan-lists (+ pos 2) 1 0)))) (and close (<= close end) - (progn (goto-char close) (skip-chars-forward " \t") - (or (>= (point) end) - (and (looking-at "->[ \t]") - (not (flan-fln--find-top "[ \t]=[ \t]" (point) end)))))))))) + (progn (goto-char end) (looking-back "[ \t]=>" close))))))) (defun flan-fln--value-opens-p (l) "Non-nil if the value the joined line L binds or assigns goes on under it: -`= match x', `= if c' with no `then', `= handler-case', `= restart-case', or -a lambda header. These are the values lib/indent_reader.ml's `value_line' -reads a block for, besides a bare `=' and a call ending in `:'." +`= match x', `= if c' with no `then', `= handler-case', `= restart-case', +`= loop i = 0', or a lambda header. These are the values +lib/indent_reader.ml's `value_line' reads a block for, besides a bare `=' +and a call ending in `:'." (let ((v (flan-fln--value-start l)) (end (flan-fln--joined-end l))) (and v (< v end) (save-excursion (goto-char v) - (or (looking-at "\\(?:match\\|handler-case\\|handler-bind\\|restart-case\\)\\(?:[ \t]\\|$\\)") + (or (looking-at "\\(?:match\\|handler-case\\|handler-bind\\|restart-case\\|loop\\)\\(?:[ \t]\\|$\\)") (and (looking-at "if[ \t]") (not (flan-fln--then l))) (flan-fln--lambda-header-p v end)))))) @@ -346,6 +424,8 @@ line's own block only." (last (flan-fln--logical-end start)) (next (flan-fln--next-code last))) (while (and next + (not (and (flan-fln--closer-line-p next) + (not (flan-fln--continues-p next start)))) (or (> (flan-fln--indent-at next) indent) (flan-fln--continuation-p next) (and (not no-clauses) @@ -355,9 +435,26 @@ line's own block only." next (flan-fln--next-code last))) last)) +(defun flan-fln--trim-closers (beg end) + "END, less the closers before it whose brackets open before BEG. +The last statement of a lambda's block inside a call ends with the call's `)' +on its line, which is not the statement's." + (save-excursion + (goto-char end) + (let (done) + (while (not done) + (skip-chars-backward " \t" beg) + (let ((open (and (> (point) beg) (memq (char-before) '(?\) ?\] ?\})) + (nth 1 (save-excursion (syntax-ppss (1- (point)))))))) + (if (and open (< open beg)) + (backward-char 1) + (setq done t)))) + (point)))) + (defun flan-fln--span (start last) "(BEG . END) from the text of START's line to the code end of LAST's." - (cons (flan-fln--first-char start) (flan-fln--code-end last))) + (let ((beg (flan-fln--first-char start))) + (cons beg (flan-fln--trim-closers beg (flan-fln--code-end last))))) (defun flan-fln--statement-bounds (start) "Bounds of the statement whose first line is START." @@ -602,7 +699,7 @@ the form." (defun flan-fln--declaration-head-at (pos &optional heads) "The paren head of the declaration written at POS, or nil. The .fln twin of `flan--declaration-head-at': POS must be at column 0 and not -in a string or comment, and the header word -- `fn', `def', `struct', ... -- +in a string or comment, and the header word -- `fn', `let', `struct', ... -- or the fallback call's name, `defmethod(', must read as one of HEADS, `flan--declaration-heads' by default." (save-excursion @@ -687,7 +784,11 @@ forms, where the clause line itself is not." (g (flan-fln--group-bounds pos))) (cond ((and arm (< pos (plist-get arm :arrow))) (plist-get arm :value)) - ((and g (cdr g) (> (car g) (car b))) + ;; Not the brackets a lambda's block is in: a line of the block is a + ;; statement of its own. + ((and g (cdr g) (> (car g) (car b)) + (not (let ((a (flan-fln--lambda-arrow pos))) + (and a (< (car g) a) (< a pos))))) (cons (flan-fln--group-form-start (car g)) (cdr g))) (arm (plist-get arm :value)) ((and l (flan-fln--clause-line-p l)) (flan-fln--clause-target l)) @@ -759,7 +860,7 @@ pattern names something, so the value cannot be evaluated alone." (let* ((pat (string-trim (buffer-substring-no-properties start arrow))) (vbeg (save-excursion (goto-char (+ arrow 2)) (skip-chars-forward " \t") (point))) - (value (if (< vbeg last) (cons vbeg last) + (value (if (< vbeg last) (cons vbeg (flan-fln--value-end l)) (flan-fln--body-bounds l)))) (and value (list :arrow arrow :value value @@ -924,8 +1025,9 @@ before point. With ARG, stop there instead, as \\[flan-eval-last-sexp] does." "Non-nil if START's statement is a `let'. A let takes no block: lines under it are its value's (`= match x', a lambda header), and its name lasts to the end of the block it is in." - (save-excursion (goto-char (flan-fln--first-char start)) - (looking-at "let[ \t]"))) + (and (> (flan-fln--indent-at start) 0) + (save-excursion (goto-char (flan-fln--first-char start)) + (looking-at "let[ \t]")))) (defun flan-fln--block-rest (start) "START's statement and every statement after it in the same block." @@ -1135,7 +1237,7 @@ Before it at the same level, else out to the line that owns this block." (goto-char start) (back-to-indentation) (and (looking-at (concat (regexp-opt flan-fln--opener-words t) - "\\(?:[ \t]\\|$\\)")) + flan-fln--word-end-re)) (let* ((w (match-string-no-properties 1)) (w-end (match-end 1)) (end (flan-fln--code-end last)) @@ -1143,8 +1245,13 @@ Before it at the same level, else out to the line that owns this block." (cond ;; `else x' after an if's block is the whole else. ((member w '("defer" "quote" "else")) alone) - ((member w '("fn" "fn-")) + ((member w '("fn" "fn-" "multi" "method")) (not (re-search-forward "[ \t]=[ \t]" end t))) + ;; `struct Pt(x: i32)' has its fields on the line. + ((member w '("struct" "union" "class")) + (not (save-excursion + (goto-char w-end) + (looking-at "[ \t]+[^][ \t\n(){},;\":]+(")))) ((member w '("if" "elif")) (not (flan-fln--then start))) (t t))))) ;; `let r = match n', `x = if c', `fn f(x) = match x', a lambda @@ -1158,15 +1265,27 @@ Before it at the same level, else out to the line that owns this block." (let ((bol (line-beginning-position))) (or (looking-back ":" bol) (looking-back "[ \t]->" bol) - ;; `let x =' and `def colors =' with the value as a block, + ;; A lambda's header, at the top of a line or inside brackets. + (looking-back "[ \t]=>" bol) + ;; `let x =' and `let colors =' at the top level with the value as a block, ;; which the author's list leaves out and the reader reads. (looking-back "[ \t]=" bol)))))) +(defun flan-fln--outside-block (p pos) + "P, or when P's line is in a lambda's block inside brackets that POS is not +inside, the first line of the statement that block's `=>' is in: a block +whose brackets have closed is no block POS can join." + (let ((a (flan-fln--lambda-arrow p))) + (if (and a (not (memq (nth 1 (save-excursion (syntax-ppss (flan-fln--bol p)))) + (nth 9 (save-excursion (syntax-ppss (flan-fln--bol pos))))))) + (flan-fln--outside-block (flan-fln--logical-start a) pos) + p))) + (defun flan-fln--stack (pos) "The open block columns above POS's line, deepest first, as (COL . LINE)." (let ((p (flan-fln--prev-code pos)) out (min most-positive-fixnum)) (while p - (setq p (flan-fln--logical-start p)) + (setq p (flan-fln--outside-block (flan-fln--logical-start p) pos)) (let ((i (flan-fln--indent-at p))) (when (< i min) (push (cons i p) out) (setq min i))) (setq p (and (> min 0) (flan-fln--prev-code p)))) @@ -1177,11 +1296,28 @@ Before it at the same level, else out to the line that owns this block." "Columns TAB offers POS's line outside brackets, deepest first." (let* ((prev (flan-fln--prev-code pos)) (stack (mapcar #'car (flan-fln--stack pos)))) - (if (and prev (flan-fln--opener-p (flan-fln--logical-start prev) prev)) - (cons (+ (flan-fln--indent-at (flan-fln--logical-start prev)) - flan-fln-indent-offset) - stack) - stack))) + (cond + ;; A lambda's block goes under the line its `=>' ends, which may be a + ;; line of a call wrapped inside its brackets. + ((and prev (flan-fln--ends-in-arrow-p prev)) + (cons (+ (flan-fln--indent-at prev) flan-fln-indent-offset) stack)) + ((and prev (flan-fln--opener-p (flan-fln--logical-start prev) prev)) + (cons (+ (flan-fln--indent-at (flan-fln--logical-start prev)) + flan-fln-indent-offset) + stack)) + (t stack)))) + +(defun flan-fln--block-levels (pos) + "Columns TAB offers POS's line at a block's level, deepest first. +In a lambda's block inside brackets, only those right of the line its `=>' +ends: a line at or left of it would be outside the block, still inside the +brackets, which the reader refuses." + (let ((arrow (flan-fln--lambda-arrow pos))) + (if (not arrow) + (flan-fln--levels pos) + (let ((base (flan-fln--indent-at arrow))) + (or (seq-filter (lambda (c) (> c base)) (flan-fln--levels pos)) + (list (+ base flan-fln-indent-offset))))))) (defun flan-fln--clause-columns (word pos) "Columns of the lines above POS a clause WORD may sit under, deepest first." @@ -1226,19 +1362,20 @@ opening line's column." (let ((s (syntax-ppss (point)))) (cond ((nth 3 s) nil) - ((> (car s) 0) (list (flan-fln--bracket-column (nth 1 s)))) + ((and (> (car s) 0) (not (flan-fln--lambda-arrow (point)))) + (list (flan-fln--bracket-column (nth 1 s)))) (t (let ((prev (flan-fln--prev-code (point)))) (cond ((null prev) (list 0)) ((save-excursion (back-to-indentation) (looking-at flan-fln--clause-re)) (or (flan-fln--clause-columns (match-string-no-properties 1) (point)) - (flan-fln--levels (point)))) + (flan-fln--block-levels (point)))) ((or (flan-fln--starts-with-op-p (point)) (flan-fln--ends-in-op-p prev)) (list (+ (flan-fln--indent-at (flan-fln--logical-start prev)) flan-fln-indent-offset))) - (t (flan-fln--levels (point)))))))))) + (t (flan-fln--block-levels (point)))))))))) (defun flan-fln-indent-line () "Indent the line to a block column. @@ -1284,11 +1421,13 @@ Never re-indents a line against the others: the columns are the program." (if (and (= arg 1) (not (use-region-p)) (> (current-column) 0) (= (current-column) (current-indentation)) - (not (flan-fln--in-open-p (point)))) + (not (flan-fln--bracketed-p (point)))) (let ((cur (current-indentation))) (indent-line-to (or (seq-find (lambda (c) (< c cur)) - (flan-fln--levels (point))) - 0))) + (flan-fln--block-levels (point))) + ;; A lambda's block inside brackets has no + ;; level left of its own. + (if (flan-fln--lambda-arrow (point)) cur 0)))) (let ((cmd (or (command-remapping 'delete-backward-char) #'delete-backward-char))) (setq this-command cmd) @@ -1306,7 +1445,7 @@ Run when the word is finished by a space or a newline." (if nl (line-end-position) (point))))) (when (and (string-match "\\`[ \t]*\\(else\\|elif\\|on\\|restart\\)[ \t]*\\'" text) - (not (flan-fln--in-open-p (point)))) + (not (flan-fln--bracketed-p (point)))) (let ((cols (flan-fln--clause-columns (match-string 1 text) (point)))) (when (and cols (not (memq (current-indentation) cols))) (indent-line-to (car cols)))))))) @@ -1524,6 +1663,29 @@ it, so a block pasted at another depth stays one block." (defconst flan-fln--name-re "\\([^][ \t\n(){},;\":]+\\)" "A declared name: a run up to a bracket, a space, or the colon of `x: T'.") +;; A defining form written as the fallback call, `defmacro(m, [x]):' or +;; `defmethod(area, point, [p]):'. The heads are `flan-mode''s own, so a +;; head it learns is drawn here too; its name is drawn as the sugar draws the +;; same kind of name. +(defconst flan-fln--fallback-type-heads + (seq-filter (lambda (h) (member h '("defstruct" "defdata" "defunion" "defenum" + "defalias" "defclass"))) + flan--definers)) + +(defconst flan-fln--fallback-variable-heads + (seq-filter (lambda (h) (member h '("def" "defonce" "defconst"))) flan--definers)) + +(defconst flan-fln--fallback-function-heads + (seq-remove (lambda (h) (or (member h flan-fln--fallback-type-heads) + (member h flan-fln--fallback-variable-heads) + (member h '("defmacro" "import" "package" + "declare" "declare-c")))) + flan--definers) + "The fallback heads that define something called, for imenu.") + +(defun flan-fln--fallback-re (heads) + (concat "^" (regexp-opt heads t) "(" flan-fln--name-re)) + (defun flan-fln--return-type-matcher (limit) "Find the next return type up to LIMIT: after the `->' of a fn header, a lambda or a `Fn(...)' type, and not after a match arm's." @@ -1554,19 +1716,44 @@ lambda or a `Fn(...)' type, and not after a match arm's." (defvar flan-fln-font-lock-keywords `(;; The header words, at the start of a line and followed by a space or the ;; end of it: `if(c, a)' is the fallback call and is not a header. - (,(concat "^[ \t]*" (regexp-opt flan-fln--header-words t) "\\(?:[ \t]\\|$\\)") + (,(concat "^[ \t]*" (regexp-opt flan-fln--header-words t) flan-fln--word-end-re) 1 font-lock-keyword-face) - (,(concat "^\\(fn-?\\)[ \t]+" flan-fln--name-re) + (,(concat "^\\(fn-?\\|macro\\|generic\\|multi\\|method\\)[ \t]+" flan-fln--name-re) 2 font-lock-function-name-face) - (,(concat "^\\(?:struct\\|data\\|union\\|enum\\)[ \t]+" flan-fln--name-re) + (,(concat "^\\(?:struct\\|data\\|union\\|enum\\|type\\|class\\)[ \t]+" flan-fln--name-re) 1 font-lock-type-face) - (,(concat "^\\(?:def\\|once\\|const\\)[ \t]+" flan-fln--name-re) + ;; A method's class, `method area(p: point)', and a value after `when'. + (,(concat "^method[ \t]+[^][ \t\n(){},;\":]+([^][ \t\n(){},;\":]+:[ \t]*" flan-fln--name-re) + 1 font-lock-type-face) + ("^method[ \t].*)[ \t]+\\(when\\)[ \t]" 1 font-lock-keyword-face) + ;; An alias's type, `type Row = Vec(i32)'. + (,(concat "^type[ \t]+[^][ \t\n(){},;\":]+[ \t]+=[ \t]+" flan-fln--name-re) + 1 font-lock-type-face) + ;; A defining form as the fallback call: the head a keyword, its first + ;; argument the name it defines. + (,(flan-fln--fallback-re flan-fln--fallback-type-heads) + (1 font-lock-keyword-face) (2 font-lock-type-face)) + (,(flan-fln--fallback-re flan-fln--fallback-variable-heads) + (1 font-lock-keyword-face) (2 font-lock-variable-name-face)) + (,(flan-fln--fallback-re + (seq-remove (lambda (h) (or (member h flan-fln--fallback-type-heads) + (member h flan-fln--fallback-variable-heads))) + flan--definers)) + (1 font-lock-keyword-face) (2 font-lock-function-name-face)) + ;; A condition's parent, `struct DiskFull :parent IoError'. + (,(concat "^struct[ \t]+[^][ \t\n(){},;\":]+\\(?:([^)\n]*)\\)?[ \t]+:parent[ \t]+" + flan-fln--name-re) + 1 font-lock-type-face) + ;; A global: a let at column 0 is one. + (,(concat "^\\(?:let\\|once\\|const\\)[ \t]+" flan-fln--name-re) 1 font-lock-variable-name-face) ;; A restart clause's name, `restart retry() "Try again"'. (,(concat "^[ \t]*restart[ \t]+" flan-fln--name-re) 1 font-lock-function-name-face) ;; A lambda's `fn', glued to its parameters. ("\\(?:^\\|[ \t=(,]\\)\\(fn\\)(" 1 font-lock-keyword-face) + ;; And the `=>' its body follows. + ("[ \t]\\(=>\\)\\(?:[ \t]\\|$\\)" 1 font-lock-keyword-face) ;; An enum member written `Dir.north', a constant as `:north' is. ("\\_<[A-Z][^][ \t\n(){},;\":.]*\\.[^][ \t\n(){},;\":.]+\\_>" . font-lock-constant-face) @@ -1593,10 +1780,13 @@ lambda or a `Fn(...)' type, and not after a match arm's." "Font lock for `flan-fln-mode'.") (defvar flan-fln-imenu-generic-expression - `(("Functions" ,(concat "^fn-?[ \t]+" flan-fln--name-re) 1) - ("Macros" ,(concat "^defmacro(" flan-fln--name-re) 1) - ("Types" ,(concat "^\\(?:struct\\|data\\|union\\|enum\\)[ \t]+" flan-fln--name-re) 1) - ("Variables" ,(concat "^\\(?:def\\|once\\|const\\)[ \t]+" flan-fln--name-re) 1)) + `(("Functions" ,(concat "^\\(?:fn-?\\|generic\\|multi\\|method\\)[ \t]+" flan-fln--name-re) 1) + ("Functions" ,(flan-fln--fallback-re flan-fln--fallback-function-heads) 2) + ("Macros" ,(concat "^\\(?:macro[ \t]+\\|defmacro(\\)" flan-fln--name-re) 1) + ("Types" ,(concat "^\\(?:struct\\|data\\|union\\|enum\\|type\\|class\\)[ \t]+" flan-fln--name-re) 1) + ("Types" ,(flan-fln--fallback-re flan-fln--fallback-type-heads) 2) + ("Variables" ,(concat "^\\(?:let\\|once\\|const\\)[ \t]+" flan-fln--name-re) 1) + ("Variables" ,(flan-fln--fallback-re flan-fln--fallback-variable-heads) 2)) "Imenu index for `flan-fln-mode'.") (defun flan-fln-current-defun-name () @@ -1605,7 +1795,7 @@ lambda or a `Fn(...)' type, and not after a match arm's." (when s (save-excursion (goto-char s) - (and (looking-at (concat "\\(?:fn-?\\|def\\|once\\|const\\|struct\\|data\\|union\\|enum\\)[ \t]+" + (and (looking-at (concat "\\(?:fn-?\\|macro\\|generic\\|multi\\|method\\|class\\|let\\|once\\|const\\|struct\\|data\\|union\\|enum\\|type\\)[ \t]+" flan-fln--name-re)) (match-string-no-properties 1)))))) diff --git a/emacs/test-flan-fln-live.el b/emacs/test-flan-fln-live.el index 5c36ff93..62f41a79 100644 --- a/emacs/test-flan-fln-live.el +++ b/emacs/test-flan-fln-live.el @@ -63,11 +63,22 @@ fn size(k: i64) -> i64 else 2 fn lam(k: i64) -> i64 - let add = fn(a: i64, b: i64) -> i64 = a + b - let dbl = fn(a: i64) -> i64 + let add = fn(a: i64, b: i64) -> i64 => a + b + let dbl = fn(a: i64) -> i64 => a * 2 add(dbl(k), 1) +fn app(x: i64, f: Fn(i64) -> i64) -> i64 = f(x) + +fn lam2(k: i64) -> i64 + let a = 1 + let b = app(k, fn(x) => + let y = x + a + y) + let c = fn(z: i64) -> i64 => + z * 2 + c(b) + fn rs() -> i64 restart-case 3 @@ -85,6 +96,55 @@ fn dir(d: Dir) -> i64 Dir.north -> 7 _ -> 8 +struct Oops :parent Error + code: i64 + +type Count = i64 + +let speed: i64 = 3 + +fn speed-of() -> i64 = speed + +struct Pair(a: i64, b: i64) + +fn pair-sum(p: Pair) -> i64 = p.a + p.b + +class shape(w, h) + +generic area(s) -> dyn + +method area(s: shape) = get(s, :w) * get(s, :h) + +multi kind(v) -> dyn = type-of(v) + +method kind(v) when :int = 1 + +method kind(v) when :else + 0 + +fn counted(n: Count) -> Count = n + 1 + +fn oops-code() -> i64 + handler-case + error(Oops{.code 7}) + on Oops(c) + c.code + +macro dbl-of(x, & more) + quote + ~x + ~x + +fn use-mac(k: i64) -> i64 = dbl-of(k) + +fn gcd(a: i64, b: i64) -> i64 + loop x = a, y = b + if y == 0 then x else recur(y, x % y) + +fn sum-to(n: i64) -> i64 + let r = loop i = 0, acc = 0 + if i > n then acc else recur(i + 1, acc + i) + r + comment(): if 1 < 2 and 3 < 4 @@ -92,6 +152,9 @@ comment(): elif 1 > 2 or 3 > 4 0 + app(5, fn(a) => + a * 3 + ) if 2 < 1 then 5 elif 2 == 1 then 6 else 7 @@ -208,13 +271,50 @@ comment(): '(("fn sign2" "(sign2 0)" "0") ("fn size" "(size 20)" "2") ("fn lam" "(lam 3)" "7") + ("fn lam2" "(lam2 3)" "8") ("fn rs" "(rs)" "3") - ("fn dir" "(dir :north)" "7"))) + ("fn dir" "(dir :north)" "7") + ("struct Oops" "(oops-code)" "7") + ("type Count" "(counted 1)" "2") + ("struct Pair" "(pair-sum (Pair {.a 1 .b 2}))" "3") + ("fn pair-sum" "(pair-sum (Pair {.a 1 .b 2}))" "3") + ("class shape" "(i64 (area (shape 2 3)))" "6") + ("generic area" "(i64 (area (shape 2 3)))" "6") + ("method area" "(i64 (area (shape 2 3)))" "6") + ("multi kind" "(i64 (kind 3))" "1") + ("method kind(v) when :else" "(i64 (kind :x))" "0") + ("fn counted" "(counted 1)" "2") + ("fn oops-code" "(oops-code)" "7") + ("macro dbl-of" "(use-mac 5)" "10") + ("fn use-mac" "(use-mac 5)" "10") + ("fn gcd" "(gcd 1071 462)" "21") + ("fn sum-to" "(sum-to 4)" "10"))) (funcall goto needle) (flan-fln-eval-defun) (test-flan--check (funcall name (format "C-c C-c installs %s" needle)) (equal (funcall value call) want))) + ;; A top-level let is a global, installed and re-run by C-c C-c. + (funcall goto "let speed: i64 = 3") + (end-of-line) + (delete-char -1) + (insert "9") + (flan-fln-eval-defun) + (test-flan--check (funcall name "C-c C-c on a top-level let installs the global") + (equal (funcall value "(speed-of)") "9")) + + ;; A macro changed in the buffer and installed again reaches the + ;; function installed after it. + (funcall goto "~x + ~x") + (delete-char 7) + (insert "~x * 3") + (funcall goto "macro dbl-of") + (flan-fln-eval-defun) + (funcall goto "fn use-mac") + (flan-fln-eval-defun) + (test-flan--check (funcall name "C-c C-c on a changed macro takes effect") + (equal (funcall value "(use-mac 5)") "15")) + ;; The pause mark. What is sent is a line and column, and the daemon ;; answers `:pause' only when a form the reader made starts exactly ;; there (`Ast.mark_pause'). Each kind of target once, and one position @@ -262,6 +362,16 @@ comment(): (unless (and (car r) (string-suffix-p (cadr r) (car r))) (message " want ...%s\n got %S" (cadr r) (car r)))))) (flan-clear-errors) + ;; A lambda's block inside a call's brackets: the call goes whole + ;; from its header line, and reads. + (funcall goto "app(5") + (flan-fln-eval-statement) + (test-flan--check (funcall name "C-c C-e on a call with a block lambda sends it whole, and it reads") + (funcall shows "15")) + (funcall goto "app(5, fn(a) =>" t) + (flan-fln-eval-last) + (test-flan--check (funcall name "C-x C-e at the end of its header line too") + (funcall shows "15")) (funcall goto "if 2 > 1" t) (flan-fln-eval-last) (test-flan--check (funcall name "C-x C-e at the end of an if line evaluates the condition") @@ -281,10 +391,19 @@ comment(): ("else 2" "a one-line else after a block, at its value") ("a: i64, b" "a typed lambda, from its fn") ("a * 2" "a typed lambda's block") + ("x) =>" "a block lambda in a call, from its fn") + ("let y = x" "a statement in a block lambda's block") + ("y)" "the block's last statement, less the call's closer") + ("let b = app" "a merged let's call with a block lambda, at its value") + ("let c = fn" "a merged let's block lambda, at its value") ("restart retry" "a restart with a report, at its block") ("Dir.north" "an enum member's arm, at its value") ("let b = 2" "a let the let above takes in, at its value") - ("let c: i64" "a typed one, at its value"))) + ("let c: i64" "a typed one, at its value") + ("loop x = a" "a loop, at its word") + ("if y == 0" "a loop's block") + ("let r = loop" "a let-bound loop, at its let") + ("if i > n" "a let-bound loop's block"))) (funcall goto (car c)) (let ((reply (flan-fln-eval-defun '(4)))) (test-flan--check (funcall name (format "C-u C-c C-c marks %s where the reader starts it" diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index 10721aaf..1a69fbd1 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -203,7 +203,7 @@ fn step() -> () (test-flan-fln--is "before any form, the next one" (test-flan-fln--thing 'flan-fln-toplevel) "fn f() -> i32 = 1")) -(test-flan-fln--in "def xs = [1 2\n3 4]\n + 1\nfn|x() -> i32 = 1\n" +(test-flan-fln--in "let xs = [1 2\n3 4]\n + 1\nfn|x() -> i32 = 1\n" (test-flan-fln--is "column 0 inside a bracket or after a leading operator is no form start" (save-excursion (beginning-of-defun) (buffer-substring-no-properties (point) (line-end-position))) @@ -598,8 +598,8 @@ fn size(n: i64) -> i64 else 2 fn k(n: i64) -> i64 - let add = fn(a: i64, b) -> i64 = a + b - let dbl = fn(a: Fn(i64) -> i64, b: i64) -> i64 + let add = fn(a: i64, b) -> i64 => a + b + let dbl = fn(a: Fn(i64) -> i64, b: i64) -> i64 => a(b) * 2 let r = match n 0 -> 1 @@ -689,7 +689,7 @@ of its line with AT-END." (null (funcall face "x:"))))) (test-flan-fln--in "fn f(d: Dir) -> i64 - let g = fn(a: Fn(i64) -> i64, b) -> Vec(i64) + let g = fn(a: Fn(i64) -> i64, b) -> Vec(i64) => a(b) restart-case 3 @@ -711,6 +711,103 @@ of its line with AT-END." (test-flan-fln--is "an enum member as a constant" (funcall face "Dir.north") 'font-lock-constant-face) (test-flan-fln--is "an arm's value is not a type" (funcall face "twice(1)") nil) (test-flan-fln--is "nor after a pattern with parentheses" (funcall face "r\n") nil))) +(test-flan-fln--in "struct DiskFull :parent IoError + free: i64 + +type Row = Vec(i64) + +macro repeat(i, n, & body) + quote + for ~i in range(~n) + ~@body + +fn gcd(a: i32, b: i32) -> i32 + loop x = a, y = b + if y == 0 then x else recur(y, x % y) +" + (font-lock-ensure) + (let ((face (lambda (needle) + (save-excursion (goto-char (point-min)) (search-forward needle) + (get-text-property (match-beginning 0) 'face))))) + (test-flan-fln--is "a struct's parent is a type" (funcall face "IoError") 'font-lock-type-face) + (test-flan-fln--is "and :parent a keyword" (funcall face ":parent") 'font-lock-constant-face) + (test-flan-fln--is "macro is a keyword" (funcall face "macro") 'font-lock-keyword-face) + (test-flan-fln--is "and its name a function's" (funcall face "repeat") 'font-lock-function-name-face) + (test-flan-fln--is "loop is a keyword" (funcall face "loop") 'font-lock-keyword-face) + (test-flan-fln--is "type is a keyword" (funcall face "type") 'font-lock-keyword-face) + (test-flan-fln--is "an alias's name is a type" (funcall face "Row") 'font-lock-type-face) + (test-flan-fln--is "and so is what it names" (funcall face "Vec(i64)") 'font-lock-type-face)) + (goto-char (point-min)) + (search-forward "Row") + (test-flan-fln--is "an alias installs as a defalias" + (flan-fln--declaration-head-at (line-beginning-position)) "defalias") + (goto-char (point-min)) + (search-forward "~@body") + (test-flan-fln--is "a macro is one top-level form" + (test-flan-fln--thing 'flan-fln-toplevel) + "macro repeat(i, n, & body) + quote + for ~i in range(~n) + ~@body") + (test-flan-fln--is "installed as a defmacro" + (flan-fln--declaration-head-at (car (flan-fln--toplevel-bounds (point)))) + "defmacro") + (search-forward "recur") + (test-flan-fln--is "a loop's statement is its header and block" + (progn (forward-line -1) + (test-flan-fln--thing 'flan-fln-statement)) + "loop x = a, y = b + if y == 0 then x else recur(y, x % y)") + (test-flan-fln--is "and its body the block" + (test-flan-fln--thing 'flan-fln-body) + "if y == 0 then x else recur(y, x % y)") + (goto-char (point-min)) + (test-flan-fln--is "the struct's head is defstruct" + (flan-fln--declaration-head-at (point)) "defstruct") + (let ((imenu-generic-expression flan-fln-imenu-generic-expression)) + (test-flan--check "imenu lists the macro" + (assoc "repeat" (cdr (assoc "Macros" (imenu--generic-function + imenu-generic-expression))))))) +(test-flan-fln--in "defmacro(m, [x]): + quote + ~x + +defmethod(area, point, [p]): + 0 + +defclass(Shape, [w dyn]): + +defstruct(Io, :parent, Error, []) + +defconst(k, 3) +" + (font-lock-ensure) + (let ((face (lambda (needle) + (save-excursion (goto-char (point-min)) (search-forward needle) + (get-text-property (match-beginning 0) 'face))))) + (dolist (c '(("defmacro" m font-lock-function-name-face) + ("defmethod" area font-lock-function-name-face) + ("defclass" Shape font-lock-type-face) + ("defstruct" Io font-lock-type-face) + ("defconst" k font-lock-variable-name-face))) + (test-flan-fln--is (format "a fallback %s's head is a keyword" (car c)) + (funcall face (concat (car c) "(")) 'font-lock-keyword-face) + (test-flan-fln--is (format "and the name it defines, %s" (cadr c)) + (save-excursion + (goto-char (point-min)) + (search-forward (concat (car c) "(")) + (get-text-property (point) 'face)) + (nth 2 c)))) + (let ((index (imenu--generic-function flan-fln-imenu-generic-expression))) + (test-flan--check "imenu lists a fallback defmacro" + (assoc "m" (cdr (assoc "Macros" index)))) + (test-flan--check "a fallback defmethod" + (assoc "area" (cdr (assoc "Functions" index)))) + (test-flan--check "a fallback defclass and defstruct" + (and (assoc "Shape" (cdr (assoc "Types" index))) + (assoc "Io" (cdr (assoc "Types" index))))) + (test-flan--check "and a fallback defconst" + (assoc "k" (cdr (assoc "Variables" index)))))) (test-flan-fln--in "fn far(a: i64,\n b: i64) -> Point\n match a\n Some(x) -> Other\n" (font-lock-ensure) (let ((face (lambda (needle) @@ -756,7 +853,7 @@ of its line with AT-END." (test-flan-fln--is "after a trailing colon too" (test-flan-fln--tabs "rl/with-drawing():\n|" 1) 2) (test-flan-fln--is "and after let x =" - (test-flan-fln--tabs "def colors =\n|" 1) 2) + (test-flan-fln--tabs "let colors =\n|" 1) 2) (test-flan-fln--is "but not after a one-line fn" (test-flan-fln--tabs "fn f() -> i32 = 1\n|" 1) 0) (test-flan-fln--is "else goes to its if's column, whatever the depth" @@ -779,14 +876,97 @@ of its line with AT-END." ("let r = if c" "a let's if") ("x = if c" "an assignment's if") ("let r = handler-case" "a let's handler-case") - ("let f = fn(a, b)" "a lambda header") - ("let f = fn(a: i64, b) -> i64" "a typed lambda header") - ("let f = fn(g: Fn(i64) -> i64) -> Option(i64)" "one with a function type in it") - ("fn(a: i64) -> i64" "a typed lambda as a statement"))) + ("let f = fn(a, b) =>" "a lambda header") + ("let f = fn(a: i64, b) -> i64 =>" "a typed lambda header") + ("let f = fn(g: Fn(i64) -> i64) -> Option(i64) =>" "one with a function type in it") + ("let r = loop i = 0, acc = 1" "a let's loop") + ("loop i = 0, acc = 1" "a loop") + ("fn(a: i64) -> i64 =>" "a typed lambda as a statement"))) (test-flan-fln--is (format "unless its value goes on under it: %s" (cadr c)) (test-flan-fln--tabs (concat "fn f()\n " (car c) "\n|") 1) 4)) (test-flan-fln--is "but not a typed lambda with its body on the line" - (test-flan-fln--tabs "fn f()\n let f = fn(a: i64) -> i64 = a\n|" 1) 2) + (test-flan-fln--tabs "fn f()\n let f = fn(a: i64) -> i64 => a\n|" 1) 2) +(test-flan-fln--is "a header word being assigned opens nothing" + (test-flan-fln--tabs "fn f()\n for = 1\n|" 1) 2) +(test-flan-fln--in "fn f()\n handler-case\n g()\n on E(c)\n h(c)\n on = 2\n data += 1\n" + (font-lock-ensure) + (goto-char (point-min)) + (search-forward "on = 2") + (test-flan--check "nor is a clause word being assigned a clause" + (not (flan-fln--clause-line-p (line-beginning-position)))) + (test-flan-fln--is "or drawn as a keyword" + (get-text-property (match-beginning 0) 'face) nil) + (search-forward "data") + (test-flan-fln--is "and a header word assigned is not either" + (get-text-property (match-beginning 0) 'face) nil)) +(test-flan-fln--is "a macro opens a block" + (test-flan-fln--tabs "macro repeat(i, n, & body)\n|" 1) 2) +(test-flan-fln--is "and a struct with a parent" + (test-flan-fln--tabs "struct DiskFull :parent IoError\n|" 1) 2) +(test-flan-fln--in "let speed: i64 = 3\n\nfn f()\n let x = 1\n x\n" + (font-lock-ensure) + (test-flan-fln--is "a top-level let's name is a variable's" + (save-excursion (goto-char (point-min)) (search-forward "speed") + (get-text-property (match-beginning 0) 'face)) + 'font-lock-variable-name-face) + (test-flan-fln--is "and installs as a def" (flan-fln--declaration-head-at (point-min)) "def") + (test-flan--check "imenu lists it" + (assoc "speed" (cdr (assoc "Variables" (imenu--generic-function + flan-fln-imenu-generic-expression))))) + (test-flan--check "it is no local let" (not (flan-fln--let-p (point-min)))) + (goto-char (point-min)) + (search-forward "let x") + (test-flan--check "one in a fn is" (flan-fln--let-p (line-beginning-position))) + (test-flan-fln--is "and its name is not a global's" + (get-text-property (match-end 0) 'face) nil)) +(test-flan-fln--is "a class with a slot per line opens a block" + (test-flan-fln--tabs "class point\n|" 1) 2) +(test-flan-fln--is "not one on one line" + (test-flan-fln--tabs "class point(x, y)\n|" 1) 0) +(test-flan-fln--is "a method opens its block" + (test-flan-fln--tabs "method area(p: point)\n|" 1) 2) +(test-flan-fln--is "but not one with its value on the line" + (test-flan-fln--tabs "method kind(v) when :int = 1\n|" 1) 0) +(test-flan-fln--is "a multi with a block opens it" + (test-flan-fln--tabs "multi kind(v) -> dyn\n|" 1) 2) +(test-flan-fln--is "a generic never does" + (test-flan-fln--tabs "generic area(p) -> dyn\n|" 1) 0) +(test-flan-fln--in "class point(x, y)\n\ngeneric area(p) -> dyn\n\nmethod area(p: point)\n 1\n\nmulti kind(v) -> dyn = type-of(v)\n\nmethod kind(v) when :int = 2\n" + (font-lock-ensure) + (let ((face (lambda (needle) + (save-excursion (goto-char (point-min)) (search-forward needle) + (get-text-property (match-beginning 0) 'face))))) + (test-flan-fln--is "class is a keyword" (funcall face "class") 'font-lock-keyword-face) + (test-flan-fln--is "and its name a type" (funcall face "point(") 'font-lock-type-face) + (test-flan-fln--is "a generic's name is a function's" (funcall face "area(p)") 'font-lock-function-name-face) + (test-flan-fln--is "a method's class is a type" (funcall face "point)") 'font-lock-type-face) + (test-flan-fln--is "a method's when is a keyword" (funcall face "when") 'font-lock-keyword-face)) + (let ((index (imenu--generic-function flan-fln-imenu-generic-expression))) + (test-flan--check "imenu lists the class, the generic and the multi" + (and (assoc "point" (cdr (assoc "Types" index))) + (assoc "area" (cdr (assoc "Functions" index))) + (assoc "kind" (cdr (assoc "Functions" index)))))) + (goto-char (point-min)) + (search-forward "method area") + (test-flan-fln--is "a method installs as a defmethod" + (flan-fln--declaration-head-at (line-beginning-position)) "defmethod")) +(test-flan-fln--is "but not a struct on one line" + (test-flan-fln--tabs "struct Pt(x: i32, y: i32)\n|" 1) 0) +(test-flan-fln--is "nor one with a parent" + (test-flan-fln--tabs "struct D(free: i64) :parent IoError\n|" 1) 0) +(test-flan-fln--in "struct D(free: i64) :parent IoError\n\nunion U(a: i32)\n" + (font-lock-ensure) + (let ((face (lambda (needle) + (save-excursion (goto-char (point-min)) (search-forward needle) + (get-text-property (match-beginning 0) 'face))))) + (test-flan-fln--is "a one-line struct's name is a type" (funcall face "D(") 'font-lock-type-face) + (test-flan-fln--is "its field's type too" (funcall face "i64") 'font-lock-type-face) + (test-flan-fln--is "and its parent" (funcall face "IoError") 'font-lock-type-face) + (test-flan-fln--is "a one-line union's name" (funcall face "U(") 'font-lock-type-face)) + (goto-char (point-min)) + (test-flan-fln--is "a one-line struct is a top-level form of one line" + (test-flan-fln--thing 'flan-fln-toplevel) + "struct D(free: i64) :parent IoError")) (test-flan-fln--is "a one-line fn whose value is a match opens it" (test-flan-fln--tabs "fn f(x) = match x\n|" 1) 2) (test-flan-fln--is "no deeper after a one-line else" @@ -849,6 +1029,108 @@ of its line with AT-END." (buffer-string) "fn f() -> ()\n if a\n while x\n y\n\n")) +;;; A lambda's block inside brackets + +(defconst test-flan-fln--lam + "fn f(xs) -> i64 + let n = 1 + sort-by(xs, fn(a, b) => + let d = a - b + d < n) + map(xs, fn(x) => + g(x) + x + ) + h(n) +") + +(defun test-flan-fln--lam-at (needle fn &optional at-end) + "What FN sends with point at NEEDLE in the lambda program, or at the end of +its line with AT-END." + (test-flan-fln--in (test-flan-fln--at test-flan-fln--lam needle) + (when at-end (end-of-line)) + (let ((r (test-flan-fln--sending (funcall fn)))) + (and r (list (test-flan-fln--sent-code r) (plist-get r :pause)))))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam "sort-by") + (test-flan-fln--is "a call with a block lambda is one statement, block and closer too" + (test-flan-fln--thing 'flan-fln-statement) + "sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)") + (test-flan-fln--is "its body is the lambda's block, less the call's closer" + (test-flan-fln--thing 'flan-fln-body) + "let d = a - b\n d < n")) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam "d < n") + (test-flan-fln--is "a line of the block is a statement of its own, less the closer" + (test-flan-fln--thing 'flan-fln-statement) "d < n")) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam " )") + (test-flan-fln--is "a closer on a line of its own belongs to the call" + (test-flan-fln--thing 'flan-fln-statement) + "map(xs, fn(x) =>\n g(x)\n x\n )")) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam " x\n") + (test-flan-fln--is "and not to the statement above it" + (test-flan-fln--thing 'flan-fln-statement) "x")) +(test-flan-fln--is "C-c C-e on the header sends the call, block and all" + (car (test-flan-fln--lam-at "sort-by" #'flan-fln-eval-statement)) + "sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)") +(test-flan-fln--is "and C-x C-e at the header's end" + (car (test-flan-fln--lam-at "sort-by" #'flan-fln-eval-last t)) + "sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)") +(test-flan-fln--is "C-x C-e at the end of the block's last line sends its statement" + (car (test-flan-fln--lam-at "d < n" #'flan-fln-eval-last t)) + "d < n") +(test-flan-fln--is "C-c C-e on a line of the block sends that statement" + (car (test-flan-fln--lam-at "g(x)" #'flan-fln-eval-statement)) + "g(x)") +(dolist (c '(("let d" (4 5) "a statement in the block, where the reader starts it") + ("g(x)" (7 5) "one in a block with its closer on a line of its own") + ("a, b)" (3 15) "the lambda, from its fn") + ("xs, fn(a" (3 3) "the call, from its name"))) + (test-flan-fln--is (format "C-u C-c C-c marks %s" (nth 2 c)) + (cadr (test-flan-fln--lam-at (car c) (lambda () (flan-fln-eval-defun '(4))))) + (cadr c))) +(test-flan-fln--is "TAB after a => inside brackets goes one level in" + (test-flan-fln--tabs "fn f()\n sort-by(xs, fn(a, b) =>\n|" 1) 4) +(test-flan-fln--is "and a line of the block stays in it" + (test-flan-fln--tabs "fn f()\n sort-by(xs, fn(a, b) =>\n g(a)\n| a < b)" 1) 4) +(test-flan-fln--is "under a header on a wrapped argument line, in from that line" + (test-flan-fln--tabs "f(a,\n fn(b) =>\n|" 1) 4) +(test-flan-fln--is "a closer on its own line goes to the call's column" + (test-flan-fln--tabs "fn f()\n m(xs, fn(x) =>\n x\n|)" 1) 2) +(test-flan-fln--is "after the block's brackets close, its column is no longer offered" + (list (test-flan-fln--tabs "fn f()\n app(k, fn(x) =>\n x)\n|g()" 1) + (test-flan-fln--tabs "fn f()\n app(k, fn(x) =>\n x)\n|g()" 2) + (test-flan-fln--tabs "fn f()\n app(k, fn(x) =>\n x)\n|g()" 3)) + '(0 2 0)) +(test-flan-fln--is "inside a bracket in the block, under its first argument" + (test-flan-fln--tabs "fn f()\n m(xs, fn(x) =>\n g(x,\n|y))" 1) 6) +(test-flan-fln--in "fn f()\n m(xs, fn(x) => x)\n" + (font-lock-ensure) + (test-flan-fln--is "=> is a keyword" + (save-excursion (goto-char (point-min)) (search-forward "=>") + (get-text-property (match-beginning 0) 'face)) + 'font-lock-keyword-face)) + +(defconst test-flan-fln--lam-arm + "fn f(n: i64) -> i64 + match n + 1 -> app(1, fn(a) => + a * 2) + _ -> 0 +") +(test-flan-fln--is "C-x C-e at the end of an arm whose value ends in => sends the value, block and all" + (test-flan-fln--in (test-flan-fln--at test-flan-fln--lam-arm "1 ->") + (end-of-line) + (test-flan-fln--sent-code (test-flan-fln--sending (flan-fln-eval-last)))) + "app(1, fn(a) =>\n a * 2)") +(test-flan-fln--is "and C-u C-c C-c on its pattern stops at the value" + (test-flan-fln--in (test-flan-fln--at test-flan-fln--lam-arm "1 ->") + (plist-get (test-flan-fln--sending (flan-fln-eval-defun '(4))) :pause)) + '(3 10)) +(test-flan-fln--is "an else whose value ends in => takes the block too" + (test-flan-fln--in "fn f(c)\n if c\n g()\n |else app(1, fn(a) =>\n a)\n" + (test-flan-fln--text (flan-fln--clause-value (line-beginning-position)))) + "app(1, fn(a) =>\n a)") + ;;; Block editing (test-flan-fln--in "fn f() -> ()\n if a\n |b()\n c()\n d()\n" diff --git a/lib/check.ml b/lib/check.ml index 169a113a..62929a34 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -6125,12 +6125,36 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = let p, pty = check_place ctx loc p in let v = lit_down ctx key pty v in expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v))) + (* (set (.name x) v) on a dyn is (put x :name v): a class slot's declared + type is checked by put, and a map takes a key it did not hold. Not + [flan_dyn_slot_set], which refuses a plain map — .name reads either, so + assigning it writes either. The target is checked once, here. *) + | Ast.Set (Ast.Pfield (target, name), v) -> + let t = check_target ctx target in + if t.Tast.ty = Types.Dyn then begin + refuse_const_change ctx loc t; + let k = dyn_kw ctx loc name in + let v = check ctx ~want:Types.Dyn v in + expect ctx loc ~want + (rt loc Types.Unit "flan_dyn_map_put" [ t; k; v; here loc ]) + end else begin + let p, pty = field_place ~store:true ctx loc target t name in + let v = check ctx ~want:pty v in + expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v))) + end | Ast.Set (p, v) -> let p, pty = check_place ctx loc p in let v = check ctx ~want:pty v in expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v))) + (* On a dyn, (.name x) is (get x :name) — the same call, so a missing key is + nil and a value that is not a map traps with get's own sentence. *) | Ast.Field (target, name) -> - let target, sname = struct_target ctx target in + let t = check_target ctx target in + if t.Tast.ty = Types.Dyn then + expect ctx loc ~want + (rt loc Types.Dyn "flan_dyn_get" [ t; dyn_kw ctx loc name; here loc ]) + else + let target, sname = struct_of ctx target t in let s = Option.get (fields_named ctx.env sname) in (match Tast.field_index s name with | None -> @@ -6675,7 +6699,7 @@ and check_fn ctx ~want ?gen loc (params : string list) body = "nothing here says what this fn's parameters are — an fn takes \ its types from the position it is written in. Pass it where a \ Fn(T, ...) -> R is expected, or name the type where it is \ - bound: let f: Fn(T, ...) -> R = fn(...)" + bound: let f: Fn(T, ...) -> R = fn(...) => ..." else fail loc "nothing here says what this fn's parameters are — an fn takes \ @@ -8976,7 +9000,7 @@ and array_build ctx loc ns elem ~pre ~element = [n T] the literal is, since an array literal is never a slice. *) and check_the ctx ~want loc (t : Ast.texpr) (v : Ast.expr) = let ty = resolve ctx.env t in - (* A typed .fln lambda, [fn(c: C) -> bool = ...], reads as [(the (Fn [C] + (* A typed .fln lambda, [fn(c: C) -> bool => ...], reads as [(the (Fn [C] bool) (fn ...))]; where a CFn of the same signature is wanted, the literal is that CFn, as an untyped one would be. *) let ty = @@ -9828,6 +9852,15 @@ and unknown_name : 'a. ?setting:bool -> ctx -> Loc.t -> string -> 'a = else Loc.failk "check/dot-access" loc ~notes "unknown name %s — %s, and %s has no field %s" name how sn field + | None, Some Types.Dyn -> + let how = + if setting then Printf.sprintf "(set (.%s %s) ...)" field head + else Printf.sprintf "(.%s %s)" field head + in + Loc.failk "check/dot-access" loc + "unknown name %s — a dot is part of the name here, not field access. \ + %s is dyn, and its :%s is reached with %s" + name head field how | None, Some t -> Loc.failk "check/dot-access" loc "unknown name %s — a dot is part of the name here, not field access. \ @@ -9907,9 +9940,9 @@ and fields_named env n : Tast.structure option = (* The target of [.field] is a struct or an untagged union, or one level of pointer to one. The auto-deref is inserted here as a real node, so no - backend re-derives it. *) -and struct_target ctx (target : Ast.expr) : Tast.expr * string = - let t = check_target ctx target in + backend re-derives it. The target comes checked, because every caller + looks first for a dyn, whose [.name] is a map entry and not a field. *) +and struct_of ctx (target : Ast.expr) (t : Tast.expr) : Tast.expr * string = let has n = fields_named ctx.env n <> None in match t.Tast.ty with | Types.Named n when has n -> t, n @@ -10044,15 +10077,19 @@ and check_place ?(store = true) ctx loc (p : Ast.place) : Tast.place * Types.t = | Some (ty, false) -> Tast.Pglobal name, ty | None -> unknown_name ~setting:true ctx loc name) | Ast.Pfield (target, name) -> - let target, sname = struct_target ctx target in - let s = Option.get (fields_named ctx.env sname) in - (match Tast.field_index s name with - | None -> - Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname) - "%s has no field %s" (tyname loc (Types.Named sname)) name - | Some i -> - if store then Option.iter (refuse_const_place ctx.env loc) (const_reached target); - Tast.Pfield (target, i), (List.nth s.Tast.fields i).Tast.fty) + let t = check_target ctx target in + if t.Tast.ty = Types.Dyn then begin + let x = match target.Ast.e with Ast.Var x -> x | _ -> "x" in + if Source.indented_at loc then + fail loc + "%s.%s is an entry of a dyn map, and has no address. Read it into \ + a local: let v = %s.%s" x name x name + else + fail loc + "(.%s %s) is an entry of a dyn map, and has no address. Read it \ + into a local: (let [v (.%s %s)] ...)" name x name x + end; + field_place ~store ctx loc target t name | Ast.Pindex (target, idx) -> let target = check_target ctx target in (match target.Tast.ty with @@ -10082,6 +10119,21 @@ and check_place ?(store = true) ctx loc (p : Ast.place) : Tast.place * Types.t = "a class slot (get inst :slot) is written with set and has no address. \ Read it into a local with let" +(* The dyn keyword [:name], for a dyn's [.name]. *) +and dyn_kw ctx loc name = check ctx ~want:Types.Dyn { Ast.e = Ast.Kw name; loc } + +(* A struct field as a place, over a target already checked. *) +and field_place ~store ctx loc target t name = + let target, sname = struct_of ctx target t in + let s = Option.get (fields_named ctx.env sname) in + match Tast.field_index s name with + | None -> + Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname) + "%s has no field %s" (tyname loc (Types.Named sname)) name + | Some i -> + if store then Option.iter (refuse_const_place ctx.env loc) (const_reached target); + Tast.Pfield (target, i), (List.nth s.Tast.fields i).Tast.fty + (* An index or a slice bound that is a literal is known now, so it is an error now rather than a trap later. Only literals: a [defconst] is a global in the typed IR, not a folded constant, so [(at arr size)] still traps at runtime — @@ -12197,8 +12249,8 @@ and named_call ?(qualified = false) ctx ~want loc name args = stored under the key. *) if target.Tast.ty = Types.Dyn then expect ctx loc ~want - (rt loc Types.Dyn "flan_dyn_map_get" - [ target; check ctx ~want:Types.Dyn k ]) + (rt loc Types.Dyn "flan_dyn_get" + [ target; check ctx ~want:Types.Dyn k; here loc ]) else begin let kt, vt = map_kv loc "get" target.Tast.ty in let k = check ctx ~want:kt k in diff --git a/lib/classes.ml b/lib/classes.ml index cc006999..f6ed51a8 100644 --- a/lib/classes.ml +++ b/lib/classes.ml @@ -185,9 +185,9 @@ let collect (decls : Ast.decl list) = is what [class-of] answers and what a generic dispatches on. Named-slot construction — the dyn twin of [(Cursor {.src s})], with an - omitted slot meaning nil — is deferred, and so is refusing an unknown slot - at [(get p :z)]. Both are recorded in TODO.org, "Class features deferred, - each with its reason". *) + omitted slot meaning nil — is deferred (TODO.org, "Class features deferred, + each with its reason"). An unknown slot, [(get p :z)], is refused at run + time by the runtime's [trap_no_slot]. *) let constructor n (slots : Ast.field list) loc : Ast.decl = (* Two slots of one name would write one entry and read one value, and the constructor would take two arguments for it. The duplicate parameter diff --git a/lib/dev.ml b/lib/dev.ml index 7bf0d8d6..4f078e5f 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -4590,10 +4590,19 @@ let rec handle t req = match Wire.string_field req "syntax", Wire.string_field req "op", Wire.string_field req "file" with | (Some _ as s), _, _ -> Source.syntax_of_field s - (* A whole file named with no [:syntax] is in the syntax its name says: - that is not a guess, it is what [Source.read_file] would do. *) - | None, Some "load-file", Some f when Source.is_indented f -> Source.Indented - | None, _, _ -> Source.Paren + (* With no [:syntax], a file named is in the syntax its name says — what + [Source.read_file] would do — for every op, so code sent from a .fln + buffer by a client that left the field out is not read as parens. A + paren expansion sent back under a .fln name says [:syntax "paren"]. *) + | None, _, Some f when Source.is_source f -> + if Source.is_indented f then Source.Indented else Source.Paren + (* A pseudo-name — "", "", whose text is built in parens — + is paren. No file at all is the program's own syntax: evaluating in a + stopped frame names none, and its code is written as the program is. *) + | None, _, Some _ -> Source.Paren + | None, _, None -> + if Source.is_indented t.session.Session.file then Source.Indented + else Source.Paren in let at = match Wire.int_field req "line", Wire.int_field req "col" with diff --git a/lib/emit.ml b/lib/emit.ml index 2f848442..51ace083 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -5007,6 +5007,7 @@ declare void @flan_dyn_class_def(i64, ptr, i64) declare void @flan_dyn_class_hook(ptr) declare i64 @flan_dyn_kw(ptr, i64) declare i64 @flan_dyn_map_get(i64, i64) +declare i64 @flan_dyn_get(i64, i64, ptr, i64) declare void @flan_dyn_map_set(i64, i64, i64) declare i64 @flan_dyn_map_contains(i64, i64) ; The ones that trap carry the site as ptr+len, the way the bounds and diff --git a/lib/indent_printer.ml b/lib/indent_printer.ml index 0214b69a..a86235e1 100644 --- a/lib/indent_printer.ml +++ b/lib/indent_printer.ml @@ -74,6 +74,11 @@ let is_sym s (f : Form.t) = match f.v with Form.Sym x -> x = s | _ -> false [let] there takes in what follows it and no one-argument [and] is dropped. *) let quasi = ref 0 +(* While [hole_lines] looks for where a lambda sits in a line, the lambda is + printed as [hole_sym], at a lambda's level. *) +let hole = ref false +let hole_sym = "\003lambda\003" + let in_quasi (f : Form.t) k = match f.v with | Form.List ({ v = Form.Sym "quasiquote"; _ } :: _) -> @@ -296,6 +301,7 @@ let flatten (f : Form.t) (rest : Form.t list) = one-line [if] or a lambda. *) let rec expr (f : Form.t) : string * int = match f.v with + | Form.Sym s when !hole && s = hole_sym -> (s, 0) | Form.Sym s -> sym f s | Form.Kw k -> if kw_ok k then (":" ^ k, 10) else unprintable f "a keyword with no spelling" @@ -420,10 +426,10 @@ and list f h args = (s ^ fst (expr m), 9) | Form.Sym "the", _ when (match typed_lambda f with Some (_, [ _ ]) -> true | _ -> false) -> (match typed_lambda f with - | Some (head, [ body ]) -> (head ^ " = " ^ unit_text body, 0) + | Some (head, [ body ]) -> (head ^ " => " ^ unit_text body, 0) | _ -> assert false) | Form.Sym "fn", [ { v = Form.Vec ps; _ }; body ] when List.for_all sym_param ps -> - ("fn(" ^ commas ps ^ ") = " ^ unit_text body, 0) + ("fn(" ^ commas ps ^ ") => " ^ unit_text body, 0) | Form.Sym "if", [ c; a; b ] -> ("if " ^ at 1 c ^ " then " ^ inline_text ~lvl:1 a ^ " else " ^ inline_text b, 0) | _ -> call () @@ -499,10 +505,15 @@ let rec ty (f : Form.t) = primitive, a capitalised or [$] name, or a bracket. [[x y]] with a lowercase [y] keeps the fallback, because what it means depends on whether [y] names a type. *) +(* The file's own class names, which are types and lowercase. Set by + [program]. *) +let classes : string list ref = ref [] + let type_shaped (f : Form.t) = match f.v with | Form.Sym t -> List.mem t Types.primitive_names || (t <> "" && t.[0] = '$') || R.capitalised t + || List.mem t !classes | Form.List [] | Form.List ({ v = Form.Sym _; _ } :: _) | Form.Vec _ -> true | _ -> false @@ -544,7 +555,17 @@ let lead_word text = (* A statement whose text leads with a reserved word, parenthesised. *) let guard text = let w, spaced = lead_word text in - if spaced && List.mem w reserved then paren text else text + (* [data = 3]: a name being assigned is read as one, header word or not. *) + let assigned = + let k = String.length w + 1 in + List.exists + (fun op -> + let o = op ^ " " in + String.length text >= k + String.length o + && String.sub text k (String.length o) = o) + [ "="; "+="; "-="; "*="; "/=" ] + in + if spaced && List.mem w reserved && not assigned then paren text else text let stmts_of (f : Form.t) = match f.v with @@ -703,9 +724,12 @@ and plain n (f : Form.t) : string list = | _ -> head_text h ^ "(" ^ commas fixed ^ "):" in [ ind n ^ guard opener ] @ block ~seq (n + 2) rest - | _ when n + String.length text > width && fst (expr f) = text -> - wrapped n "" f - | _ -> one) + | _ -> + match hole_lines ~guarded:true n "" f with + | Some ls -> ls + | None -> + if n + String.length text > width && fst (expr f) = text then wrapped n "" f + else one) | _ -> one (* A call too long for its line, broken after commas inside its @@ -761,6 +785,131 @@ and wrapped n prefix (f : Form.t) = go (ind n ^ open_) [] ts | _ -> [ ind n ^ prefix ^ at 0 f ] +(* A lambda as its header, [fn(a, b)] or [fn(a: C) -> R], and its body. *) +and lambda_parts (f : Form.t) = + match typed_lambda f with + | Some _ as l -> l + | None -> + match f.v with + | Form.List ({ v = Form.Sym "fn"; _ } :: { v = Form.Vec ps; _ } :: (_ :: _ as body)) + when List.for_all sym_param ps -> + Some ("fn(" ^ commas ps ^ ")", body) + | _ -> None + +(* A lambda body that reads better as a block under [=>] than on the line: + several statements, or one that is a statement. *) +and block_body (body : Form.t list) = + match body with + | [ { v = Form.List ({ v = Form.Sym h; _ } :: _); _ } ] -> + List.mem h sugar_heads && h <> "if" && h <> "update" + | [ _ ] -> false + | _ -> true + +(* A lambda that takes a block: one whose body does, or has a comment in + it, or holds a lambda that takes one. *) +and wants_block (l : Form.t) body = + let rec holds (x : Form.t) = + match lambda_parts x with + | Some (_, b) -> wants_block x b + | None -> + match x.v with + | Form.List ({ v = Form.Sym ("quote" | "quasiquote"); _ } :: _) -> false + | Form.List xs | Form.Vec xs | Form.Map xs -> List.exists holds xs + | _ -> false + in + block_body body || !inside l || List.exists holds body + +(* A block under [=>]: a body that is one [do] is its statements, written + straight under the header rather than in a [do:] of their own. *) +and lambda_block n body = + match body with + | [ { Form.v = Form.List ({ v = Form.Sym "do"; _ } :: (_ :: _ :: _ as ss)); _ } ] -> block (n + 2) ss + | _ -> block (n + 2) body + +(* A line holding a lambda that takes its block there: [prefix] and the + line's text up to the lambda, [fn(x) =>], the block under it, and what + followed the lambda on the line at the end of the block's last line. The + reader ends a block inside brackets where they close, so the lambda must + be the last thing in its brackets: what follows it starts with a closer. + Each lambda in [f], outside the others, is tried in turn, a lambda that + wants a block (several statements, a statement, a comment inside) or any + when the line is too long; its place is found by printing the line with + a placeholder where it stands. [guarded]: the text is a statement's. *) +and hole_lines ?(guarded = false) n prefix (f : Form.t) = + let rec cands (x : Form.t) = + if lambda_parts x <> None then [ x ] + else + match x.v with + | Form.List ({ v = Form.Sym ("quote" | "quasiquote"); _ } :: _) -> [] + | Form.List xs | Form.Vec xs | Form.Map xs -> List.concat_map cands xs + | _ -> [] + in + let inner = + match f.v with + | Form.List xs | Form.Vec xs | Form.Map xs -> List.concat_map cands xs + | _ -> [] + in + if inner = [] then None + else + let long = lazy (n + String.length prefix + String.length (fst (expr f)) > width) in + let rec subst (l : Form.t) (x : Form.t) = + if x == l then Form.make (Form.Sym hole_sym) l.loc + else + match x.v with + | Form.List xs -> { x with v = Form.List (List.map (subst l) xs) } + | Form.Vec xs -> { x with v = Form.Vec (List.map (subst l) xs) } + | Form.Map xs -> { x with v = Form.Map (List.map (subst l) xs) } + | _ -> x + in + let find t = + let k = String.length hole_sym in + let rec go i = + if i + k > String.length t then None + else if String.sub t i k = hole_sym then Some i + else go (i + 1) + in + go 0 + in + let rec try_ = function + | [] -> None + | (l : Form.t) :: rest -> + let head, body = Option.get (lambda_parts l) in + if not (wants_block l body || Lazy.force long) then try_ rest + else begin + hole := true; + let t = + Fun.protect ~finally:(fun () -> hole := false) + (fun () -> fst (expr (subst l f))) + in + let t = if guarded then guard t else t in + match find t with + | Some i + when (let j = i + String.length hole_sym in + j < String.length t && (t.[j] = ')' || t.[j] = ']' || t.[j] = '}')) -> + let j = i + String.length hole_sym in + let post = String.sub t j (String.length t - j) in + let ls = lambda_block n body in + let ls = + match List.rev ls with + | last :: before -> List.rev ((last ^ post) :: before) + | [] -> ls + in + Some ((ind n ^ prefix ^ String.sub t 0 i ^ head ^ " =>") :: ls) + | _ -> try_ rest + end + in + try_ inner + +(* [prefix = fn(a) =>] or [prefix = f(x, fn(a) =>] and a lambda's block, + when the value is a lambda that takes one or ends a bracket with one. *) +and lambda_value n prefix (v : Form.t) = + match lambda_parts v with + | Some (head, body) + when wants_block v body + || n + String.length prefix + 3 + String.length (fst (expr v)) > width -> + Some ((ind n ^ prefix ^ " = " ^ head ^ " =>") :: lambda_block n body) + | _ -> hole_lines n (prefix ^ " = ") v + (* [prefix = v], or [prefix =] and the value as an indented block when it is too long for the line. *) and value_lines n prefix (v : Form.t) = @@ -771,15 +920,16 @@ and value_lines n prefix (v : Form.t) = | _ -> false in if is_do then [ ind n ^ prefix ^ " =" ] @ block (n + 2) (stmts_of v) - else if n + String.length inline <= width then [ ind n ^ inline ] + else if loop_head v <> None then + let head, body = Option.get (loop_head v) in + [ ind n ^ prefix ^ " = " ^ head ] @ block (n + 2) body + else + match lambda_value n prefix v with + | Some ls -> ls + | None -> + if n + String.length inline <= width then [ ind n ^ inline ] else match v.v with - | _ when typed_lambda v <> None -> - let head, body = Option.get (typed_lambda v) in - [ ind n ^ prefix ^ " = " ^ head ] @ block (n + 2) body - | Form.List ({ v = Form.Sym "fn"; _ } :: { v = Form.Vec ps; _ } :: (_ :: _ as body)) - when List.for_all sym_param ps -> - [ ind n ^ prefix ^ " = fn(" ^ commas ps ^ ")" ] @ block (n + 2) body | Form.List ({ v = Form.Sym h; _ } :: _) when not (List.mem h sugar_heads || h = "fn" || h = "if") -> wrapped n (prefix ^ " = ") v @@ -789,6 +939,23 @@ and value_lines n prefix (v : Form.t) = and slot n (f : Form.t) = block n (stmts_of f) +(* [(loop [x a y b] body ...)] as the header [loop x = a, y = b] and its + body, when every binding is a plain name. A lambda or one-line if as a + value is parenthesised, so its else cannot run on into the next binding. *) +and loop_head (f : Form.t) = + match f.v with + | Form.List ({ v = Form.Sym "loop"; _ } :: { v = Form.Vec bs; _ } :: (_ :: _ as body)) -> + (match pairs bs with + | Some (_ :: _ as prs) + when List.for_all (fun ((x : Form.t), _) -> + match x.v with Form.Sym x -> def_name x | _ -> false) prs -> + Some + ("loop " + ^ String.concat ", " (List.map (fun (x, v) -> fst (expr x) ^ " = " ^ at 1 v) prs), + body) + | _ -> None) + | _ -> None + and label_of = function | ({ Form.v = Form.Kw k; _ }) :: rest when kw_ok k -> (":" ^ k ^ " ", rest) | rest -> ("", rest) @@ -805,7 +972,8 @@ and sugar n (f : Form.t) : string list option = Some [ i ^ guard (inline_text f) ] | Form.List [ { v = Form.Sym "set"; _ }; t; v ] -> let line = i ^ guard (assign_text t v) in - if String.length line <= width then Some [ line ] + if String.length line <= width && lambda_value n (guard (at 9 t)) v = None + then Some [ line ] else Some (value_lines n (guard (at 9 t)) v) | Form.List [ { v = Form.Sym "if"; _ }; c; a; b ] -> let simple (x : Form.t) = @@ -877,7 +1045,10 @@ and sugar n (f : Form.t) : string list option = ((i ^ "for " ^ lbl ^ v ^ " in range(" ^ commas bs ^ ")") :: block (n + 2) body) | _ -> None) | Form.List [ { v = Form.Sym "return"; _ } ] -> Some [ i ^ "return" ] - | Form.List [ { v = Form.Sym "return"; _ }; v ] -> Some [ i ^ "return " ^ at 0 v ] + | Form.List [ { v = Form.Sym "return"; _ }; v ] -> + (match hole_lines n "return " v with + | Some ls -> Some ls + | None -> Some [ i ^ "return " ^ at 0 v ]) | Form.List [ { v = Form.Sym (("break" | "continue") as w); _ } ] -> Some [ i ^ w ] | Form.List [ { v = Form.Sym (("break" | "continue") as w); _ }; { v = Form.Kw k; _ } ] when kw_ok k -> @@ -939,9 +1110,9 @@ and sugar n (f : Form.t) : string list option = @ List.concat_map Option.get cs) | Form.List [ { v = Form.Sym "quasiquote"; _ }; x ] -> Some ((i ^ "quote") :: slot (n + 2) x) - | Form.List ({ v = Form.Sym "fn"; _ } :: { v = Form.Vec ps; _ } :: (_ :: _ :: _ as body)) - when List.for_all sym_param ps -> - Some ((guard (i ^ "fn(" ^ commas ps ^ ")")) :: block (n + 2) body) + | _ when (match lambda_parts f with Some (_, body) -> wants_block f body | None -> false) -> + let head, body = Option.get (lambda_parts f) in + Some ((i ^ head ^ " =>") :: lambda_block n body) | Form.List ({ v = Form.Sym (("defn" | "defn-") as d); _ } :: { v = Form.Sym name; _ } :: { v = Form.Vec ps; _ } :: ret :: body) when def_name name -> @@ -971,7 +1142,8 @@ and sugar n (f : Form.t) : string list option = | Form.List (({ v = Form.Sym h; _ } as hf) :: args) -> (* A call that takes a block is a statement, not a value. *) not (List.mem h sugar_heads) && body_split hf args = None - | _ -> true) + && lambda_value (n + 2) "" x = None + | _ -> lambda_value (n + 2) "" x = None) && String.length head + 3 + String.length (at 0 x) <= width && not (!inside f) -> Some [ head ^ " = " ^ unit_text x ] @@ -979,9 +1151,12 @@ and sugar n (f : Form.t) : string list option = | Form.List ({ v = Form.Sym (("def" | "defonce" | "defconst") as d); _ } :: { v = Form.Sym name; _ } :: rest) when def_name name -> - let w = match d with "def" -> "def" | "defonce" -> "once" | _ -> "const" in + (* A global [def] is a top-level [let]; nested, where a let is local, it + keeps the fallback. *) + let w = match d with "def" -> "let" | "defonce" -> "once" | _ -> "const" in let pre = i ^ w ^ " " ^ name in (match d, rest with + | "def", _ when n > 0 -> None | "defconst", [ v ] -> Some (value_lines n (w ^ " " ^ name) v) | "defconst", [ t; v ] -> Some (value_lines n (w ^ " " ^ name ^ ": " ^ ty t) v) | "defconst", _ -> None @@ -989,14 +1164,64 @@ and sugar n (f : Form.t) : string list option = | _, [ t ] when type_shaped t -> Some [ pre ^ ": " ^ ty t ] | _, [ t; v ] -> Some (value_lines n (w ^ " " ^ name ^ ": " ^ ty t) v) | _ -> None) - | Form.List [ { v = Form.Sym (("defstruct" | "defunion") as d); _ }; - { v = Form.Sym name; _ }; { v = Form.Vec fs; _ } ] + | Form.List ({ v = Form.Sym "loop"; _ } :: _) when loop_head f <> None -> + let head, body = Option.get (loop_head f) in + Some ((i ^ head) :: block (n + 2) body) + | Form.List ({ v = Form.Sym "defmacro"; _ } :: { v = Form.Sym name; _ } + :: { v = Form.Vec ps; _ } :: (_ :: _ as body)) when def_name name -> + (* A parameter is a name, a destructuring vector, or & and the rest's + name, last. *) + let rec go = function + | [] -> Some [] + | [ { Form.v = Form.Sym "&"; _ }; { Form.v = Form.Sym r; _ } ] when def_name r -> + Some [ "& " ^ r ] + | { Form.v = Form.Sym x; _ } :: rest when def_name x && x <> "&" -> + Option.map (fun r -> x :: r) (go rest) + | ({ Form.v = Form.Vec _; _ } as v) :: rest -> + Option.map (fun r -> fst (expr v) :: r) (go rest) + | _ -> None + in + Option.map + (fun pt -> + (i ^ "macro " ^ name ^ "(" ^ String.concat ", " pt ^ ")") :: block (n + 2) body) + (go ps) + | Form.List ({ v = Form.Sym (("defstruct" | "defunion") as d); _ } + :: { v = Form.Sym name; _ } :: rest) + when def_name name + && (match d, rest with + | _, [ { v = Form.Vec _; _ } ] -> true + (* [(defstruct N :parent P [])] keeps the fallback: no field lines + reads as the form with no vector. *) + | "defstruct", [ { v = Form.Kw "parent"; _ }; { v = Form.Sym pn; _ } ] + | "defstruct", [ { v = Form.Kw "parent"; _ }; { v = Form.Sym pn; _ }; + { v = Form.Vec (_ :: _); _ } ] -> + def_name pn + | _ -> false) -> + let parent, fs = + match rest with + | [ { v = Form.Vec fs; _ } ] -> ("", fs) + | [ _; pf ] -> (" :parent " ^ ty pf, []) + | [ _; pf; { v = Form.Vec fs; _ } ] -> (" :parent " ^ ty pf, fs) + | _ -> assert false + in + (* One line when it fits, [struct Pt(x: i32, y: i32)], and a line per + field otherwise. *) + let one = + match fs, params_text fs with + | _ :: _, Some pt -> + let line = + i ^ (if d = "defstruct" then "struct " else "union ") ^ name ^ "(" ^ pt ^ ")" ^ parent + in + if String.length line <= width && not (!inside f) then Some [ line ] else None + | _ -> None + in (match pairs fs with + | _ when one <> None -> one | Some prs when List.for_all (fun ((f : Form.t), _) -> match f.v with Form.Sym x -> def_name x | _ -> false) prs -> Some - ((i ^ (if d = "defstruct" then "struct " else "union ") ^ name) + ((i ^ (if d = "defstruct" then "struct " else "union ") ^ name ^ parent) :: List.map (fun ((f : Form.t), t) -> let fname = fst (expr f) in @@ -1004,6 +1229,58 @@ and sugar n (f : Form.t) : string list option = (ind (n + 2) ^ if is_sym "dyn" t then fname else fname ^ ": " ^ ty t)) prs) | _ -> None) + | Form.List [ { v = Form.Sym "defclass"; _ }; { v = Form.Sym name; _ }; { v = Form.Vec ss; _ } ] + when def_name name -> + (* A name, and the type after it when the next item is shaped like one: + read back, the slots are the same items in the same order. *) + let rec walk = function + | [] -> Some [] + | ({ Form.v = Form.Sym x; _ } as xf) :: t :: rest when def_name x && type_shaped t -> + Option.map (fun r -> (xf, Some t) :: r) (walk rest) + | ({ Form.v = Form.Sym x; _ } as xf) :: rest when def_name x -> + Option.map (fun r -> (xf, None) :: r) (walk rest) + | _ -> None + in + let slots = walk ss in + Option.map + (fun sl -> + let one (x, t) = + fst (expr x) ^ match t with Some t -> ": " ^ ty t | None -> "" + in + let line = i ^ "class " ^ name ^ "(" ^ String.concat ", " (List.map one sl) ^ ")" in + if sl = [] then [ i ^ "class " ^ name ] + else if String.length line <= width && not (!inside f) then [ line ] + else + (i ^ "class " ^ name) + :: List.map (fun ((x : Form.t), t) -> + Source_text.tag x.loc.Loc.line (ind (n + 2) ^ one (x, t))) sl) + slots + | Form.List [ { v = Form.Sym "defgeneric"; _ }; { v = Form.Sym name; _ }; + { v = Form.Vec ps; _ }; r ] + when def_name name && List.for_all sym_param ps && not (is_sym "_" r) -> + Some [ i ^ "generic " ^ name ^ "(" ^ commas ps ^ ") -> " ^ ty r ] + | Form.List ({ v = Form.Sym "defmulti"; _ } :: { v = Form.Sym name; _ } + :: { v = Form.Vec ps; _ } :: r :: (_ :: _ as body)) + when def_name name && List.for_all sym_param ps && not (is_sym "_" r) -> + Some (fn_like n f (i ^ "multi " ^ name ^ "(" ^ commas ps ^ ") -> " ^ ty r) body) + | Form.List ({ v = Form.Sym "defmethod"; _ } :: { v = Form.Sym name; _ } :: key + :: { v = Form.Vec ps; _ } :: (_ :: _ as body)) + when def_name name && List.for_all sym_param ps -> + (* A class written as the first parameter's type; any other value, and a + class with no parameter to hang it on, after when. *) + let head = + match key.v, ps with + | Form.Sym k, p0 :: rest when k <> "true" && k <> "false" && name_ok k -> + Some ("(" ^ fst (expr p0) ^ ": " ^ ty key + ^ String.concat "" (List.map (fun p -> ", " ^ fst (expr p)) rest) ^ ")") + | (Form.Kw _ | Form.Str _ | Form.Int _ | Form.Sym _), _ -> + Some ("(" ^ commas ps ^ ") when " ^ at 9 key) + | _ -> None + in + Option.map (fun h -> fn_like n f (i ^ "method " ^ name ^ h) body) head + | Form.List [ { v = Form.Sym "defalias"; _ }; { v = Form.Sym name; _ }; t ] + when def_name name && type_shaped t -> + Some [ i ^ "type " ^ name ^ " = " ^ ty t ] | Form.List [ { v = Form.Sym "defdata"; _ }; { v = Form.Sym name; _ }; { v = Form.Vec cs; _ } ] when def_name name -> let case (c : Form.t) = @@ -1044,6 +1321,19 @@ and sugar n (f : Form.t) : string list option = and is_else (f : Form.t) = match f.v with Form.Kw "else" -> true | _ -> false +(* A header and its body: [head = value] when the body is one value that + fits the line, else the block under it, as a [fn]'s. *) +and fn_like n (f : Form.t) head body = + match body with + | [ x ] when (match x.v with + | Form.List (({ v = Form.Sym h; _ } as hf) :: args) -> + not (List.mem h sugar_heads) && body_split hf args = None + | _ -> true) + && String.length head + 3 + String.length (at 0 x) <= width + && not (!inside f) -> + [ head ^ " = " ^ unit_text x ] + | _ -> head :: block (n + 2) body + and handler_clauses n cls = let clause (c : Form.t) = match c.v with @@ -1080,6 +1370,13 @@ and let_lines n prs body = file's own macros are known and no imported package's. *) let program ?source ?macros:m (fs : Form.t list) : string = macros := (match m with Some m -> m | None -> Body_macros.table fs); + classes := + List.filter_map + (fun (f : Form.t) -> + match f.v with + | Form.List [ { v = Form.Sym "defclass"; _ }; { v = Form.Sym c; _ }; _ ] -> Some c + | _ -> None) + fs; spelling := (match source with Some src -> Source_text.spelling src | None -> fun _ -> None); let cs = match source with Some src -> Source_text.comments src | None -> [] in @@ -1089,8 +1386,8 @@ let program ?source ?macros:m (fs : Form.t list) : string = (fun (c : Source_text.comment) -> f.loc.Loc.line <= c.line && c.line < f.loc.Loc.eline) cs); - (* A flat [let] at the top level would take in the forms after it, so one - that is not last goes in a [do:] block. *) + (* A [let] at the top level is a global, so a local one goes in a [do:] + block. *) let top x = Hashtbl.reset used; Hashtbl.reset made; @@ -1099,7 +1396,6 @@ let program ?source ?macros:m (fs : Form.t list) : string = in let rec go = function | [] -> [] - | [ x ] -> [ (x, top x) ] | x :: rest -> (x, top (if let_sugar x then in_do x else x)) :: go rest in let text = @@ -1108,6 +1404,7 @@ let program ?source ?macros:m (fs : Form.t list) : string = in spelling := (fun _ -> None); inside := (fun _ -> false); + classes := []; (* With the source, its comments go back where they were; without it the tags come out and nothing goes in. *) Source_text.weave ~starts:(Source_text.form_starts fs) diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index 4aaa75f0..b703d7f4 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -261,35 +261,82 @@ 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"). *) + 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. *) - let base = if snippet && n > 0 then arr.(0).loc.Loc.col else base in + 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 + 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 - let depth = ref 0 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 then + 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 + t.loc.Loc.col !base end else let p = arr.(i - 1) in - if !depth = 0 && t.loc.Loc.line > p.loc.Loc.eline then begin + (* 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 @@ -324,15 +371,33 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar if not continues then begin first_line := false; let at = point p.loc in - add NEWLINE at; 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 + if col < !base then failk "dedent" t.loc "%s" (if snippet then @@ -342,11 +407,11 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar 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 + col !base !base else Printf.sprintf "this line starts at column %d, left of the top level at \ - column %d" col base); + column %d" col !base); let closed = ref top in let rec pop () = match !stack with @@ -368,12 +433,46 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar 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 -> incr depth - | RP | RB | RC -> if !depth > 0 then decr depth + | 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; @@ -384,11 +483,17 @@ let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token ar (* ── Parsing ───────────────────────────────────────────────────────── *) -type p = { toks : token array; mutable i : int } +(* [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 = @@ -487,6 +592,7 @@ let expect_name p s ~what = (* 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); @@ -637,7 +743,7 @@ and postfix p = loop (mk p l0 (Form.List (f :: args)), 9) | LB -> ignore (advance p); - let idx = items p RB t.loc ~what:"indices" in + let idx = items p RB t.loc ~what:"indices" ~head:(text_of f) in loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 9) | NAME s when String.length s > 1 && s.[0] = '.' -> ignore (advance p); @@ -785,36 +891,83 @@ and inline_stmt p : Form.t = 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 +(* [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 - match (peek p).tok with - | NAME "=" -> + 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 i0 = p.i and t0 = peek p in - let body, _ = expr p in - let body = unit_slot p i0 t0 body 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 ]), + (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)), 9) -(* A lambda with a block written inside a call's brackets, where no block can - open. The fix shown is the typed form, since a lambda bound by [let] has - no call to take its types from; [header] is [fn(a: T) -> R] or the - header as written. *) -and lambda_in_brackets : 'a. Loc.t -> string -> 'a = fun at header -> - failk "lambda-block-in-brackets" at - "a lambda's block cannot go inside brackets, where a line break is only \ - a space. Name it first, with its types and the block under it:\n\n\ - \ let f = %s\n ...\n\n\ - and pass f, or write it on one line: %s = value" +(* 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 [->] @@ -852,7 +1005,8 @@ and lambda_params 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 = +(* [head] is the text of what is indexed, for [[ ]]'s message. *) +and items ?head p closer open_loc ~what = let opener = if closer = RB then '[' else '(' in let rec go acc = let t = peek p in @@ -871,22 +1025,7 @@ and items p closer open_loc ~what = | EOF -> unclosed p opener open_loc | _ -> let n = peek p in - let block_lambda = - match e.v with - | Form.List ({ v = Form.Sym "fn"; _ } :: ps) -> - List.for_all (fun (a : Form.t) -> match a.v with Form.Sym _ -> true | _ -> false) ps - && n.loc.Loc.line > e.loc.Loc.eline - | _ -> false - in - if block_lambda then - let names = - match e.v with - | Form.List (_ :: ps) -> List.map text_of ps - | _ -> [] - in - lambda_in_brackets n.loc - ("fn(" ^ String.concat ", " (List.map (fun x -> x ^ ": T") names) ^ ") -> R") - else if starts_value n.tok && n.sp && not (negative_literal n.tok) + 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. *) @@ -898,8 +1037,11 @@ and items p closer open_loc ~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)" + commas: %s" (show n.tok) (text_of e) what + (match head with + | Some h -> Printf.sprintf "%s[%s, %s]" h (text_of e) (show n.tok) + | None -> "f(a, b)") else stray p ~after:(text_of e)) in go [] @@ -1042,14 +1184,15 @@ let blk (s : st) l (ss : Form.t list) = | (first : Form.t) :: _ -> mk s.p first.loc (Form.List (sym first.loc "do" :: ss)) | [] -> mk s.p l (Form.List [ sym l "do" ]) -let is_lambda_candidate (e : Form.t) = - match e.v with - | Form.List ({ v = Form.Sym "fn"; _ } :: args) -> - List.for_all (fun (a : Form.t) -> match a.v with Form.Sym _ -> true | _ -> false) args - | _ -> false +(* 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 @@ -1066,6 +1209,26 @@ let header_follow p s = 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 "=" + (* [loop x = a, ...]: a name and its [=]. A name and a comma or the end + of the line, or [loop] alone over a block, is a loop missing its first + values, which [header] answers. *) + | "loop" -> + (n.sp && plain_name n.tok + && (match (peek_at p 2).tok with NAME "=" | COMMA | NEWLINE -> true | _ -> false)) + || (n.tok = NEWLINE && (peek_at p 2).tok = INDENT) | "return" -> n.tok = NEWLINE || (n.sp && starts_value n.tok) | "break" | "continue" -> n.tok = NEWLINE || (n.sp && (match n.tok with KW _ -> true | _ -> false)) @@ -1122,11 +1285,32 @@ let params p (lp : token) = in go [] -(* [fn(a: C, b) -> R = body] is [(the (Fn [C dyn] R) (fn [a b] body))]: the +(* [(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. A - block body is added by [lambda_block]. *) + 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 @@ -1136,18 +1320,22 @@ let () = typed_fn_expr := fun p -> | _ -> ([], []) 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" - (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)) + 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 = @@ -1155,26 +1343,21 @@ let () = typed_fn_expr := fun p -> mk p t.loc (Form.List (sym t.loc "fn" :: vec :: body)) ]) in match (peek p).tok with - | NAME "=" -> + | NAME "=>" -> ignore (advance p); - let i0 = p.i and t0 = peek p in - let body, _ = expr p in - (wrap [ unit_slot p i0 t0 body ], 0) - | NEWLINE when (peek_at p 1).tok = INDENT -> (wrap [], 0) + 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 -> - let header = - "fn(" ^ 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) - ^ ") -> " ^ text_of r - in - lambda_in_brackets (peek p).loc header + 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" + "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 @@ -1209,7 +1392,7 @@ and value_line ?(block_ok = false) (s : st) ~after : Form.t = 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) + | NAME (("match" | "handler-case" | "handler-bind" | "restart-case" | "loop") as w) when header_follow p w -> header s w | NAME "if" when header_follow p "if" && not (then_on_line p) -> header s "if" @@ -1249,33 +1432,13 @@ and then_on_line p = 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 - match e.v with - (* A typed lambda waiting for its block, from [typed_fn_expr]. *) - | Form.List [ ({ v = Form.Sym "the"; _ } as th); fty; - ({ v = Form.List [ ({ v = Form.Sym "fn"; _ } as fh); ({ v = Form.Vec _; _ } as vec) ]; _ } as fn_) ] - when (peek p).tok = NEWLINE && (peek_at p 1).tok = INDENT -> - ignore (advance p); - let body = block s ~after in - mk p e.loc (Form.List [ th; fty; { fn_ with v = Form.List (fh :: vec :: body) } ]) - | _ -> - if is_lambda_candidate e && (last p).tok = RP && (peek p).tok = NEWLINE - && (peek_at p 1).tok = INDENT - then begin - ignore (advance p); - let body = block s ~after in - match e.v with - | Form.List (h :: args) -> - mk p e.loc - (Form.List (h :: Form.make (Form.Vec args) (span_of_list e.loc args) :: body)) - | _ -> assert false - end - else begin - if block_ok && (peek p).tok = NEWLINE then ignore (advance p) - else expect_eol p ~after; - e - end + if block_ok && p.i <> p.closed && (peek p).tok = NEWLINE then ignore (advance p) + else expect_eol p ~after; + e and let_stmt (s : st) : Form.t list = let p = s.p in @@ -1331,6 +1494,7 @@ and stmt (s : st) : Form.t = 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 \ @@ -1473,6 +1637,9 @@ and header (s : st) w : Form.t = named (if w = "fn" then "defn" else "defn-") (name :: Form.make (Form.Vec ps) lp.loc :: ret :: (where_clause @ body)) | "def" | "once" | "const" -> + (* 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 @@ -1483,7 +1650,7 @@ and header (s : st) w : Form.t = match (peek p).tok with | NAME "=" -> ignore (advance p); - Some (value_line s ~after:(w ^ " " ^ text_of name ^ " =")) + Some (value_line s ~after:(shown ^ " " ^ text_of name ^ " =")) | _ -> expect_eol p ~after:(match tyf with Some f -> text_of f | None -> text_of name); None @@ -1491,6 +1658,18 @@ and header (s : st) w : Form.t = 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 ] @@ -1505,13 +1684,39 @@ and header (s : st) w : Form.t = failk "def-empty" l0 "%s %s names neither a type nor a value. Give it one or both: %s %s: \ i32 = 0" - w (text_of name) w (text_of name) + shown (text_of name) shown (text_of name) in named head items | "struct" | "union" -> let name = name_tok p ~what:"the type's name" in - expect_eol_block p ~after:(w ^ " " ^ text_of name); + (* [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 = @@ -1522,8 +1727,196 @@ and header (s : st) w : Form.t = 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") - [ name; Form.make (Form.Vec fields) (span p name.loc) ] + (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) + | "loop" -> + let missing () = + failk "loop-bindings" l0 + "loop names each variable with its first value: loop i = 0, acc = 1. \ + A loop with no variables is written loop([]):" + in + if (peek p).tok = NEWLINE then missing (); + let rec binds acc = + let n = name_tok p ~what:"a loop variable's name" in + (match (peek p).tok with + | NAME "=" -> ignore (advance p) + | _ -> + failk "loop-bindings" n.loc + "%s needs its first value: loop %s = 0. Each variable of a loop \ + takes one, separated by commas: loop i = 0, acc = 1" + (text_of n) (text_of n)); + let v, _ = expr p in + match (peek p).tok with + | COMMA -> ignore (advance p); binds (v :: n :: acc) + | _ -> List.rev (v :: n :: acc) + in + let bs = binds [] in + expect_line_end p ~after:(text_of (List.nth bs (List.length bs - 1))); + let body = block s ~after:"loop" in + form (Form.make (Form.Vec bs) (span_of_list (List.hd bs).loc bs) :: body) | "data" -> let name = name_tok p ~what:"the type's name" in expect_eol_block p ~after:("data " ^ text_of name); @@ -1578,7 +1971,7 @@ and header (s : st) w : Form.t = let clauses ~oneline body = let rec elifs acc = match (peek p).tok with - | NAME "elif" -> + | NAME "elif" when not (assigns p) -> ignore (advance p); let c, _ = binary p 1 in (match (peek p).tok with @@ -1600,7 +1993,7 @@ and header (s : st) w : Form.t = let els_ = elifs [] in let else_ = match (peek p).tok with - | NAME "else" -> + | 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") @@ -1748,7 +2141,7 @@ and header (s : st) w : Form.t = 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 -> + | NAME "on", n when n.sp && not (assigns p) -> let ot = advance p in let head, _ = postfix p in let ty, var = @@ -1777,7 +2170,7 @@ and header (s : st) w : Form.t = 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 -> + | 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 @@ -1837,6 +2230,7 @@ and clause_end p 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 @@ -1861,9 +2255,11 @@ and lines (s : st) (one : unit -> Form.t list) : Form.t list = go [] end +let () = block_of := fun p -> block { p; lets = [] } ~after:"=>" + (** 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 ~file src = +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 @@ -1874,8 +2270,20 @@ let read_all ?(line = 1) ?col ?indent ~file src = (String.make (line - 1) '\n' ^ String.make (col - 1) ' ' ^ src))); Fun.protect ~finally:(fun () -> source := saved) (fun () -> let toks = layout ~snippet ~base:col ?indent (lex ~line ~col ~file src) in - let s = { p = { toks; i = 0 }; lets = [] } in - let fs = stmts s 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 f = header s "def" in + f :: 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)); diff --git a/lib/source.ml b/lib/source.ml index 48966744..ba670faf 100644 --- a/lib/source.ml +++ b/lib/source.ml @@ -72,9 +72,11 @@ let read_paren ?(line = 1) ?(col = 1) ~file src = in go [] -(** Editor code, in the request's syntax and at its position. With [expr], an - indented snippet of several statements is one expression, [(do ...)]: a - block of lines means its lines in order. *) +(** Editor code, in the request's syntax and at its position. With [expr], + an indented snippet of several statements is one expression, + [(do ...)]: a block of lines means its lines in order. A [let] is a + global only in code from column 1 that is not an expression: a form cut + from inside a body, for a macroexpansion say, keeps its lets local. *) let read_code ?(expr = false) ~file code = let line, col = match !code_at with Some (l, c) -> (l, c) | None -> (1, 1) @@ -82,7 +84,8 @@ let read_code ?(expr = false) ~file code = match !code_syntax with | Paren -> read_paren ~line ~col ~file code | Indented -> - (match Indent_reader.read_all ~line ~col ?indent:!code_indent ~file code with + (match Indent_reader.read_all ~line ~col ?indent:!code_indent ~global_let:(not expr && col = 1) + ~file code with | (first :: _ :: _ as forms) when expr -> let last = List.nth forms (List.length forms - 1) in let loc = diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index ace75dbc..250f8f37 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -1610,15 +1610,10 @@ flan_dyn flan_dyn_map_new(void) { * * **What the registry constrains.** A store into a slot the class declares * — the constructor's, [put]'s, [set]'s — is checked against the slot's - * type. A key the class does not declare is not refused by [put]: a class - * instance is an open map — TODO.org, "Class features deferred, each with its - * reason", defers unknown-slot checking — so a key nobody declared can be - * written to one, and the migration below will *drop* it at the next - * redefinition, because its rule is that an instance's keys are the class's - * slots. That is real data loss and it is written down as such in TODO.org, - * "A redefined defclass migrates its instances lazily", rather than dressed - * up as enforcement. [set] does refuse an undeclared key, because a slot it - * writes has to exist. + * type. A key the class does not declare is refused by [get], [put] and + * [set] alike ([trap_no_slot]), so an instance's keys are its class's slots + * and the migration below, which keeps only those, drops nothing a program + * wrote. * * **Where a migration happens.** [want_map], so every [get], [put] and * [has-key?]; [flan_dyn_len]'s map arm; and [dyn_equal]'s, so two instances @@ -1963,6 +1958,10 @@ flan_dyn flan_dyn_vec_new(void); flan_dyn flan_dyn_map_new(void); void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen); void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v); +flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc, + int64_t loclen); +void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc, + int64_t loclen); /* The user hook, run on an instance the name-matching has just brought up to * date. [inst], [added] and [gone] are rooted by the caller. @@ -3037,8 +3036,12 @@ flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i, const uint8_t *loc, int64_t loclen) { int64_t k; flan_obj *o; + /* m[:k] on a map is (get m :k), whatever the key: get's rule, nil when + * absent. */ + if (is_map(v)) return flan_dyn_get(v, i, loc, loclen); if (!is_text(v) && !is_vec(v)) - trap2(loc, loclen, TYPE_TRAP, "at", "only a text or a vec is indexed", v, i); + trap2(loc, loclen, TYPE_TRAP, "at", "only a text, a vec or a map is indexed", + v, i); k = need_index(loc, loclen, "at", v, i); o = dyn_obj(v); if (o->kind == OBJ_VIEW) { @@ -3086,8 +3089,14 @@ void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x, const uint8_t *loc, if (is_text(v)) trap2(loc, loclen, TYPE_TRAP, "set-at", "a text is immutable — build another one", v, i); + /* Assigning m[:k] is (put m :k x), a class slot's type check with it. */ + if (is_map(v)) { + flan_dyn_map_put(v, i, x, loc, loclen); + return; + } if (!is_vec(v)) - trap2(loc, loclen, TYPE_TRAP, "set-at", "only a vec is assigned into", v, i); + trap2(loc, loclen, TYPE_TRAP, "set-at", + "only a vec or a map is assigned into", v, i); k = need_index(loc, loclen, "set-at", v, i); o = dyn_obj(v); if (o->kind == OBJ_VIEW) { @@ -3191,6 +3200,24 @@ flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k) { return i < 0 ? flan_dyn_nil() : o->u.v.items[i * 2 + 1]; } +/* A program's (get m k) and (.k m): the same, with the site a value that is + * not a map is refused at, and a key an instance's class does not declare + * refused rather than answered nil — [trap_no_slot]. */ +static class_entry *class_sync(flan_obj *o); +static int64_t class_slot(class_entry *e, flan_dyn k); +static _Noreturn void trap_no_slot(const uint8_t *loc, int64_t loclen, + const char *op, flan_obj *o, + class_entry *e, flan_dyn k); +flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc, + int64_t loclen) { + class_entry *e; + if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "get", "only a map answers it", m, k); + e = class_sync(dyn_obj(m)); + if (e != NULL && class_slot(e, k) < 0) + trap_no_slot(loc, loclen, "get", dyn_obj(m), e, k); + return flan_dyn_map_get(m, k); +} + flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k) { flan_obj *o = want_map("has-key?", m, k); return flan_dyn_from_bool(map_find(o, k) >= 0); @@ -3272,6 +3299,29 @@ static flan_dyn check_slot(const uint8_t *loc, int64_t loclen, int by, static inline void map_store(flan_obj *o, flan_dyn k, flan_dyn v); +/* A key an instance's class does not declare, read or written. An instance + * has exactly its class's slots — a typo in a slot name is an error at the + * access and not a new key — so get, put and set all refuse one; a plain map + * takes any key. */ +static _Noreturn void trap_no_slot(const uint8_t *loc, int64_t loclen, + const char *op, flan_obj *o, + class_entry *e, flan_dyn k) { + char sk[SAY_MAX]; + kw_entry *c = o->u.v.klass; + int64_t i; + say(sk, SAY_MAX, k); + said_len = 0; + said_add("dyn %s: %.*s has no slot %s. Its slots are", op, (int)c->len, + (const char *)(c + 1), sk); + if (e == NULL || e->nslots == 0) said_add(" none"); + else + for (i = 0; i < e->nslots; i++) + said_add(" :%.*s", (int)e->slots[i]->len, + (const char *)(e->slots[i] + 1)); + flan_say(loc, loclen, "%s", said_buf); + flan_trap((const uint8_t *)"DynType", 7); +} + /* A constructor's stores: [flan_dyn_map_set]'s, with the refusal worded for * the constructor call it happened inside rather than for a [put] nobody * wrote, and placed at the slot's declaration. */ @@ -3286,8 +3336,7 @@ void flan_dyn_slot_init(flan_dyn m, flan_dyn k, flan_dyn v, * they are three different mistakes: the value is not a class instance at * all (a map's entries are written with [put], which is where inserting a * key is real); the key is not a slot the class declares; the value does not - * fit the slot's type. The first two are why this is not [put]: a declared - * slot always exists, so writing one is a store and never an insertion. */ + * fit the slot's type. The first is why this is not [put]. */ void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc, int64_t loclen) { flan_obj *o; @@ -3307,43 +3356,37 @@ void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v, o = dyn_obj(m); e = class_sync(o); j = class_slot(e, k); - if (j < 0) { - char sk[SAY_MAX]; - kw_entry *c = o->u.v.klass; - int64_t i; - say(sk, SAY_MAX, k); - said_len = 0; - said_add("dyn set: %.*s has no slot %s. Its slots are", - (int)c->len, (const char *)(c + 1), sk); - if (e == NULL || e->nslots == 0) said_add(" none"); - else - for (i = 0; i < e->nslots; i++) - said_add(" :%.*s", (int)e->slots[i]->len, - (const char *)(e->slots[i] + 1)); - said_add("; a key the class does not declare is added with put, not set"); - flan_say(loc, loclen, "%s", said_buf); - flan_trap((const uint8_t *)"DynType", 7); - } + if (j < 0) trap_no_slot(loc, loclen, "set", o, e, k); if (!slot_admit(&e->types[j], v, &out)) trap_slot_type(loc, loclen, BY_SET, o, e, j, m, v); map_store(o, k, out); } -void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc, - int64_t loclen) { +static void map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc, + int64_t loclen, int any_key) { flan_obj *o; class_entry *e; - if (!is_map(m)) trap2(NULL, 0, TYPE_TRAP, "put", "only a map answers it", m, k); + if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "put", "only a map answers it", m, k); o = dyn_obj(m); e = class_sync(o); + if (!any_key && e != NULL && class_slot(e, k) < 0) + trap_no_slot(loc, loclen, "put", o, e, k); /* A map with no class, and a class with no typed slot, stop at the test. */ if (e != NULL && e->typed) v = check_slot(loc, loclen, BY_PUT, o, e, m, k, v); map_store(o, k, v); } -/* The same with no site: a map literal's stores, and test/dyn_ops.c. */ +/* A program's put, and the store under a dyn's [.k] and [:k]. */ +void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc, + int64_t loclen) { + map_put(m, k, v, loc, loclen, 0); +} + +/* With no site and any key: an untagged map literal's stores, and + * test/dyn_ops.c, which builds instances' odd states by hand. No program + * reaches an instance through it. */ void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v) { - flan_dyn_map_put(m, k, v, NULL, 0); + map_put(m, k, v, NULL, 0, 1); } /* The store under all three, with the instance already brought up to date. */ diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 39d6173e..2a5b1bd5 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -156,10 +156,8 @@ flan_dyn flan_dyn_type_of(flan_dyn v); * declares are dropped. The instance's identity is preserved throughout; * this is CLHS 4.3.6, and [flan_dyn_class_hook] is its user hook. * - * The drop is unconditional, which is the honest cost of a class instance - * being an open map: a key written by a raw [put] that the class never - * declared is dropped by the next migration too. The registry describes the - * class's intention and does not enforce it. */ + * No key the class never declared can be there to drop: [get], [put] and + * [set] refuse one on an instance. */ void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n); /* The body update-instance-for-redefined-class dispatches through, as a @@ -244,6 +242,9 @@ void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen); * to ask when nil might also be stored. [set] replaces the value of an equal * key in place, so a key occurs once and insertion order is print order. */ flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k); +/* [get]'s, and a dyn's [.field]: [flan_dyn_map_get] with a site. */ +flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc, + int64_t loclen); void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v); /* [put]'s: [flan_dyn_map_set], with the site a typed class slot's refusal * prints. */ diff --git a/spec-syntax.md b/spec-syntax.md index 0416c89a..bda24f0a 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -117,10 +117,10 @@ Each item: the proposal, then the reason in one line. trailing block is allowed. Make it parser-driven, the way GDScript's `push_multiline` is (`gdscript_parser.cpp` 658-672, 3695-3770), not a paren counter in the lexer, or a block inside a call can't work. - *Built as a depth counter instead: inside brackets a line break is always - whitespace, so no block opens inside a call's parentheses (§3.1's blocks all - open after the `)`; a lambda with a block body is a statement or a value, - `let f = fn(x)` plus a block).* + *Built as a depth counter instead: inside brackets a line break is + whitespace, with one exception. A `=>` that ends a line inside brackets opens + a lambda's block there, laid out as at the top level against the column its + line starts at, and the block ends where the enclosing bracket closes.* - **Continuation outside brackets:** a line that starts with a spaced infix operator (`+`, `and`, `==`, …) continues the previous line; so does a line after one that ends in a spaced infix operator. (F# `LexFilter.fs` 360-380, @@ -172,6 +172,10 @@ Each item: the proposal, then the reason in one line. one symbol. `test/programs/dev-rerun.flan:65` names a global `.init-once.counter`; rename it. **Built**, without the rename: it prints and reads back through the fallback, `defonce(.init-once.counter, i64, 7)`. +- **On a dyn value, `x.name` is `(get x :name)` and `x.name = v` is + `(put x :name v)`**, for a class slot and a plain map's key alike; `m[:k]` + is `(get m :k)` and `m[:k] = v` puts. The paren spellings `(.name x)` and + `(at m :k)` mean the same. **Built.** - **`and`, `or`, `not` are words**, since they are Flan's own names. **Built.** - **Casts and type-taking builtins are calls:** `i32(x)`, `vec-new(u8)`, `max-value(u8)`, `the([3 f32], [1 2 3.5])`. A pointer cast is the type @@ -267,11 +271,28 @@ Each item: the proposal, then the reason in one line. - **Unit:** `()` as a statement reads `(do)`; in a type it is `()`. **Built**; inside an expression `()` stays `()`, and the printer writes a lone `()` statement as `(())`. A bare `()` in a one-line body slot (`fn f() -> () = ()`, - `_ -> ()`, `fn() = ()`, `then ()`) is a statement too, and reads `(do)`. -- **Lambda:** `fn(i, j) = i * 10 + j`, or `fn(i, j)` plus a block. **Built**; + `_ -> ()`, `fn() => ()`, `then ()`) is a statement too, and reads `(do)`. +- **Lambda:** `fn(i, j) => i * 10 + j`, or `fn(i, j) =>` plus a block. **Built**; its parameters are bare names, as `(fn [i j] …)` wants, with no `dyn`. `fn(…)` followed by anything else is the fallback call. A lambda may state - its types, `fn(a: C, b) -> bool = …` or plus a block (section 3, item 7). + its types, `fn(a: C, b) -> bool => …` or plus a block (section 3, item 7). + `=>` is a lambda's only spelling: `fn(a) = x` and a lambda header with a + block under it and no `=>` are refused, with the `=>` form as the fix. + Named functions keep `=`. A block lambda may sit inside brackets: + + ``` + sort-by(slice(xs), fn(a, b) => + let d = a.n - b.n + d < 0) + ``` + + The block ends where the brackets close, with the `)` at the end of its + last line or on a line of its own at the call's column. It is the last + thing in them: a comma after the block is refused (so a call takes one + block lambda, as its last argument; name any other with `let`), as is a + line inside the brackets at or left of the column the `=>` line starts at. + Block lambdas nest, each block ending at its own brackets. `flan convert` + writes a call whose last argument is a lambda with a block this way. ### Definitions @@ -280,20 +301,45 @@ Each item: the proposal, then the reason in one line. becomes `where ordered?($t)` after the return type. **Built**; with no `-> R` the return is `_`, read off the body. Several predicates are `where p, q`. -- `def x = v`, `def x: T = v`, `once x: T`, `once x = v`, `const n = 3`, - `def scratch: [4 u8] = uninit`. **Built.** `def x = v` and `once x = v` read - with `dyn`; `const n = 3` reads `(defconst n 3)`, its type inferred as today. +- A `let` at the top level is a global: `let x = v`, `let x: T = v`, + `let scratch: [4 u8] = uninit` read `(def x dyn v)`, `(def x T v)`; + `once x: T`, `once x = v`, `const n = 3`. **Built.** `let x = v` and + `once x = v` read with `dyn`; `const n = 3` reads `(defconst n 3)`, its type + inferred as today. `def` is refused with the `let` to write. A `let` in a + block, `comment:`'s included, is local, and so is one in code the editor + evaluates as an expression. - `struct Cell` with a `name: Type` line per field. `data Shape` with a line per case: `Circle(r: f32)`, `Empty`. `enum K` with `lo = -1`, `mid`. `union U` like `struct`. **Built** (an untyped field is `dyn`; `Empty()` is `(Empty [])`). A member is `:mid` or `K.mid`, in a value and in a match arm, in both - syntaxes (section 3, item 8). + syntaxes (section 3, item 8). A condition names its parent after the name, + `struct DiskFull :parent IoError` with its field lines, reading + `(defstruct DiskFull :parent IoError [free i64])`; with no field lines it + reads `(defstruct IoError :parent Error)`. A struct or union fits on one + line with its fields in parentheses, `struct Pt(x: i32, y: i32)` or + `struct DiskFull(free: i64) :parent IoError`; `flan convert` writes that + when it fits the line and no comment sits among the fields. **Built.** +- `type Row = Vec(i32)` reads `(defalias Row (Vec i32))`. **Built.** +- `macro repeat(i, n, & body)` plus a block reads + `(defmacro repeat [i n & body] …)`. A parameter is a bare name, a + destructuring vector `[a b]`, or `& rest`, last. **Built.** +- `loop x = a, y = b` plus a block reads `(loop [x a y b] …)`, as a statement + or as a value, `let r = loop i = 0`. `recur(y, x % y)` is a call. A loop with + no variables is the fallback, `loop([]):`. **Built.** +- `class lambda(param, body, env)`, or `class lambda` with a slot per line, + reads `(defclass lambda [param body env])`; a typed slot is `pause: bool` + and its type follows its name in the vector. **Built.** +- `generic describe(v) -> dyn` reads `(defgeneric describe [v] dyn)`; + `multi kind(v) -> dyn = type-of(v)`, or plus a block, reads + `(defmulti kind [v] dyn (type-of v))`. Their parameters are bare names. + **Built.** +- `method describe(f: lambda)` plus a block reads + `(defmethod describe lambda [f] …)`: a class is the first parameter's type. + Any other dispatch value follows `when`: `method kind(v) when :int`, + `when :else` for the default. `= value` for a one-line body. **Built.** - `import rl "vendor:raylib"`. **Built.** - **Every other form uses the fallback** (next item) until someone asks for - sugar: `defclass`, `defgeneric`, `defmulti`, `defmethod`, `declare`, - `declare-c`, `defalias`, `defmacro`, `loop`/`recur`, `array-fill`. **Built.** - The class forms keep the fallback for good (2026-09-26): - `defmethod(area, point, [p]):` reads well enough. + sugar: `declare`, `declare-c`, `array-fill`. **Built.** ### The fallback @@ -317,7 +363,7 @@ value, `vec-new(Fn([i32], i32))` is the call spelling). ### Macro templates ``` -defmacro(with-mode-2d, [camera & body]): +macro with-mode-2d(camera, & body) quote begin-mode-2d(~camera) ~@body @@ -376,16 +422,15 @@ Settled 2026-09-26, after writing programs by hand (`test/syntax/handwritten/`): is refused at its last line: the first `else` took `if b then y` as its value, and the chain is `if a then x else if b then y else z` on one line, or `elif b then y` on the second. -7. **Typed lambdas.** `fn(a: C, b) -> R = body`, or plus a block, reads +7. **Typed lambdas.** `fn(a: C, b) -> R => body`, or plus a block, reads `(the (Fn [C dyn] R) (fn [a b] body))`: the paren `fn` has no typed parameters, and `the` is how a value states its type, as in `let x: T = v`. An untyped parameter is `dyn`; the return type is required. Where a `CFn` of the same signature is wanted, the literal is that `CFn`; at a generic's `CFn($t) -> $t` parameter the literal is a `CFn` at its own types, which bind `$t` as any argument's would. The - printer writes that form back as the typed lambda. A block lambda cannot - sit inside a call's brackets; the refusal shows the typed `let` form to - bind it with. + printer writes that form back as the typed lambda. A typed block lambda + sits inside brackets as an untyped one does. 8. **`Dir.north` is the enum member `:north`**, in a value and in a match pattern, in both syntaxes. `:north` stays. A local named `Dir` shadows the enum as a local shadows any global: `Dir.north` is then its field. @@ -433,9 +478,11 @@ Each step lands on its own, with `dune test --root .` green. space-padding in `flan--text-at` (`emacs/flan.el:2602-2622`), which breaks significant indentation, with `:line`/`:col` fields; the reader seeds its indent stack with that column. **Built** (also `load-file` and restart - arguments; no `:syntax` means paren, except a `load-file` of a `.fln` - file; several indented statements sent as one expression read as - `(do …)`). + arguments; with no `:syntax` a request is read in the syntax of the + source `:file` it names, as paren under a pseudo-name such as ``, + and with no `:file` at all in the program's — so + evaluating in a stopped frame of a `.fln` program reads indented; + several indented statements sent as one expression read as `(do …)`). 5. **Emacs mode** for `.fln`: - A top-level form runs from a column-0 line that isn't `else`, `elif`, `on` or `restart` to just before the next one, minus trailing blank and @@ -453,7 +500,7 @@ Each step lands on its own, with `dune test --root .` green. (`ast.ml:491-492`). **Built** (`emacs/flan-fln-mode.el`; keys and objects in `emacs/MANUAL.md`, - "Indented files"). A line ending in `=` or `fn(…)` also opens a block for + "Indented files"). A line ending in `=` or `=>` also opens a block for TAB, and a body is its statement's own block, up to its first clause. 6. **Return-type inference** in `Check`, with the recursion refusal and the stale-caller cause. This is independent of steps 1-5 once the marker exists. diff --git a/test/programs/dev-fln-dyn.fln b/test/programs/dev-fln-dyn.fln new file mode 100644 index 00000000..aa66939b --- /dev/null +++ b/test/programs/dev-fln-dyn.fln @@ -0,0 +1,26 @@ +;; A dyn class instance in a .fln program, for code evaluated from its buffer: +;; eval-expr reads the request's syntax, at a stop as well as at a park. +import agent "vendor:agent" + +defclass(State, [paused bool step bool]) + +once state = State(false, false) +once go = false + +struct Missing(id: i32) + +fn boom() -> i64 + restart-case + error(Missing{.id 1}) + 0 + restart carry-on() + -1 + +fn main() -> i32 + agent/start("/tmp/flan-dev-fln-dyn-fallback.sock") + for i in range(4000) + agent/wait(5) + if go + go = false + boom() + 0 diff --git a/test/programs/dyn-class-slots.flan b/test/programs/dyn-class-slots.flan index 662d1ade..d5bb62d3 100644 --- a/test/programs/dyn-class-slots.flan +++ b/test/programs/dyn-class-slots.flan @@ -28,12 +28,9 @@ (println (get s :step)) (set (get s :tag) [1 2]) (println (get s :tag)) - ;; put reaches the same check for a declared slot, and still inserts a - ;; key the class does not declare -- an instance is an open map to put. + ;; put reaches the same check for a declared slot. (put s :speed 2.5) - (put s :scratch 9) (println (get s :speed)) - (println (get s :scratch)) (println (length s)) ;; A typed caller boxes into the dyn parameter as any call does. (set (get s :step) (twelve)) diff --git a/test/programs/dyn-class.flan b/test/programs/dyn-class.flan index 87f9eeb4..9a5991d1 100644 --- a/test/programs/dyn-class.flan +++ b/test/programs/dyn-class.flan @@ -64,7 +64,9 @@ (println (get p :x)) (println (has-key? p :x)) (println (has-key? p :nothing)) - (println (get p :nothing)) + ;; A key its class does not declare is refused on an instance; a plain + ;; map answers nil for one it lacks. + (println (get {:x 1} :nothing)) ;; The shape tag, as a value. Every value can be asked; only an instance ;; answers with a name. diff --git a/test/programs/dyn-field-trap.flan b/test/programs/dyn-field-trap.flan new file mode 100644 index 00000000..9ae1150e --- /dev/null +++ b/test/programs/dyn-field-trap.flan @@ -0,0 +1,20 @@ +;;;; A dyn's .field and [:key] trap with get's and put's own sentences, one +;;;; per run. The argument chooses which; the test asserts the line numbers. +(defclass State [paused bool step bool]) + +(defn as-dyn [d dyn] dyn d) + +(defn main [args [str]] i32 + (let [which (if (> (length args) 1) (i32 (bytes->i64 (bytes-view (at args 1)))) 0) + s (State false false) + n (as-dyn 3)] + (println "before") + (cond + (= which 0) (println (.paused n)) + (= which 1) (set (.paused s) 1) + (= which 2) (set (.paused n) true) + (= which 3) (set (at s :step) 2) + (= which 5) (set (.pasued s) true) + (= which 6) (println (.pasued s)) + :else (println (at n :paused)))) + 0) diff --git a/test/programs/dyn-field-trap.fln b/test/programs/dyn-field-trap.fln new file mode 100644 index 00000000..bed59b9f --- /dev/null +++ b/test/programs/dyn-field-trap.fln @@ -0,0 +1,27 @@ +;;;; A dyn's .field and [:key] trap with get's and put's own sentences, one +;;;; per run. The argument chooses which; the test asserts the line numbers. +defclass(State, [paused bool step bool]) + +fn as-dyn(d) -> dyn = d + +fn main(args: [str]) -> i32 + let which = + if length(args) > 1 then i32(bytes->i64(bytes-view(args[1]))) else 0 + let s = State(false, false) + let n = as-dyn(3) + println("before") + if which == 0 + println(n.paused) + elif which == 1 + s.paused = 1 + elif which == 2 + n.paused = true + elif which == 3 + s[:step] = 2 + elif which == 5 + s.pasued = true + elif which == 6 + println(s.pasued) + else + println(n[:paused]) + 0 diff --git a/test/programs/dyn-fields.flan b/test/programs/dyn-fields.flan new file mode 100644 index 00000000..8f48a9b0 --- /dev/null +++ b/test/programs/dyn-fields.flan @@ -0,0 +1,36 @@ +;;;; A dyn value's (.field x) and (at m :key): a read is get, a set is put. +(defclass State [paused bool step bool]) +(defclass Pos [x y]) +(defclass Body [pos count items]) + +(defonce state (State false false)) + +(defn game-input [] () + (when true + (set (.paused state) (not (.paused state))))) + +(defn pick [b dyn] dyn b) + +(defn main [] i32 + (game-input) + (println (.paused state)) + (game-input) + (println (.paused state)) + (let [b (Body (Pos 1 2) 0 [10 20 30])] + (set (.count b) (+ (.count b) 1)) + (++ (.count b)) + (update (.count (pick b)) + 10) + (set (.x (.pos b)) 7) + (update (.y (.pos b)) * 10) + (set (at (.items b) 0) 11) + (update (at (.items b) 1) + 5) + (println (.count b) (.x (.pos b)) (.y (.pos b)) (at (.items b) 0) (at (.items b) 1)) + (println (.missing {:a 1}) (= (.missing {:a 1}) (get {:a 1} :missing)))) + (let [m {:hp 3}] + (set (.hp m) (- (.hp m) 1)) + (set (at m :mp) 9) + (update (at m :mp) + 1) + (set (.name m) "slime") + (set (at m 1) :one) + (println (at m :hp) (.mp m) (at m :gone) (at m 1) m)) + 0) diff --git a/test/syntax/handwritten/csv.fln b/test/syntax/handwritten/csv.fln index 7aa9f652..e5de89b4 100644 --- a/test/syntax/handwritten/csv.fln +++ b/test/syntax/handwritten/csv.fln @@ -8,11 +8,11 @@ enum State quote-in-quoted once rows-seen: i32 -def fields-seen: i32 = 0 +let fields-seen: i32 = 0 const separator = \, ; Frame the body's output with a title line and a closing rule. -defmacro(with-section, [title & body]): +macro with-section(title, & body) quote println("--", ~title, "--") ~@body diff --git a/test/syntax/handwritten/dynfields.fln b/test/syntax/handwritten/dynfields.fln new file mode 100644 index 00000000..e36d11b8 --- /dev/null +++ b/test/syntax/handwritten/dynfields.fln @@ -0,0 +1,34 @@ +;; A dyn value's .field and [:key]: a read is get, an assignment is put. + +defclass(State, [paused bool step bool]) +defclass(Pos, [x y]) +defclass(Body, [pos count items]) + +once state = State(false, false) + +fn game-input() -> () + if true + state.paused = not(state.paused) + +fn main() -> i32 + game-input() + println(state.paused) + game-input() + println(state.paused) + let b = Body(Pos(1, 2), 0, [10 20 30]) + b.count += 1 + b.count += 1 + b.pos.x = 7 + b.pos.y *= 10 + b.items[0] = 11 + b.items[1] += 5 + println(b.count, b.pos.x, b.pos.y, b.items[0], b.items[1]) + let plain = {:a 1} + println(plain.missing) + let m = {:hp 3} + m.hp -= 1 + m[:mp] = 9 + m[:mp] += 1 + m.name = "slime" + println(m[:hp], m.mp, m[:gone], m) + 0 diff --git a/test/syntax/handwritten/dynfields.out b/test/syntax/handwritten/dynfields.out new file mode 100644 index 00000000..780925d7 --- /dev/null +++ b/test/syntax/handwritten/dynfields.out @@ -0,0 +1,5 @@ +true +false +2 7 20 11 25 +nil +2 10 nil {:hp 2 :mp 10 :name "slime"} diff --git a/test/syntax/handwritten/inventory.fln b/test/syntax/handwritten/inventory.fln index 94bcd51c..79d7b494 100644 --- a/test/syntax/handwritten/inventory.fln +++ b/test/syntax/handwritten/inventory.fln @@ -8,24 +8,24 @@ struct Rule label: str applies: CFn(stock/Item) -> bool -def report-width: i32 = 28 +let report-width: i32 = 28 once runs: i32 const reorder-below = 5 ; Run the body n times, counting passes in the name given. -defmacro(repeat, [i n & body]): +macro repeat(i, n, & body) quote for ~i in range(~n) ~@body ; Say what went wrong when a check does not hold. -defmacro(expect, [test message]): +macro expect(test, message) quote if not ~test println("expected:", ~message) fn gcd(a: i32, b: i32) -> i32 - loop([x a y b]): + loop x = a, y = b if y == 0 then x else recur(y, x % y) fn line(it: stock/Item) -> () @@ -51,7 +51,7 @@ fn main() -> i32 let items = [stock/item("bolts", 12, 400), stock/item("nuts", 5, 3), stock/item("gears", 1250, 7), stock/item("belts", 899, 0)] let rules = [Rule{.label "reorder", .applies low?}, - Rule{.label "valuable", .applies fn(it) = stock/value(it) > 5000}] + Rule{.label "valuable", .applies fn(it) => stock/value(it) > 5000}] let frame = arena-new(4096) repeat(pass, 2): runs += 1 @@ -75,7 +75,7 @@ fn main() -> i32 println("sign bits", stock/sign-bit(-2.5), stock/sign-bit(2.5), "masked", bit-and(-total, 0xFF)) let big = 1000 - println("worth over", big, count-if(slice(items), fn(it) = stock/value(it) > big)) + println("worth over", big, count-if(slice(items), fn(it) => stock/value(it) > big)) expect(total == 407, "407 on even rows") expect(runs == 2, "two runs") 0 diff --git a/test/syntax/handwritten/ledger.fln b/test/syntax/handwritten/ledger.fln index bea43893..02ba52ef 100644 --- a/test/syntax/handwritten/ledger.fln +++ b/test/syntax/handwritten/ledger.fln @@ -2,11 +2,11 @@ ; overdraw signals, and the caller picks a restart: skip it, cap it at what ; the account holds, or allow an overdraft up to a limit it supplies. -defstruct(Overdraft, :parent, Error, [account i32 short i64]) - -struct Audit +struct Overdraft :parent Error account: i32 - amount: i64 + short: i64 + +struct Audit(account: i32, amount: i64) once balances: [4 i64] once audits: i32 diff --git a/test/syntax/handwritten/lisp.fln b/test/syntax/handwritten/lisp.fln index 9b7c8743..6e4ab048 100644 --- a/test/syntax/handwritten/lisp.fln +++ b/test/syntax/handwritten/lisp.fln @@ -2,22 +2,21 @@ ; first element is a keyword naming the operation, variables are keywords, ; and environments are dyn maps chained through a :parent key. -defclass(lambda, [param body env]) +class lambda(param, body, env) -defgeneric(describe, [v], dyn) +generic describe(v) -> dyn -defmethod(describe, lambda, [f]): +method describe(f: lambda) "a function of one argument" -defmulti(kind, [v], dyn, type-of(v)) +multi kind(v) -> dyn = type-of(v) -defmethod(kind, :int, [v]): +method kind(v) when :int "number" -defmethod(kind, :vec, [v]): - "form" +method kind(v) when :vec = "form" -defmethod(kind, :else, [v]): +method kind(v) when :else "value" once steps = 0 diff --git a/test/syntax/handwritten/ring.fln b/test/syntax/handwritten/ring.fln index bc80797c..bc6c7260 100644 --- a/test/syntax/handwritten/ring.fln +++ b/test/syntax/handwritten/ring.fln @@ -60,7 +60,7 @@ fn repeat-apply(f: CFn($t) -> $t, x: $t, n: i32) -> $t v = f(v) v -fn halve-all(x: $t) -> $t where numeric?($t) = repeat-apply(fn(a: $t) -> $t = a / 2, x, 3) +fn halve-all(x: $t) -> $t where numeric?($t) = repeat-apply(fn(a: $t) -> $t => a / 2, x, 3) fn main() -> i32 let r: Ring(5, i32) = zeroed() @@ -80,16 +80,16 @@ fn main() -> i32 let d = distinct(slice(samples)) println("distinct", slice(d)) free(d) - let big = filter(slice(samples), fn(x) = x >= 7) - println("seven and up", slice(big), "sum", reduce(slice(big), 0, fn(a, b) = a + b)) + let big = filter(slice(samples), fn(x) => x >= 7) + println("seven and up", slice(big), "sum", reduce(slice(big), 0, fn(a, b) => a + b)) free(big) let rs = [Reading{.sensor 2, .value 40} Reading{.sensor 1, .value 15} Reading{.sensor 3, .value 22}] - sort-by(slice(rs), fn(a, b) = a.value < b.value) + sort-by(slice(rs), fn(a, b) => a.value < b.value) for i in range(length(rs)) println("sensor", rs[i].sensor, rs[i].value) let raw: [4 u32] = [1 2 3 4] let p = Ptr(u8)(addr(raw[0])) println("checksum", checksum(p, 16)) - println("doubled", repeat-apply(fn(a: i32) -> i32 = a * 2, 1, 10), "halved", halve-all(800.0)) + println("doubled", repeat-apply(fn(a: i32) -> i32 => a * 2, 1, 10), "halved", halve-all(800.0)) 0 diff --git a/test/syntax/handwritten/words.fln b/test/syntax/handwritten/words.fln index fc4cb2f4..90da8830 100644 --- a/test/syntax/handwritten/words.fln +++ b/test/syntax/handwritten/words.fln @@ -58,7 +58,7 @@ fn main() -> i32 let text = "The cat saw the dog. The dog didn't see the cat, but the bird saw both!" let ws = words(bytes-view(text)) let counts = tally(slice(ws)) - let by-count = fn(a: Count, b: Count) -> bool + let by-count = fn(a: Count, b: Count) -> bool => if a.n != b.n return a.n > b.n bytes i32 let distinct = i32(length(counts)) let g = grade(distinct, total) println(distinct, "of", total, "distinct:", describe(g)) - let longest = reduce(slice(ws), slice(ws[0], 0, 0), fn(a, b) = + let longest = reduce(slice(ws), slice(ws[0], 0, 0), fn(a, b) => if length(b) > length(a) then b else a) println("longest", str(longest)) let short = 0 < length(longest) < 5 diff --git a/test/syntax/sand.fln b/test/syntax/sand.fln index 00c5bd9c..6a63d6fe 100644 --- a/test/syntax/sand.fln +++ b/test/syntax/sand.fln @@ -15,8 +15,8 @@ const brush-size = 10 fn dyn->f64(v: f64) -> f64 = v fn dyn->u32(v: i64) -> u32 = u32(v) -def gravity = 0.05 -def colors = +let gravity = 0.05 +let colors = let v = vec-new(dyn) push(v, 0xFFF00FFF) push(v, 0x3B6E8CFF) @@ -121,7 +121,7 @@ fn game-draw() -> () rl/draw-fps(20, 20) once frame: Allocator = arena-new(262144) -def game-data = +let game-data = handler-case edn/read-file("game-data.edn") on FileError(c) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 9fc012c8..a0d42819 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -5660,13 +5660,12 @@ level "1" (* Typed class slots and set on a slot: the stores that fit, then one run per refusal. The constructor, put and set each check a declared - slot's type, set refuses a slot the class does not declare and a - value that is not an instance, and put still inserts an undeclared - key. On both backends, because every one of these is a runtime call + slot's type, and set refuses a slot the class does not declare and a + value that is not an instance. On both backends, because every one of these is a runtime call whose arguments the two emit separately. *) let slots_out = "#state{:pause false :step 3 :speed 1.5 :name \"sand\" :tag :x}\n\ - true\n-7\n[1 2]\n2.5\n9\n6\n12\n3.5\ntrue\n2\n:state\n" + true\n-7\n[1 2]\n2.5\n5\n12\n3.5\ntrue\n2\n:state\n" in outputs "dyn: typed class slots" "programs/dyn-class-slots.flan" slots_out; outputs ~x86:true "dyn: typed class slots, --x86" @@ -5696,8 +5695,7 @@ level "1" is declared i32, and 5000000000 is not a value it holds \ exactly"); ("3", "dyn-slot-trap.flan:17:19: dyn set: state has no slot :paws. \ - Its slots are :pause :step :tag; a key the class does not \ - declare is added with put, not set"); + Its slots are :pause :step :tag"); ("4", "dyn-slot-trap.flan:18:19: dyn set: (get m k) is a place only \ on a class instance, and this is a map with no class"); ("5", "dyn-slot-trap.flan:19:28: dyn construct: the slot :owner of \ @@ -5709,6 +5707,73 @@ level "1" slot_trap (); slot_trap ~x86:true (); + (* A dyn's (.field x) and (at m :key) are get and put: a class slot, a + plain map's key, nil for one it lacks, and chained and compound forms + over both. The .fln spelling is test/syntax/handwritten/dynfields.fln. *) + let fields_out = + "true\nfalse\n12 7 20 11 25\nnil true\n\ + 2 10 nil :one {:hp 2 :mp 10 :name \"slime\" 1 :one}\n" + in + outputs "dyn: .field and [:key]" "programs/dyn-fields.flan" fields_out; + outputs ~opt:"-O0" "dyn: .field and [:key], -O0" "programs/dyn-fields.flan" + fields_out; + outputs ~x86:true "dyn: .field and [:key], --x86" "programs/dyn-fields.flan" + fields_out; + outputs ~dev:true "dyn: .field and [:key], --dev" "programs/dyn-fields.flan" + fields_out; + (* Their refusals are get's, put's and at's own sentences, placed at the + access, in both syntaxes. *) + let field_trap ?x86 path rows = + let exe = compile ?x86 path in + List.iter + (fun (arg, want) -> + let code, text = run exe (Some arg) in + if code <> 134 || not (contains text want) then begin + incr failures; + Printf.printf + "FAIL dyn: a .field's refusal%s\n got: %S (exit %d)\n \ + wanted: %S (exit 134)\n" + (match x86 with Some true -> ", --x86" | _ -> "") + text code want + end) + rows; + (try Sys.remove exe with Sys_error _ -> ()) + in + (* 5 and 6 are a misspelt slot on a class instance, written and read: + refused naming the class and its slots, never a new key or a nil. *) + let field_rows (l0, l1, l2, l3, l4, l5, l6) = + [ ("0", l0 ^ ": dyn get: int and keyword, and only a map answers it — \ + (get 3 :paused)"); + ("1", l1 ^ ": dyn put: the slot :paused of State is declared bool, \ + and this is int — (put #State{:paused false :step false} \ + :paused 1)"); + ("2", l2 ^ ": dyn put: int and keyword, and only a map answers it — \ + (put 3 :paused)"); + ("3", l3 ^ ": dyn put: the slot :step of State is declared bool, and \ + this is int"); + ("4", l4 ^ ": dyn at: int and keyword, and only a text, a vec or a \ + map is indexed — (at 3 :paused)"); + ("5", l5 ^ ": dyn put: State has no slot :pasued. Its slots are \ + :paused :step"); + ("6", l6 ^ ": dyn get: State has no slot :pasued. Its slots are \ + :paused :step") ] + in + let flan_rows = + field_rows ("dyn-field-trap.flan:13:28", "dyn-field-trap.flan:14:19", + "dyn-field-trap.flan:15:19", "dyn-field-trap.flan:16:19", + "dyn-field-trap.flan:19:22", "dyn-field-trap.flan:17:19", + "dyn-field-trap.flan:18:28") + and fln_rows = + field_rows ("dyn-field-trap.fln:14:13", "dyn-field-trap.fln:16:5", + "dyn-field-trap.fln:18:5", "dyn-field-trap.fln:20:5", + "dyn-field-trap.fln:26:13", "dyn-field-trap.fln:22:5", + "dyn-field-trap.fln:24:13") + in + field_trap "programs/dyn-field-trap.flan" flan_rows; + field_trap ~x86:true "programs/dyn-field-trap.flan" flan_rows; + field_trap "programs/dyn-field-trap.fln" fln_rows; + field_trap ~x86:true "programs/dyn-field-trap.fln" fln_rows; + (* A numeric cast opening a dyn box — TODO.org, "A numeric cast opens a dyn box". programs/dyn-cast.flan is one program because the three behaviours are one story told in order: the same-kind casts print, the @@ -7322,6 +7387,14 @@ level "1" cli_case "check on a file that is not there" "check no-such-file.flan" ~code:1 ~says:[ "no-such-file.flan"; "No such file or directory" ]; + (* A file that checks prints nothing; --defs lists what it defined. *) + (match cli "check programs/recur.flan" with + | 0, "" -> () + | code, text -> + incr failures; + Printf.printf "FAIL a clean check prints nothing\n got: %S (exit %d)\n" text code); + cli_case "check --defs lists the definitions" "check programs/recur.flan --defs" ~code:0 + ~says:[ "defn gcd : (Fn [i32 i32] i32)" ]; (* And every other front end takes the same route, since the arm is on the one wrapper they all go through. *) cli_case "build on a file that is not there" diff --git a/test/test_dev.ml b/test/test_dev.ml index a2287ce7..be21fb4e 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -7283,6 +7283,78 @@ let () = List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ xsock2; xout2; msock; mout ]; + (* ── Code from a .fln program, with no :syntax ────────────────────── + A request that leaves :syntax out is read in the syntax of the file it + names, and with no file — evaluating in a stopped frame names none — + in the program's. So a dyn instance's dot assignment evaluates from a + .fln buffer at a park and in the program's own stopped frame. *) + List.iter + (fun backend -> + let fsock = tmp ("flndyn" ^ backend ^ ".sock") + and fout = tmp ("flndyn" ^ backend ^ ".out") in + (try Sys.remove fsock with Sys_error _ -> ()); + let ffd = + Unix.openfile fout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 + in + let fpid = + Unix.create_process flan + [| flan; "dev"; "programs/dev-fln-dyn.fln"; "-s"; fsock; + "--" ^ backend |] + Unix.stdin ffd Unix.stderr + in + Unix.close ffd; + if not (listening ~pid:fpid fsock) then begin + fail "the .fln dyn daemon (--%s) %s" backend !listen_why; + (try Unix.kill fpid Sys.sigkill with Unix.Unix_error _ -> ()) + end + else begin + let c = connect fsock in + let ask ?frame code = + request c + (match frame with + | Some n -> + Printf.sprintf "(:op \"eval-expr\" :frame %d :code %s)" n + (Wire.quote code) + | None -> + Printf.sprintf + "(:op \"eval-expr\" :code %s :file \"programs/dev-fln-dyn.fln\")" + (Wire.quote code)) + in + let answer r = + match Wire.string_field r "value" with + | Some v -> v + | None -> Option.value ~default:(status r) (Wire.string_field r "message") + in + let is ?frame what code want = + let a = answer (ask ?frame code) in + if a <> want then fail "--%s: %s answered %S, not %S" backend what a want + in + if not (await ~ms:20000 (fun () -> status (ask "1") = "ok")) then + fail "--%s: the .fln dyn program never took an expression" backend + else begin + is "a dot assignment from a .fln file" "state.paused = not(state.paused)" "()"; + is "and a dot read" "state.paused" "true"; + ignore (ask "go = true"); + let stopped () = + match Wire.field (request c "(:op \"describe\")") "stopped" with + | Some { Form.v = Form.Sym "t"; _ } -> true + | _ -> false + in + if not (await ~ms:20000 stopped) then + fail "--%s: the .fln dyn program did not stop in boom" backend + else begin + is ~frame:0 "a dot assignment in a stopped frame" + "state.paused = not(state.paused)" "()"; + is ~frame:0 "and a dot read there" "state.paused" "false" + end + end; + (try Unix.close c with Unix.Unix_error _ -> ()); + (try Unix.kill fpid Sys.sigkill with Unix.Unix_error _ -> ()); + (try ignore (Unix.waitpid [] fpid) with Unix.Unix_error _ -> ()) + end; + List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ fsock; fout ]) + [ "x86"; "llvm" ]; + (* ── The dyn globals a park holds ─────────────────────────────────── *) (* The banner a finished run prints says the globals are as it left them, @@ -7418,6 +7490,22 @@ let () = if read () <> "\"kept\"" then fail "--%s: the global was not readable before any thunk ran: %S" backend (read ()); + (* A dyn's .field and [:key] are get and put in a thunk too. *) + List.iter + (fun (code, want) -> + let r = + request c + (Printf.sprintf + "(:op \"eval-expr\" :code %s \ + :file \"programs/dev-dyn-global.flan\")" + (Wire.quote code)) + in + if answer r <> want then + fail "--%s: %s answered %S (%s: %s), not %S" backend code + (answer r) (status r) (said r) want) + [ ("(.s config)", "\"kept\""); + ("(do (set (.n config) 5) (++ (.n config)) (.n config))", "6"); + ("(do (set (at config :n) 1) (at config :n))", "1") ]; for cycle = 1 to 3 do let r = churn () in if status r <> "ok" then @@ -9330,13 +9418,13 @@ let () = (* ── A slot lost, and a second generation ── [:y] goes. Nothing calls [area] after this: its method reads :y, - which is now nil, and a generic that traps on a slot its class no + which is now refused, and a generic that traps on a slot its class no longer has is the program being wrong rather than the migration. *) let r = redefine "(defclass point [x z])" in if status r <> "ok" then fail "removing a slot from a class: %s" (said r) else begin - holds "a lost slot reads as absent" - "(if (= (get (at instances 0) :y) nil) 1 0)"; + holds "a lost slot is absent" + "(if (has-key? (at instances 0) :y) 0 1)"; holds "a lost slot is gone from the count" "(if (= (length (at instances 0)) 2) 1 0)"; holds "the slots either side of it are untouched" @@ -9366,34 +9454,9 @@ let () = "(if (= (type-of (at instances 1)) :point) 1 0)"; holds "and a kind for anything that is not an instance" "(if (= (type-of (get (at instances 1) :w)) :nil) 1 0)" - end; - - (* ── A definition that did not change ── - Every C-c C-k re-runs a file's class definitions, and a generation - bumped per registration rather than per *change* would migrate - every instance in the program on every save. Here that would be - visible: the value written below is put into a slot the class - declares, and a spurious migration would keep it — so the - discriminating half is the raw key on the line after, which a real - migration drops and an ignored re-registration leaves alone. *) - holds "a key written straight into an instance" - "(do (put (at instances 0) :scratch 7) 1)"; - let r = redefine "(defclass point [x z w])" in - if status r <> "ok" then fail "re-evaluating an unchanged class: %s" (said r) - else - holds "an unchanged definition migrates nothing" - "(if (= (get (at instances 0) :scratch) 7) 1 0)"; - (* And the same key after a definition that *did* change, which is - the advisory registry stated as a test rather than as a hope: a - class instance is an open map, [put] accepts any key, and the next - migration drops the ones the class does not declare. TODO.org, "A - redefined defclass migrates its instances lazily", says so in as - many words. *) - let r = redefine "(defclass point [x z w q])" in - if status r <> "ok" then fail "a fourth redefinition: %s" (said r) - else - holds "a migration drops a key the class never declared" - "(if (= (get (at instances 0) :scratch) nil) 1 0)" + end + (* A definition that did not change migrates nothing: the hook block + below asks that with a method that would mark the instance. *) end; (try Unix.close c with Unix.Unix_error _ -> ()); (try Unix.kill mpid Sys.sigkill with Unix.Unix_error _ -> ()); @@ -9625,6 +9688,26 @@ let () = if not (await warned) then fail "%sno warning for a kept value that does not fit: %S" what (output ()) + end; + (* ── A definition that did not change ── + Every C-c C-k re-runs a file's class definitions, and one that + bumped the generation per registration rather than per change + would migrate every instance on every save. A method that marks + the instance says whether a migration ran: not for the same + definition again, and once for a changed one. *) + if defined "a method that marks the instance" + "(defmethod update-instance-for-redefined-class point \ + [p added discarded] (set (get p :radius) 777) nil)" + && defined "the same definition again" + "(defclass point [x str radius z n i32 note str])" + then begin + holds "an unchanged definition migrates nothing" + "(if (= (get (at instances 0) :radius) 4) 1 0)"; + if defined "a changed definition after it" + "(defclass point [x str radius z n i32 note str w])" + then + holds "a changed one runs the method" + "(if (= (get (at instances 0) :radius) 777) 1 0)" end end; (try Unix.close c with Unix.Unix_error _ -> ()); diff --git a/test/test_dyn.ml b/test/test_dyn.ml index 96875fce..7ab4675d 100644 --- a/test/test_dyn.ml +++ b/test/test_dyn.ml @@ -230,7 +230,7 @@ let () = ("atrange", "index 9 is out of bounds for text of length 2"); ("atnegative", "index -1 is out of bounds"); ("setattext", "a text is immutable"); - ("setatnotvec", "only a vec is assigned into"); + ("setatnotvec", "only a vec or a map is assigned into"); ("setatrange", "index 0 is out of bounds for vec of length 0"); ("push", "only a vec is pushed to"); ("needi64", "dyn i64: text"); diff --git a/test/test_flan.ml b/test/test_flan.ml index 70221049..d8189f64 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -2903,6 +2903,12 @@ let () = "(defstruct P [x i32]) (defn f [] i32 (let [p (P {.x 1})] (set (.x p) 2) (.x p)))"; rejects_check "a dotted head that is not a struct says what it is" "(defn f [] i32 (let [n 1] n.x))" ~needle:"n is i32, which has no fields"; + rejects_check "a dotted dyn head points to the accessor" + "(defn f [] i32 (let [s {:p 1}] (println s.p) 0))" + ~needle:"s is dyn, and its :p is reached with (.p s)"; + rejects_check "and to the place in a set" + "(defn f [] i32 (let [s {:p 1}] (set s.p 2) 0))" + ~needle:"s is dyn, and its :p is reached with (set (.p s) ...)"; (* The fourth shape: nothing is bound under the head either, so the message claims nothing about what q is — only that the dot is not the operator the writer took it for. *) diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 1b3843a1..e5de70a1 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -58,7 +58,8 @@ let describe_diff a b = [let] takes in the statements after it (the printer's flat [let]; with every [let] name unique by now, nothing after it can mean one of them). A [(do x)] whose [x] is a [let] is [x], a [let] whose whole body is another - [let] is the merged [let], and [(and x)] and [(or x)] are [x]. The flat + [let] is the merged [let], [(fn [a] (do x y))] is [(fn [a] x y)], and + [(and x)] and [(or x)] are [x]. The flat [let] and the one-argument [and] stop at a quote or quasiquote: data, or a template whose unquotes could name anything. *) @@ -203,6 +204,12 @@ let rec shape ?(q = false) (f : Form.t) : Form.t = | [ { v = Form.List ({ v = Form.Sym "let"; _ } :: { v = Form.Vec bs2; _ } :: body2); _ } ] -> Form.List (h :: Form.make (Form.Vec (List.map sh bs @ bs2)) loc :: body2) | body -> Form.List (h :: Form.make (Form.Vec (List.map sh bs)) loc :: body)) + (* A lambda whose body is one [do] is the lambda of its statements: + the printer writes them straight under [=>]. *) + | Form.List [ ({ v = Form.Sym "fn"; _ } as h); ({ v = Form.Vec _; _ } as ps); + { v = Form.List ({ v = Form.Sym "do"; _ } :: (_ :: _ :: _ as ss)); _ } ] + when not q -> + (sh { f with v = Form.List (h :: ps :: ss) }).v | Form.List (({ v = Form.Sym "handler-case"; _ } as h) :: body :: ({ v = Form.Vec cls; _ } as cv) :: more) -> Form.List (h :: sh body :: { cv with v = Form.Vec (List.map clause cls) } :: List.map sh more) @@ -411,17 +418,19 @@ let () = (* ── Lexical edge cases ────────────────────────────────────────────── *) -let read src = Indent_reader.read_all ~file:"" src +(* [~global:false] reads as an expression the editor sends, where a [let] is + local; a file's top-level [let] is a global. *) +let read ?(global = true) src = Indent_reader.read_all ~global_let:global ~file:"" src -let reads name src want = - match read src with +let reads ?global name src want = + match read ?global src with | forms -> let got = String.concat "\n" (List.map Form.to_string forms) in if got <> want then fail "%s: read %s, wanted %s" name got want | exception e -> fail "%s: refused: %s" name (diag_text e) -let refuses name src kind needle = - match read src with +let refuses ?global name src kind needle = + match read ?global src with | forms -> fail "%s: read %s, wanted the refusal %s" name (String.concat " " (List.map Form.to_string forms)) kind @@ -462,7 +471,7 @@ let () = reads "character literals" "x = [\\( \\, \\space \\)]" "(set x [\\( \\, \\space \\)])"; reads "character arguments" "f(\\,, \\))" "(f \\, \\))"; (* Keywords and annotations. *) - reads "keyword" "def k = :else" "(def k dyn :else)"; + reads "keyword" "let k = :else" "(def k dyn :else)"; reads "annotation" "once grid: [4 [8 u32]]" "(defonce grid [4 [8 u32]])"; reads "keyword argument" "rl/key-pressed?(:key-r)" "(rl/key-pressed? :key-r)"; refuses "colon inside a name" "fn f(x:i32) -> () = x" "indent/colon-in-name" "x: i32"; @@ -521,8 +530,8 @@ let () = (* Statements. *) reads "lets merge" "fn f() -> i32\n let a = 1\n let b = 2\n a + b" "(defn f [] i32 (let [a 1 b 2] (+ a b)))"; - refuses "let with a block" "let a = 1\n a\nb" "indent/let-block" "go at the let's column"; - reads "flat let" "let a = 1\na\nb" "(let [a 1] a b)"; + refuses ~global:false "let with a block" "let a = 1\n a\nb" "indent/let-block" "go at the let's column"; + reads ~global:false "flat let" "let a = 1\na\nb" "(let [a 1] a b)"; reads "elif" "if a\n 1\nelif b\n 2\nelse\n 3" "(cond a 1 b 2 :else 3)"; reads "one-line if" "x = if a then 1 else 2" "(set x (if a 1 2))"; reads "assignment ops" "a[i] += 1" "(set (at a i) (+ (at a i) 1))"; @@ -545,15 +554,71 @@ let () = reads "quote block" "defmacro(m, [x & ys]):\n quote\n f(~x)\n ~@ys" "(defmacro m [x & ys] (quasiquote (do (f (unquote x)) (unquote-splicing ys))))"; - reads "lambda" "g = fn(i, j) = i * 10 + j" "(set g (fn [i j] (+ (* i 10) j)))"; - reads "lambda with a block" "g = fn(i)\n a(i)\n b(i)" "(set g (fn [i] (a i) (b i)))"; + reads "lambda" "g = fn(i, j) => i * 10 + j" "(set g (fn [i j] (+ (* i 10) j)))"; + reads "lambda with a block" "g = fn(i) =>\n a(i)\n b(i)" "(set g (fn [i] (a i) (b i)))"; reads "where" "fn s(xs: [$t]) -> () where ordered?($t) = f(xs)" "(defn s [xs [$t]] () {:where (ordered? $t)} (f xs))"; reads "data" "data Shape\n Circle(r: f32)\n Empty" "(defdata Shape [(Circle [r f32]) Empty])"; reads "enum" "enum K\n lo = -1\n mid" "(defenum K [lo -1 mid])"; reads "struct" "struct Cell\n row: i32\n tag" "(defstruct Cell [row i32 tag dyn])"; - reads "read-only pointer" "def p: Ptr(const u8) = uninit" "(def p (Ptr const u8) uninit)"; + reads "struct with a parent" "struct DiskFull :parent IoError\n free: i64" + "(defstruct DiskFull :parent IoError [free i64])"; + reads "type alias" "type Row = Vec(i32)" "(defalias Row (Vec i32))"; + reads "type alias of an array" "type V2 = [2 f32]" "(defalias V2 [2 f32])"; + reads "a local named type" "type = 3" "(set type 3)"; + reads "a header word assigned" "data += 3" "(set data (+ data 3))"; + reads "a clause word assigned after its header" + "handler-case\n g()\non E(c)\n h(c)\non = 2" + "(handler-case (g) [(E [c] (h c))])\n(set on 2)"; + reads "an else assigned after an if" "if a\n b\nelse = 2" "(when a b)\n(set else 2)"; + reads "class" "class lambda(param, body, env)" "(defclass lambda [param body env])"; + reads "class with typed slots" "class state\n pause: bool\n tag" + "(defclass state [pause bool tag])"; + reads "generic" "generic describe(v) -> dyn" "(defgeneric describe [v] dyn)"; + reads "multi" "multi kind(v) -> dyn = type-of(v)" "(defmulti kind [v] dyn (type-of v))"; + reads "method on a class" "method describe(f: lambda, x)\n g(f)" + "(defmethod describe lambda [f x] (g f))"; + reads "method on a value" "method kind(v) when :else = 1" "(defmethod kind :else [v] 1)"; + refuses "a generic's typed parameter" "generic g(p: point) -> dyn" "indent/dyn-parameter" + "write generic g(p) -> dyn"; + refuses "a method with no dispatch" "method g(p)\n 1" "indent/method-key" + "method g(p: point)"; + refuses "a method with both" "method g(p: point) when :x\n 1" "indent/method-key" + "not both"; + reads "a top-level let is a global" "let g = 1\nlet h: i32 = 2\nlet s: [4 u8]\nf(g)" + "(def g dyn 1)\n(def h i32 2)\n(def s [4 u8])\n(f g)"; + refuses "def" "def g: i32 = 1" "indent/def-is-let" "let g: i32 = 1"; + refuses "a bare def" "def g" "indent/def-is-let" "let g: i32 = 0"; + (match read "generic f(a)\n\nfn g() = 1" with + | exception Loc.Error d when d.Loc.dloc.Loc.line = 1 && d.Loc.dloc.Loc.col = 13 -> () + | exception Loc.Error d -> + fail "a generic with no arrow is refused at %d:%d, not at its line's end" + d.Loc.dloc.Loc.line d.Loc.dloc.Loc.col + | _ -> fail "a generic with no arrow was read"); + reads "a let in a comment block stays local" "comment:\n let x = 1\n f(x)" + "(comment (let [x 1] (f x)))"; + reads "and in a fn" "fn f() -> i32\n let x = 1\n x" "(defn f [] i32 (let [x 1] x))"; + reads "a one-line struct" "struct Pt(x: i32, y)" "(defstruct Pt [x i32 y dyn])"; + reads "a one-line union" "union U(a: i32)" "(defunion U [a i32])"; + reads "a one-line struct with a parent" "struct D(free: i64) :parent Io" + "(defstruct D :parent Io [free i64])"; + refuses "a one-line struct takes no block" "struct Pt(x: i32)\n y: i32" "indent/stray-indent" + "takes no block"; + reads "a parent and no fields" "struct Io :parent Error" "(defstruct Io :parent Error)"; + reads "macro" "macro repeat(i, n, & body)\n quote\n f(~i)\n ~@body" + "(defmacro repeat [i n & body] (quasiquote (do (f (unquote i)) (unquote-splicing body))))"; + reads "macro with a pattern" "macro m([a b], c)\n a" "(defmacro m [[a b] c] a)"; + reads "macro with no parameters" "macro m()\n a" "(defmacro m [] a)"; + refuses "a rest parameter not last" "macro m(& a, b)\n a" "indent/macro-rest-last" + "comes last: macro m(b, & a)"; + reads "loop" "loop x = a, y = b + 1\n recur(y, x)" "(loop [x a y (+ b 1)] (recur y x))"; + reads ~global:false "a let-bound loop" "let r = loop i = 0\n recur(i)\nr" "(let [r (loop [i 0] (recur i))] r)"; + reads "the loop call stays a call" "loop([x 1]):\n x" "(loop [x 1] x)"; + refuses "a loop with no values" "loop\n g()" "indent/loop-bindings" "loop([]):"; + refuses "a loop variable with no value" "loop x, y = 1\n g()" "indent/loop-bindings" + "loop x = 0"; + reads "read-only pointer" "let p: Ptr(const u8) = uninit" "(def p (Ptr const u8) uninit)"; (* Statements that fit on a line, in one-line slots. *) reads "arm statements" "match s\n 1 -> break\n 2 -> continue :outer\n _ -> x += 1" "(match s 1 (break) 2 (continue :outer) _ (set x (+ x 1)))"; @@ -569,9 +634,9 @@ let () = refuses "two assignments" "if a then b = c = d" "indent/assign-in-test" "if a then b = c,"; refuses "a let-bound if with no block" "let r = if a > 1\nr" "indent/expected-block" "if a > 1 takes"; refuses "two bindings on a line" "let v: i32 = a, w = b" "indent/one-binding" "a is followed by a comma"; - reads "a let-bound match" "let r = match a\n 1 -> 2\n _ -> 3\nr" "(let [r (match a 1 2 _ 3)] r)"; - reads "a let-bound if" "let q = if a\n 1\nelse\n 2\nq" "(let [q (if a 1 2)] q)"; - reads "a let-bound call with a block" "let v = foo(a):\n x\nv" "(let [v (foo a x)] v)"; + reads ~global:false "a let-bound match" "let r = match a\n 1 -> 2\n _ -> 3\nr" "(let [r (match a 1 2 _ 3)] r)"; + reads ~global:false "a let-bound if" "let q = if a\n 1\nelse\n 2\nq" "(let [q (if a 1 2)] q)"; + reads ~global:false "a let-bound call with a block" "let v = foo(a):\n x\nv" "(let [v (foo a x)] v)"; refuses "colon after if" "if c:\n y" "indent/header-colon" "no colon"; refuses "colon after a return type" "fn f() -> i32:\n 0" "indent/header-colon" "no colon"; refuses "colon after a number" "while x < 3:\n y" "indent/header-colon" "no colon"; @@ -583,7 +648,7 @@ let () = refuses "mixed separators" "x = [1 2, 3]" "indent/mixed-separators" "[1, 2, 3]"; reads "one-line quote" "defmacro(m, [x]):\n quote ~x + 1" "(defmacro m [x] (quasiquote (+ (unquote x) 1)))"; - reads "typed let" "let x: i32 = 5\nx" "(let [x (the i32 5)] x)"; + reads ~global:false "typed let" "let x: i32 = 5\nx" "(let [x (the i32 5)] x)"; refuses "a let takes no block" "fn f() -> ()\n let x = 1\n g(x)\n h(x)" "indent/let-block" "go at the let's column"; (* Mistakes carried over from other languages, answered in this one. *) @@ -592,10 +657,51 @@ let () = refuses "field with =" "p = P{x = 1}" "indent/brace-field" "{.x value}"; refuses "field with a colon" "p = P{x: 1}" "indent/brace-field" "no colon"; refuses "a dotted range" "for i in 0..10\n g(i)" "indent/dot-range" "range(0, 10)"; - refuses "a block lambda inside a call" "sort-by(xs, fn(a, b)\n a < b)" - "indent/lambda-block-in-brackets" "let f = fn(a: T, b: T) -> R"; - refuses "a typed block lambda inside a call" "sort-by(xs, fn(a: C, b: C) -> bool\n a < b)" - "indent/lambda-block-in-brackets" "let f = fn(a: C, b: C) -> bool"; + (* A lambda's block inside brackets ends where they close. *) + reads "a block lambda inside a call" "sort-by(xs, fn(a, b) =>\n let c = a + 1\n c < b)" + "(sort-by xs (fn [a b] (let [c (+ a 1)] (< c b))))"; + reads "its closer on a line of its own" "sort-by(xs, fn(a, b) =>\n a < b\n)\ng()" + "(sort-by xs (fn [a b] (< a b)))\n(g)"; + reads "a typed block lambda inside a call" "sort-by(xs, fn(a: C, b: C) -> bool =>\n a < b)" + "(sort-by xs (the (Fn [C C] bool) (fn [a b] (< a b))))"; + reads "nested block lambdas" + "map(xs, fn(x) =>\n let ys = map(x, fn(y) =>\n if y > 0\n y\n else\n 0)\n sum(ys))" + "(map xs (fn [x] (let [ys (map x (fn [y] (if (> y 0) y 0)))] (sum ys))))"; + reads "a block lambda on a wrapped argument line" "f(a,\n fn(b) =>\n g(b)\n h(b))" + "(f a (fn [b] (g b) (h b)))"; + reads "a block lambda in a vector" "x = [1, fn(b) =>\n b]" "(set x [1 (fn [b] b)])"; + reads ~global:false "a call after a block lambda's call" "let v = f(fn(a) =>\n a)\ng(v)" + "(let [v (f (fn [a] a))] (g v))"; + refuses "a block lambda is the last argument" "sort-by(fn(a, b) =>\n a < b, xs)" + "indent/lambda-block-last" "let f = fn(a) =>"; + refuses "one block lambda to a call" "f(fn(a) =>\n a\n, fn(b) =>\n b)" + "indent/lambda-block-last" "on line 1"; + refuses "a line back at the header's column" "f(fn(a) =>\n a\nb)" + "indent/lambda-block-last" "b follows the block of the lambda on line 1"; + refuses "an element after a block lambda, left of its block" "m = {:a fn(x) =>\n x\n :b 2}" + "indent/lambda-block-last" ":b follows the block"; + refuses "a block's first line not indented" "f(fn(a) =>\na)" + "indent/lambda-block-left" "Indent it into the block"; + refuses "a block lambda's brackets left open" "f(fn(a) =>\n a\n" + "indent/unclosed" "ends where this bracket closes"; + refuses "indices with no comma" "x = grid[row col].color-idx" + "indent/missing-comma" "Separate indices with commas: grid[row, col]"; + refuses "arguments with no comma" "x = f(a b)" + "indent/missing-comma" "Separate arguments with commas: f(a, b)"; + refuses "a body glued to =>" "x = fn(a) =>a" + "indent/lambda-arrow-space" "fn(a) => a"; + refuses "a lambda written with =" "x = fn(a, b) = a + b" + "indent/lambda-equals" "fn(a, b) => a + b"; + refuses "a typed lambda written with =" "x = fn(a: C) -> bool = a.n > 1" + "indent/lambda-equals" "fn(a: C) -> bool => a.n > 1"; + refuses "a block lambda with no =>" "let f = fn(a)\n a\nf" + "indent/lambda-arrow" "fn(a) =>"; + refuses "a block lambda with no => inside a call" "sort-by(xs, fn(a, b)\n a < b)" + "indent/lambda-arrow" "fn(a, b) =>"; + refuses "a typed block lambda with no => inside a call" "sort-by(xs, fn(a: C, b: C) -> bool\n a < b)" + "indent/lambda-arrow" "fn(a: C, b: C) -> bool =>"; + refuses "a => with nothing under it" "let f = fn(a) =>\nf" + "indent/lambda-body" "fn(a) =>"; refuses "an else after else-if on one line" "if a then x\nelse if b then y\nelse z" "indent/orphan-else" "Write that line as elif"; refuses "else deeper than a one-line if" "if a then b\n else c" @@ -610,17 +716,17 @@ let () = "(cond a b c d e (do (f) (g)) :else (h))"; refuses "else left of a one-line if" "while x\n if a then b\nelse c" "indent/orphan-else" "goes at the if's column"; - reads "a typed lambda" "f = fn(a: C, b) -> bool = a.n < b" + reads "a typed lambda" "f = fn(a: C, b) -> bool => a.n < b" "(set f (the (Fn [C dyn] bool) (fn [a b] (< (.n a) b))))"; - reads "a typed lambda with a block" "let f = fn(x: i32) -> i32\n let y = x + 1\n y\ng(f)" + reads ~global:false "a typed lambda with a block" "let f = fn(x: i32) -> i32 =>\n let y = x + 1\n y\ng(f)" "(let [f (the (Fn [i32] i32) (fn [x] (let [y (+ x 1)] y)))] (g f))"; refuses "a typed lambda states its return type" "f = fn(a: C) = a" - "indent/lambda-return" "fn(a: C) -> R = value"; + "indent/lambda-return" "fn(a: C) -> R => value"; reads "a restart's report on its header" "restart-case\n go()\nrestart retry(n: i32) \"Try again\"\n n" "(restart-case (go) (retry [n i32] :report \"Try again\" n))"; reads "a bare () in a body slot does nothing" - "fn f() -> () = ()\nfn g(x) -> ()\n match x\n 1 -> h()\n _ -> ()\n k = fn() = ()" + "fn f() -> () = ()\nfn g(x) -> ()\n match x\n 1 -> h()\n _ -> ()\n k = fn() => ()" "(defn f [] () (do))\n(defn g [x dyn] () (match x 1 (h) _ (do)) (set k (fn [] (do))))"; reads "a parenthesised () stays a value" "x = (())" "(set x ())"; reads "a template's for takes an unquoted variable" @@ -700,7 +806,7 @@ let () = prints "comment is a body in order" "(comment (let [a 1] (g a)) (h))" "comment:\n let a = 1\n g(a)\n h()"; prints "a later lambda keeps the outer name" "(defn f [] i32 (let [x 1] (let [x 5] (g x)) (app (fn [y] (+ x y)) 2)))" - " let x = 1\n let x-2 = 5\n g(x-2)\n app(fn(y) = x + y, 2)"; + " let x = 1\n let x-2 = 5\n g(x-2)\n app(fn(y) => x + y, 2)"; prints "a renamed name renamed again counts on" "(defn f [] () (let [x 1] (let [x 2] (let [x 3] (g x)) (g x)) (g x)))" " let x = 1\n let x-2 = 2\n let x-3 = 3\n g(x-3)\n g(x-2)\n g(x)"; @@ -790,15 +896,61 @@ let () = "(defn f [a bool b bool c bool] bool (or (and a b) c))" "= (a and b) or c"; prints "a typed lambda prints as one" "(defn f [] () (let [g (the (Fn [C] bool) (fn [c] (> (.n c) 3)))] (h g)))" - "let g = fn(c: C) -> bool = c.n > 3"; + "let g = fn(c: C) -> bool => c.n > 3"; + (* A lambda with a block as a call's last argument prints inside the call, + and reads back as it was. *) + let round name src want = + prints name src want; + let forms = Reader.read_all ~file:"

" src in + let text = Indent_printer.program ~source:src forms in + match Indent_reader.read_all ~file:"

" text with + | back -> + if not (same_forms forms back) then + fail "%s: read back %s from %S" name (describe_diff forms back) text + | exception e -> fail "%s: its text is refused: %s\n%s" name (diag_text e) text + in + round "a one-line lambda" "(defn f [] () (h (fn [a] (+ a 1)) 2))" "= h(fn(a) => a + 1, 2)"; + round "a block lambda as a call's last argument" + "(defn f [] () (sort-by xs (fn [a b] (g a) (< a b))))" + " sort-by(xs, fn(a, b) =>\n g(a)\n a < b)"; + round "one closer for a call in a call" + "(defn f [] () (println (run (fn [] (g) 1))))" " println(run(fn() =>\n g()\n 1))"; + round "a typed block lambda in a call" + "(defn f [] () (h (the (Fn [C] bool) (fn [c] (g c) (> (.n c) 3)))))" + " h(fn(c: C) -> bool =>\n g(c)\n c.n > 3)"; + round "a let's call with a block lambda" + "(defn f [] () (let [v (m xs (fn [x] (g x) x))] (h v)))" + " let v = m(xs, fn(x) =>\n g(x)\n x)\n h(v)"; + round "nested block lambdas" + "(defn f [] () (m xs (fn [x] (let [y (m x (fn [z] (g z) z))] (h y)))))" + " m(xs, fn(x) =>\n let y = m(x, fn(z) =>\n g(z)\n z)\n h(y))"; + round "a block lambda inside an expression, the line going on after it" + "(defn f [] i64 (+ (ap 1 (fn [x] (g x) x)) 1))" " ap(1, fn(x) =>\n g(x)\n x) + 1"; + round "one in a lambda that fits on its line" + "(defn f [] i64 (ap 1 (fn [x] (ap x (fn [y] (g y) y)))))" + " ap(1, fn(x) =>\n ap(x, fn(y) =>\n g(y)\n y))"; + round "a struct literal's last field" + "(defn f [] i64 (let [o (Ops {.k 1 .run (fn [x] (g x) x)})] (.k o)))" + " let o = Ops{.k 1, .run fn(x) =>\n g(x)\n x}"; + round "a returned call" + "(defn f [] i64 (return (ap 2 (fn [x] (g x) x))))" " return ap(2, fn(x) =>\n g(x)\n x)"; + round "a let in a lambda inside a call's argument" + "(defn f [] i64 (println (call0 (fn [] (let [n 0] (set n 1) n)))) 0)" + " println(call0(fn() =>\n let n = 0\n n = 1\n n))"; + prints "a body that is one do goes straight under =>" + "(defn f [] i64 (call0 (fn [] (do (g 1) 2))))" " call0(fn() =>\n g(1)\n 2)"; + round "a block lambda not last keeps the fallback" + "(defn f [] () (r (fn [a b] (g a) b) 0))" "= r(fn([a b], g(a), b), 0)"; + round "a lambda bound by let" + "(defn f [] () (let [k (fn [a] (g a) a)] (k 1)))" " let k = fn(a) =>\n g(a)\n a\n k(1)"; prints "a restart's report goes on its header" "(defn f [] i32 (restart-case (go) (retry [] :report \"Try again\" 7)))" "restart retry() \"Try again\"\n 7"; prints "adjacent one-line globals stay adjacent" "(defonce a i32)\n(def b i32 2)\n\n(defconst c 3)\n" - "once a: i32\ndef b: i32 = 2\n\nconst c = 3"; + "once a: i32\nlet b: i32 = 2\n\nconst c = 3"; back "adjacent one-line globals stay adjacent in parens" - "once a: i32\ndef b: i32 = 2\n\nconst c = 3\n" + "once a: i32\nlet b: i32 = 2\n\nconst c = 3\n" "(defonce a i32)\n(def b i32 2)\n\n(defconst c 3)"; prints "a field of a field chains" "(defn f [] () (g (.count (.x w))))" "g(w.x.count)"; prints "an else-if chain on one line" @@ -807,7 +959,42 @@ let () = prints "a long vector wraps" ("(defn f [] () (let [v [" ^ String.concat " " (List.init 30 string_of_int) ^ "]] (g v)))") " let v = [0 1 2 3"; prints "a template's for keeps its unquotes" - "(defmacro m [i n & body] `(dotimes [~i ~n] ~@body))" "for ~i in range(~n)" + "(defmacro m [i n & body] `(dotimes [~i ~n] ~@body))" "for ~i in range(~n)"; + prints "a macro" "(defmacro m [[a b] n & body] `(do ~@body))" "macro m([a b], n, & body)\n quote"; + prints "a header word assigned keeps no parentheses" + "(defn f [] () (set data 3) (set loop 4) (set on 5))" " data = 3\n loop = 4\n on = 5"; + prints "a class" "(defclass point [x y])" "class point(x, y)"; + prints "a class's typed slots, and one typed as a class of the file" + "(defclass state [pause bool tag])\n(defclass node [owner state n])" + "class state(pause: bool, tag)\n\nclass node(owner: state, n)"; + prints "a generic" "(defgeneric area [self] dyn)" "generic area(self) -> dyn"; + prints "a multi" "(defmulti kind [v] dyn (type-of v))" "multi kind(v) -> dyn = type-of(v)"; + prints "a method on a class" "(defmethod area point [p] (g p) (h p))" + "method area(p: point)\n g(p)\n h(p)"; + prints "a method on a value" "(defmethod kind :int [v] \"n\")" "method kind(v) when :int = \"n\""; + prints "a global is a top-level let" "(def g dyn 1)\n(def h i32 2)" "let g = 1\nlet h: i32 = 2"; + prints "a def inside a form keeps the fallback" "(comment (def g i32 1))" "comment:\n def(g, i32, 1)"; + prints "a local let at the top level goes in a do block" "(let [x 1] (f x))" + "do:\n let x = 1\n f(x)"; + prints "a type alias" "(defalias Row (Vec i32))" "type Row = Vec(i32)"; + prints "a struct with a parent" "(defstruct D :parent Io [free i64])" + "struct D(free: i64) :parent Io"; + prints "a struct on one line" "(defstruct Pt [x i32 y dyn])" "struct Pt(x: i32, y)\n"; + prints "a union too" "(defunion U [a i32 b f32])" "union U(a: i32, b: f32)"; + prints "a struct too long for a line takes a line per field" + ("(defstruct W [" ^ String.concat " " (List.init 8 (Printf.sprintf "field-number-%d i32")) ^ "])") + "struct W\n field-number-0: i32\n"; + prints "and so does one with a comment among its fields" + "(defstruct C [a i32 ; first\n b i32])" "struct C\n a: i32 ; first\n b: i32"; + prints "a parent with no fields" "(defstruct D :parent Io)" "struct D :parent Io"; + prints "an empty field vector under a parent keeps the fallback" + "(defstruct D :parent Io [])" "defstruct(D, :parent, Io, [])"; + prints "a loop" "(defn f [a i32] i32 (loop [x a y 0] (if (= x 0) y (recur (- x 1) (+ y 1)))))" + " loop x = a, y = 0\n if x == 0 then y"; + prints "a let-bound loop" "(defn f [] i32 (let [r (loop [i 0] (recur i))] r))" + " let r = loop i = 0\n recur(i)"; + prints "a lambda as a loop's value is parenthesised" + "(defn f [] () (loop [g (fn [x] x) n 0] (recur g n)))" "loop g = (fn(x) => x), n = 0" (* ── Spans, for pause marks and error overlays ──────────────────────── *) @@ -1014,8 +1201,8 @@ let () = [ "it has to be written: i32(x)" ]; refused "unknown-type.fln" "fn f(p: Keyword) -> i32 = 0\n\nfn main() -> i32 = 0\n" [ "unknown type Keyword" ]; - refused "untyped-lambda.fln" "fn main() -> i32\n let f = fn(a)\n a\n 0\n" - [ "let f: Fn(T, ...) -> R = fn(...)" ]; + refused "untyped-lambda.fln" "fn main() -> i32\n let f = fn(a) =>\n a\n 0\n" + [ "let f: Fn(T, ...) -> R = fn(...) =>" ]; refused "plusplus.fln" "fn main() -> i32\n let x = 1\n x++\n x\n" [ "write ++(x) or x += 1" ]; refused "plusplus-global.fln" "once g = 0\n\nfn main() -> i32\n g--\n 0\n" @@ -1045,25 +1232,28 @@ let () = fail "map-key.fln: %d errors, wanted one" (List.length ds) | exception _ -> () | _ -> ()); - (* The fix the lambda-in-brackets refusal shows compiles, with its - placeholders filled in. *) + (* The fix a block lambda with no => is shown compiles, inside the call. *) (match read "sort-by(xs, fn(a, b)\n a.n < b.n)" with | _ -> fail "lambda in brackets: read" | exception Loc.Error d -> let header = let m = d.Loc.dmsg in - let i = String.index m '=' + 2 in + let i = String.index m '\n' + 6 in String.sub m i (String.index_from m i '\n' - i) in - let header = - String.concat "C" (String.split_on_char 'T' header) - |> String.split_on_char 'R' |> String.concat "bool" - in checks "lambda-fix.fln" ("struct C\n n: i32\n\nfn main() -> i32\n let xs = [C{.n 2} C{.n 1}]\n" - ^ " let f = " ^ header ^ "\n a.n < b.n\n sort-by(slice(xs), f)\n xs[0].n\n")); + ^ " sort-by(slice(xs), " ^ header ^ "\n a.n < b.n)\n xs[0].n\n")); + (* A typed block lambda inside a call, a closer on its own line, and one + nested in another's block. *) + checks "block-lambdas.fln" + ("struct C\n n: i32\n\nfn app(x: i64, f: Fn(i64) -> i64) -> i64 = f(x)\n\n" + ^ "fn main() -> i32\n let xs = [C{.n 2} C{.n 1}]\n" + ^ " sort-by(slice(xs), fn(a: C, b: C) -> bool =>\n let d = a.n - b.n\n d < 0)\n" + ^ " let k = app(2, fn(a) =>\n let b = app(a, fn(c) =>\n c * 10\n )\n b + 1)\n" + ^ " i32(k) + xs[0].n\n"); refused "cfn-captures.fln" - "fn app(f: CFn(Option(i32)) -> i32) -> i32 = f(None)\n\nfn main() -> i32\n let k = 1\n app(fn(o) = k)\n" + "fn app(f: CFn(Option(i32)) -> i32) -> i32 = f(None)\n\nfn main() -> i32\n let k = 1\n app(fn(o) => k)\n" [ "so it is a Fn(Option(i32)) -> i32 and not a CFn(Option(i32)) -> i32" ]; refused "defvar.fln" "defvar(x, 1)\n\nfn main() -> i32 = 0\n" [ "once x = 1 initialises once"; "def x = 1 re-initialises" ] diff --git a/web/examples/quotes.sh b/web/examples/quotes.sh index 630157fd..631bf749 100644 --- a/web/examples/quotes.sh +++ b/web/examples/quotes.sh @@ -81,10 +81,9 @@ do name=${pair%%:*}; src=${pair#*:} printf '%s\n' "$src" > "$here/.q.flan" out=$("$FLAN" check "$here/.q.flan" 2>&1) - # A probe that compiles is the failure this loop is most likely to meet, and - # it is the one that used to be unreadable: `flan check` answers a clean - # program with its whole symbol table, so the needle became eighty lines of - # prelude signatures and the diff said nothing. Say what actually happened. + # A probe that compiles is the failure this loop is most likely to meet: + # `flan check` prints nothing for it, so the needle would be empty and the + # diff would say nothing. Say what actually happened. if "$FLAN" check "$here/.q.flan" >/dev/null 2>&1; then echo "FAIL message: $name" echo " this program compiles now — the page still says it is refused" diff --git a/web/index.html b/web/index.html index e5395bb2..8fa15b52 100644 --- a/web/index.html +++ b/web/index.html @@ -700,7 +700,9 @@ program was edited elsewhere would not be worth having.

{:a 1 :b "two"} is a dyn map and [1 2 3] is a dyn vector. get, put, has-key?, at and length read and write them, the same names the typed -Map and Vec answer to. A keyword is a value here rather +Map and Vec answer to. (.hp m) is +(get m :hp) and (at m :hp) is too; set on +either is put. A keyword is a value here rather than only a way to name an enum member: keywords are interned, so comparing two is comparing two pointers.

@@ -724,10 +726,11 @@ for anything that is not an instance. type-of answers any value's kind as a keyword — :nil, :bool, :int, :float, :text, :vec, :map or :keyword — and an instance's class name, so a class cannot be named -after one of those kinds. The slots are map keys: get -reads one, and set writes one, as in -(set (get s :pause) true). put writes one too, and is -also how a key the class does not declare is added.

+after one of those kinds. The slots are map keys: (.pause s) +reads one, and (set (.pause s) true) writes one, checking its +type. get and put do the same. A key the class does +not declare is refused, so a misspelled slot stops the program at the line that +misspelled it.

Dispatch comes in the two styles and they are one mechanism. defgeneric dispatches on the class of the first argument, which is