diff --git a/TODO.org b/TODO.org index 7a1bfdb8..0269ce97 100644 --- a/TODO.org +++ b/TODO.org @@ -296,6 +296,15 @@ keyword resolves against the expected type and against nothing else, so two enum could always share a member spelling. What the prefix buys is the call site read on its own. +** WAIT ML-style patterns +Held 2026-09-25 as a future direction, like the JS backend: nested destructuring, +guards, or-patterns, literals at any depth, exhaustiveness over the nesting. + +** DONE match over numbers and strings +CLOSED: [2026-09-25] +Rules out a literal the scrutinee's type cannot hold (refused, not widened as =(=)= +would), keyword arms over a dyn, and a bare-name catch-all: a bare name is a nullary case. + ** DONE match over enums CLOSED: [2026-09-25] =Ast.Pkw= is the keyword pattern; =Check.check_match= resolves it against the @@ -637,6 +646,10 @@ of !=. * Checker +** WAIT A _ body that returns an fn literal +Refused today; allowing it when the literal writes its parameter types is the +proposal. Postponed 2026-09-25 while .fln takes priority. + ** DONE The ownership flow analysis is repealed CLOSED: [2026-09-18] Static use-after-move and double-free checking is gone; types, allocators and the @@ -736,15 +749,10 @@ depth it gave up at. The bare depth number is a backstop that also prints the chain. Before any of it, the compiler hung rather than failed, which wedges =C-c C-c= with nothing to show. -** NEXT Generic types -Decided 2026-09-25: the freeze is lifted for this; build both type and length parameters. -=(defstruct Pair [a $t b $t])= cannot be spelled, and neither can a length -parameter. =Types.Named= is a bare string with no room for parameters; giving it -some changes the type, the layout calculator, both backends, the renderer and the -DWARF path. Same price for one as for both. Decided and unblocked, deliberately -not started — it is a language feature under a freeze, and it was stopped once -already for that reason. The motivating case is Odin's =Small_Array=: a -fixed-capacity array with a count and no allocation. +** DONE Generic types +CLOSED: [2026-09-25] +A struct's parameters are its fields' $-names in first-written order, a length by position; there is no +explicit parameter vector. Each application is an ordinary struct under a key, so no backend sees a parameter. ** WAIT A value predicate over a length parameter Decided 2026-09-25: waits until a program wants one. @@ -753,12 +761,6 @@ clause here admits nothing but type predicates. Whether it should take value predicates over a length parameter deserves answering deliberately rather than falling out of the implementation. -** TODO "In instantiation of" notes -A refusal inside a copy points at the generic's source with no note naming the -call site that asked for that type. The data is there — =instantiation_origin= -exists and the session already uses it — and wiring it into every failure under an -instantiation is a lane of its own. - ** DONE A program is one compilation, so a generic's body is always visible CLOSED: [2026-09-25] Odin's and Zig's model: packages are never compiled separately. The cost is build @@ -982,15 +984,6 @@ ignore order, writable access has to alias the real storage. Flexible field orde waits for classes deliberately, because a class owns its layout and a =Vector2= should not pay for identity and metadata. Not implemented. -** TODO An error in a called generic's body is reported twice -=(defn g [x $t] u64 (nosuch x))= called once from =main= prints "unknown -function nosuch" twice at the same place and counts 2 errors — once from the -abstract pass and once from the instantiation. - -** TODO A type variable is printed without its $ -=Types.to_string= prints =Var t= as =t=, so a refusal reads "selection-sort -expects [t] here, found [3 i32]" where the source wrote =[$t]=. - ** DONE Two refusals suggested something that does not compile CLOSED: [2026-09-25] =vec-new= and =map-new= with no type no longer say "or give the binding a type"; @@ -1448,6 +1441,11 @@ out the first element typing the rest. * Dev loop +** WAIT A _ caller whose type follows a redefined callee +Its signature changes in the session but its body is not recompiled, so every call +stops on StaleCall naming a type nobody wrote. Proposal: recompile such callers. +Postponed 2026-09-25 while .fln takes priority. + ** TODO A prelude function shadowed live is reached by the prelude's own calls A defn of a prelude function's name sent to a running =flan dev= installs into the host's cell for that name, so the prelude's calls compiled into the host follow it; diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index be05ee23..a44a8dce 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -1187,6 +1187,54 @@ Use `C-c C-g` if you need frames. --- +## Indented files (.fln) + +`.fln` files open in `flan-fln-mode`. The session keys (`C-c C-b`, `C-c C-i`, +`C-c C-k`, the REPL, watch, dape) work as in a `.flan` file; these differ. + +| Holy | Evil | Does | +|---|---|---| +| `C-c C-c`, `C-M-x` | same | the top-level form: a declaration installed, anything else evaluated | +| `C-u C-c C-c` | same | ...and stop at the innermost bracket group, else the statement on point's line: an elif's condition, an else's block, a match arm's value (`C-u C-u`: on entry) | +| `C-x C-e` | same, cursor on the line's last character | at a line's end, the innermost statement ending there: a match arm's value, an if/elif/while condition, or the whole statement a header or clause line opens; elsewhere, the term before point | +| `C-c C-e` | same | the statement at point with its body and clauses, or the region's whole lines; on a bare `let x = v`, the `let` and the rest of its block | +| `C-c C-n` | same | `C-c C-e`, then move to the next statement | +| `C-c C-s` | same | step through the top-level `fn` at point | +| `C-c C-k` | same | the whole buffer | +| `C-M-a` / `C-M-e` / `C-M-h` | `[[` / `]]` | top-level form: start, end, mark | +| `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 | +| `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 | +| `M-` / `M-` | same | pull the next statement into this block / push its last one out | +| `M-r` | same | replace the block's owner with the statement at point | +| `M-k` | `das` | kill the statement's lines | +| — | `ie` `ae` | term | +| — | `is` `as` | statement (`as`: whole lines) | +| — | `ii` `ai` | body / whole statement | +| — | `ik` `ak` | clause's block / clause | +| — | `id` `ad` | top-level form with the comment block directly above it (`ad`: and the empty lines after it, or before it for the last form) | + +`else`, `elif`, `on` and `restart` snap to their header's column as you type +them. `indent-region` and `C-y` move lines only as a block, never one line +against another. expand-region steps term, group, statement, clause, +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. +- **body**: a statement's own block, up to its first clause. +- **clause**: one `else`/`elif`/`on`/`restart` line and its block. +- **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. + +`flan-fln-indent-offset` (2) is one level. `flan-fln-smartparens` (`t`) turns +on plain `smartparens-mode`, which pairs brackets and strings but not `'`. + +--- + ## Full key reference | Key | Does | @@ -1267,6 +1315,7 @@ fix is to delete `-dev` from it. | File | What it is | |---|---| | `flan-mode.el` | the major mode: syntax, indentation, imenu, the keymap | +| `flan-fln-mode.el` | the mode for indented `.fln` files: objects, keys, indentation | | `flan.el` | the client — the socket, evaluation, xref, eldoc, completion | | `flan-repl.el` | the `*flan-repl*` buffer | | `flan-watch.el` | watched values: the program pushes, this paints them in a buffer and inline | diff --git a/emacs/flan-dape.el b/emacs/flan-dape.el index 4ba767be..9f55a19a 100644 --- a/emacs/flan-dape.el +++ b/emacs/flan-dape.el @@ -89,14 +89,14 @@ does and does not buy." :type '(repeat string)) (defun flan-dape--source () - "The .flan file this session is about. + "The .flan or .fln file this session is about. The buffer's own file, or the nearest one up from it — so M-x flan-debug from a *compilation* buffer or a dired still has an answer." (or (and buffer-file-name - (string-suffix-p ".flan" buffer-file-name) + (string-match-p "\\.fla?n\\'" buffer-file-name) buffer-file-name) - (car (directory-files default-directory t "\\.flan\\'")) - (user-error "No .flan file here to debug"))) + (car (directory-files default-directory t "\\.fla?n\\'")) + (user-error "No .flan or .fln file here to debug"))) (defun flan-dape--binary (source) "Where the debug build of SOURCE goes. @@ -126,7 +126,7 @@ one made by `flan build'." ;; can just read `buffer-file-name'. `dape' itself expects an already ;; evaluated config, so `flan-debug' below must not hand it the raw entry. (defconst flan-dape-config - '(modes (flan-mode) + '(modes (flan-base-mode) ensure dape-ensure-command command-cwd dape-command-cwd compile (flan-dape--compile-command (flan-dape--source)) @@ -178,7 +178,7 @@ common case is one command rather than a config prompt." ;; thing entirely. ;;;###autoload (with-eval-after-load 'flan-mode - (define-key (symbol-value 'flan-mode-map) (kbd "C-c C-g") #'flan-debug)) + (define-key (symbol-value 'flan-base-mode-map) (kbd "C-c C-g") #'flan-debug)) ;;; --dev and --debug are different builds ;; diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el new file mode 100644 index 00000000..dfd90147 --- /dev/null +++ b/emacs/flan-fln-mode.el @@ -0,0 +1,1715 @@ +;;; flan-fln-mode.el --- Major mode for indented Flan (.fln) -*- lexical-binding: t; -*- + +;; Author: Joseph Ferano +;; Version: 0.1.0 +;; Package-Requires: ((emacs "29.1")) +;; Keywords: languages, tools + +;; The mode for the indented syntax (spec-syntax.md). It shares everything +;; that talks to the running program with `flan-mode' through their parent, +;; `flan-base-mode', and owns the one thing that differs: what a piece of the +;; text is. +;; +;; Nothing here parses Flan. Every object is found from lines, columns and +;; the syntax table: a statement is a line and the lines under it, a term is a +;; run with no space outside brackets. The reader decides what the text +;; means; this only decides which text to send, and sends it unchanged with +;; the line and column it starts at. +;; +;; The objects, smallest first: +;; +;; term a run with no space in it outside brackets: `x', `f(a, b)', +;; `grid[r, c]', `camera.target.x'. +;; group a bracket pair and what is inside it. +;; statement a line, the deeper lines under it, the lines inside brackets it +;; 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. +;; 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. +;; top-level a column-0 statement start and everything to the next one. + +;;; Code: + +(require 'flan-mode) +(require 'thingatpt) +(require 'seq) + +;; The client, which every command that sends code needs and which this file +;; must not load merely to edit one. +(declare-function flan--eval "flan" (code what &optional start end pause step)) +(declare-function flan--eval-expression "flan" (start end arg)) +(declare-function flan--text "flan" (start end)) +(declare-function flan--text-at "flan" (start end)) +(declare-function flan--report "flan" (reply what &optional at)) +(declare-function flan--request "flan" (form)) +(defvar flan--declaration-heads) +(defvar flan--defun-heads) +;; Set buffer-locally when the packages are there; declared so the setq-local +;; below does not make a stray global. +(defvar er/try-expand-list) +(defvar evil-shift-width) +(defvar evil-state) +(declare-function smartparens-mode "smartparens" (&optional arg)) +(declare-function sp-local-pair "smartparens" (modes open close &rest args)) +(declare-function evil-define-key* "evil-core" (state keymap key def &rest bindings)) +(declare-function evil-range "evil-common" (beg end &optional type &rest properties)) + +(defcustom flan-fln-indent-offset 2 + "Columns one block level adds in a .fln file." + :type 'integer + :group 'flan) + +(defcustom flan-fln-smartparens t + "Turn on plain `smartparens-mode' in .fln buffers, when it is installed. +Plain rather than strict: strict mode keeps s-expressions balanced, and a +.fln file's blocks are not s-expressions, so it would refuse edits that are +fine here. Brackets and strings are still paired." + :type 'boolean + :group 'flan) + +;;; Words + +;; The spaced binary operators of lib/indent_reader.ml (`binops'). A line +;; that starts with one, or follows a line that ends with one, continues the +;; line above. +(defconst flan-fln--binops + '("or" "and" "==" "!=" "<" "<=" ">" ">=" "<<" ">>" "+" "-" "*" "/" "%")) + +(defconst flan-fln--binop-re (regexp-opt flan-fln--binops)) + +(defconst flan-fln--clause-words '("else" "elif" "on" "restart") + "Words that start a clause of the statement above, at its column.") + +(defconst flan-fln--clause-re + "\\(else\\|elif\\|on\\|restart\\)\\(?:[ \t]\\|$\\)") + +(defconst flan-fln--clause-headers + '(("else" "if" "elif") ("elif" "if" "elif") + ("on" "handler-case" "handler-bind" "on") + ("restart" "restart-case" "restart")) + "For each clause word, the words of the lines it may sit under.") + +;; `header_follow' in lib/indent_reader.ml: the words a statement starts with. +(defconst flan-fln--header-words + '("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")) + +;; 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 +;; when it is the one-line `fn f(x) = e'; `if' does not when it is the one-line +;; `if c then a else b'. +(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")) + +(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")) + "Each declaration header word, and the paren head it reads as.") + +;;; Syntax + +(defvar flan-fln-mode-syntax-table + (let ((table (make-syntax-table))) + ;; Name characters: a name is anything up to a delimiter (`is_delimiter' + ;; in lib/reader.ml), so `key-pressed?', `dyn->f64' and `rl/draw-fps' are + ;; one symbol each. + (dolist (c '(?- ?_ ?? ?! ?/ ?. ?$ ?& ?* ?+ ?< ?> ?= ?% ?@ ?# ?^ ?| ?~)) + (modify-syntax-entry c "_" table)) + ;; Not `:', which ends `x: T' and `comment:': a name glued to it would + ;; otherwise read as `x:', a name nothing defines. A `:key' keyword is + ;; drawn by its own font-lock rule instead. + (modify-syntax-entry ?: "." table) + (modify-syntax-entry ?\; "<" table) + (modify-syntax-entry ?\n ">" table) + (modify-syntax-entry ?\" "\"" table) + ;; `\c' is a character literal, `\(' included: escaping it keeps the + ;; paren out of the bracket count. + (modify-syntax-entry ?\\ "\\" table) + (modify-syntax-entry ?' "'" table) + ;; A comma separates, so it is not part of any term. + (modify-syntax-entry ?, "." table) + (modify-syntax-entry ?\( "()" table) + (modify-syntax-entry ?\) ")(" table) + (modify-syntax-entry ?\[ "(]" table) + (modify-syntax-entry ?\] ")[" table) + (modify-syntax-entry ?{ "(}" table) + (modify-syntax-entry ?} "){" table) + table) + "Syntax table for `flan-fln-mode'.") + +;;; Lines + +;; Every object is built from these. A position argument names the line it +;; is on; each answers for that line and leaves point alone. + +(defun flan-fln--bol (pos) + (save-excursion (goto-char pos) (line-beginning-position))) + +(defun flan-fln--indent-at (pos) + (save-excursion (goto-char pos) (current-indentation))) + +(defun flan-fln--first-char (pos) + "Where the text of POS's line starts." + (save-excursion (goto-char pos) (back-to-indentation) (point))) + +(defun flan-fln--in-open-p (pos) + "Non-nil if POS's line starts inside a bracket or a string." + (let ((s (save-excursion (syntax-ppss (flan-fln--bol pos))))) + (or (> (car s) 0) (nth 3 s)))) + +(defun flan-fln--blank-p (pos) + "Non-nil if POS's line holds no code: it is blank, or only a comment." + (save-excursion + (goto-char (flan-fln--bol pos)) + (and (not (nth 3 (syntax-ppss (point)))) + (progn (skip-chars-forward " \t") + (or (eolp) (eq (char-after) ?\;)))))) + +(defun flan-fln--code-end (pos) + "Where the code on POS's line ends: before a trailing comment and spaces." + (save-excursion + (goto-char pos) + (let* ((bol (line-beginning-position)) + (eol (line-end-position)) + (s (syntax-ppss eol))) + (goto-char (if (and (nth 4 s) (>= (nth 8 s) bol)) (nth 8 s) eol)) + (skip-chars-backward " \t" bol) + (point)))) + +(defun flan-fln--ends-in-op-p (pos) + "Non-nil if POS's line ends in a spaced binary operator." + (save-excursion + (let ((end (flan-fln--code-end pos))) + (goto-char end) + (and (not (nth 3 (syntax-ppss end))) + (looking-back (concat "\\(?:^\\|[ \t]\\)" flan-fln--binop-re) + (line-beginning-position)))))) + +(defun flan-fln--starts-with-op-p (pos) + "Non-nil if POS's line starts with a spaced binary operator." + (save-excursion + (goto-char pos) + (back-to-indentation) + (looking-at (concat flan-fln--binop-re "\\(?:[ \t]\\|$\\)")))) + +(defun flan-fln--prev-code (pos) + "The start of the nearest code line above POS's line, or nil." + (save-excursion + (goto-char pos) + (let (hit) + (while (and (not hit) (zerop (forward-line -1))) + (unless (flan-fln--blank-p (point)) (setq hit (point)))) + hit))) + +(defun flan-fln--next-code (pos) + "The start of the nearest code line below POS's line, or nil." + (save-excursion + (goto-char pos) + (let (hit) + (while (and (not hit) (zerop (forward-line 1)) (not (eobp))) + (unless (flan-fln--blank-p (point)) (setq hit (point)))) + ;; The last line of a buffer with no final newline: `forward-line' + ;; reports it could not move a whole line, but it did reach it. + (when (and (not hit) (eobp) (not (bolp)) + (> (line-beginning-position) (flan-fln--bol pos)) + (not (flan-fln--blank-p (point)))) + (setq hit (line-beginning-position))) + hit))) + +(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) + (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--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." + (and (not (flan-fln--continuation-p pos)) + (save-excursion + (goto-char pos) + (back-to-indentation) + (looking-at flan-fln--clause-re)))) + +(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)) + 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) + (while (and (setq n (flan-fln--next-code bol)) + (flan-fln--continuation-p n)) + (setq bol n)) + bol)) + +;;; Statements + +(defun flan-fln--statement-last (start &optional no-clauses) + "The last code line of the statement whose first line is START. +With NO-CLAUSES, stop before the first clause at START's column: the first +line's own block only." + (let* ((indent (flan-fln--indent-at start)) + (last (flan-fln--logical-end start)) + (next (flan-fln--next-code last))) + (while (and next + (or (> (flan-fln--indent-at next) indent) + (flan-fln--continuation-p next) + (and (not no-clauses) + (= (flan-fln--indent-at next) indent) + (flan-fln--clause-line-p next)))) + (setq last (flan-fln--logical-end next) + next (flan-fln--next-code last))) + last)) + +(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))) + +(defun flan-fln--statement-bounds (start) + "Bounds of the statement whose first line is START." + (flan-fln--span start (flan-fln--statement-last start))) + +(defun flan-fln--clause-header (start) + "The header line the clause at START belongs to, or START if it is not one." + (if (not (flan-fln--clause-line-p start)) + start + (let ((ind (flan-fln--indent-at start)) (p start) hit) + (while (and (not hit) (setq p (flan-fln--prev-code p))) + (setq p (flan-fln--logical-start p)) + (let ((i (flan-fln--indent-at p))) + (cond ((< i ind) (setq hit start)) + ((and (= i ind) (not (flan-fln--clause-line-p p))) + (setq hit p))))) + (or hit start)))) + +(defun flan-fln--code-line-at (pos) + "POS's line if it holds code, else the code line above, else the one below." + (let ((bol (flan-fln--bol pos))) + (if (flan-fln--blank-p bol) + (or (flan-fln--prev-code bol) (flan-fln--next-code bol)) + bol))) + +(defun flan-fln--line-statement (pos) + "The first line of the statement POS's line starts or continues. +A clause line answers for itself." + (let ((l (flan-fln--code-line-at pos))) + (and l (flan-fln--logical-start l)))) + +(defun flan-fln--statement-start-at (pos) + "The first line of the statement at POS; a clause gives its header's." + (let ((l (flan-fln--line-statement pos))) + (and l (flan-fln--clause-header l)))) + +(defun flan-fln--parent (start) + "The nearest line above START that is shallower: its block's owner, or nil. +A clause line owns its own block, so it can be the answer." + (let ((ind (flan-fln--indent-at start)) (p start) hit) + (when (> ind 0) + (while (and (not hit) (setq p (flan-fln--prev-code p))) + (setq p (flan-fln--logical-start p)) + (when (< (flan-fln--indent-at p) ind) (setq hit p)))) + hit)) + +(defun flan-fln--body-bounds (start) + "Bounds of the block under START's first line, or nil when it has none." + (let* ((head-last (flan-fln--logical-end start)) + (first (flan-fln--next-code head-last))) + (when (and first (> (flan-fln--indent-at first) (flan-fln--indent-at start))) + (flan-fln--span first (flan-fln--statement-last start t))))) + +(defun flan-fln--clause-at (pos) + "The clause line whose clause holds POS, the innermost one, or nil. +A match arm is a clause too: its pattern line and its value or block." + (let* ((line (flan-fln--line-statement pos)) + (p line) + (limit (and line (1+ (flan-fln--indent-at line)))) + hit) + (while (and p (not hit)) + (let ((i (flan-fln--indent-at p))) + (when (< i limit) + (if (or (flan-fln--clause-line-p p) (flan-fln--arm p)) + (setq hit p) + (setq limit i))) + (setq p (and (> limit 0) + (let ((q (flan-fln--prev-code p))) + (and q (flan-fln--logical-start q))))))) + hit)) + +(defun flan-fln--clause-bounds (start) + (flan-fln--span start (flan-fln--statement-last start t))) + +;;; Top-level forms + +(defun flan-fln--toplevel-start-p (pos) + "Non-nil if POS's line starts a top-level form. +Column 0 and code, and not a clause, an operator continuation, or a line +inside a bracket or a string -- spec-syntax.md §4.5, tightened." + (and (not (flan-fln--blank-p pos)) + (zerop (flan-fln--indent-at pos)) + (not (flan-fln--continuation-p pos)) + (not (flan-fln--clause-line-p pos)))) + +(defun flan-fln--toplevel-start (pos) + "The first line of the top-level form at or before POS, else the next one." + (save-excursion + (goto-char (flan-fln--bol pos)) + (let (hit) + (while (and (not hit) + (progn (when (flan-fln--toplevel-start-p (point)) + (setq hit (point))) + (not hit)) + (zerop (forward-line -1)))) + (unless hit + (goto-char (flan-fln--bol pos)) + (while (and (not hit) (zerop (forward-line 1)) (not (eobp))) + (when (flan-fln--toplevel-start-p (point)) (setq hit (point))))) + hit))) + +(defun flan-fln--toplevel-last (start) + "The last code line of the top-level form starting at START." + (save-excursion + (goto-char start) + (let ((last start)) + (while (and (zerop (forward-line 1)) (not (eobp)) + (not (flan-fln--toplevel-start-p (point)))) + (unless (flan-fln--blank-p (point)) (setq last (point)))) + ;; A last line with no newline after it. + (when (and (eobp) (not (bolp)) + (not (flan-fln--toplevel-start-p (line-beginning-position))) + (not (flan-fln--blank-p (point)))) + (setq last (line-beginning-position))) + last))) + +(defun flan-fln--toplevel-bounds (pos) + "Bounds of the top-level form at POS, or nil in a buffer with none." + (let ((s (flan-fln--toplevel-start pos))) + (and s (flan-fln--span s (flan-fln--toplevel-last s))))) + +(defun flan-fln--beginning-of-defun (&optional arg) + "Move to the start of the ARGth top-level form back; forward when negative. +For `beginning-of-defun-function'." + (setq arg (or arg 1)) + (let ((found t)) + (if (> arg 0) + (dotimes (_ arg) + (let ((here (point)) hit) + (beginning-of-line) + (when (and (< (point) here) (flan-fln--toplevel-start-p (point))) + (setq hit t)) + (while (and (not hit) (zerop (forward-line -1))) + (when (flan-fln--toplevel-start-p (point)) (setq hit t))) + (unless hit (setq found nil)))) + (dotimes (_ (- arg)) + (let (hit) + (while (and (not hit) (zerop (forward-line 1)) (not (eobp))) + (when (flan-fln--toplevel-start-p (point)) (setq hit t))) + (unless hit (setq found nil))))) + found)) + +(defun flan-fln--end-of-defun () + "From the start of a top-level form, move to its end. +For `end-of-defun-function'." + (goto-char (cdr (flan-fln--span (point) (flan-fln--toplevel-last (point)))))) + +;;; Terms and groups + +(defun flan-fln--term-forward (pos) + (save-excursion + (goto-char pos) + (let (done) + (while (and (not done) (not (eobp))) + (let ((c (char-after)) + (syn (syntax-class (syntax-after (point))))) + (cond ((memq c '(?\s ?\t ?\n ?, ?\;)) (setq done t)) + ((eq c ?\\) (forward-char (min 2 (- (point-max) (point))))) + ((memq syn '(4 7)) + (condition-case nil (forward-sexp 1) + (scan-error (setq done t)))) + ((eq syn 5) (setq done t)) + (t (forward-char 1))))) + (point)))) + +(defun flan-fln--term-back (pos) + (save-excursion + (goto-char pos) + (let (done) + (while (and (not done) (not (bobp))) + (let ((c (char-before)) + (syn (syntax-class (syntax-after (1- (point)))))) + (cond ((and (> (1- (point)) (point-min)) + (eq (char-before (1- (point))) ?\\)) + (backward-char 2)) + ((memq c '(?\s ?\t ?\n ?,)) (setq done t)) + ((eq syn 4) (setq done t)) + ((memq syn '(5 7)) + (condition-case nil (backward-sexp 1) + (scan-error (setq done t)))) + (t (backward-char 1))))) + (point)))) + +(defun flan-fln--trim-colon (beg end) + "END, less the trailing colon of `x:' or `f(x):' when BEG..END has one." + (if (and (> end (1+ beg)) (eq (char-before end) ?:) + (not (eq (char-after beg) ?:))) + (1- end) + end)) + +(defun flan-fln--term-bounds (pos) + "Bounds of the term at POS: a run with no space in it outside brackets." + (let ((s (save-excursion (syntax-ppss pos)))) + (unless (nth 4 s) + (let* ((pos (if (nth 3 s) (nth 8 s) pos)) + (beg (flan-fln--term-back pos)) + (end (flan-fln--trim-colon beg (flan-fln--term-forward pos)))) + (and (< beg end) (cons beg end)))))) + +(defun flan-fln--term-before (pos) + "Bounds of the term that ends at POS, spaces before POS skipped, or nil. +A comment is skipped too: nothing in one is a term." + (save-excursion + (goto-char pos) + (let ((s (syntax-ppss pos))) + (when (nth 4 s) (goto-char (nth 8 s)))) + (skip-chars-backward " \t,") + (let* ((end (point)) + (beg (flan-fln--term-back end)) + (end (flan-fln--trim-colon beg end))) + (and (< beg end) (cons beg end))))) + +(defun flan-fln--group-bounds (pos) + "Bounds of the innermost bracket pair around POS, or nil." + (let ((open (nth 1 (save-excursion (syntax-ppss pos))))) + (and open (cons open (ignore-errors (scan-lists open 1 0)))))) + +(defun flan-fln--group-form-start (open) + "Where the reader starts the form the bracket at OPEN belongs to. +A bracket glued to what is before it is a call, an index or a struct literal, +and the form starts where that term does (`f' of `f(x)', not the paren). A +free `(' groups one value and the form is that value. A free `[' or `{' is +the form." + (let ((before (char-before open))) + (cond + ((and before (not (memq before '(?\s ?\t ?\n ?, ?\( ?\[ ?\{)))) + (let ((b (flan-fln--term-back open))) + ;; `-f(x)' reads as the negation of the call, and the call starts at f. + (if (and (eq (char-after b) ?-) + (string-match-p "[a-zA-Z$_*]" (string (char-after (1+ b))))) + (1+ b) + b))) + ((eq (char-after open) ?\() + (save-excursion + (goto-char (1+ open)) + (skip-chars-forward " \t\n") + (if (eq (char-after) ?\)) open (point)))) + (t open)))) + +;;; Declarations + +(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', ... -- +or the fallback call's name, `defmethod(', must read as one of HEADS, +`flan--declaration-heads' by default." + (save-excursion + (let ((state (syntax-ppss pos))) + (goto-char pos) + (and (zerop (current-column)) + (zerop (car state)) + (not (nth 8 state)) + (let ((head + (cond + ((looking-at (concat (regexp-opt (mapcar #'car flan-fln--declaration-words) t) + "[ \t]")) + (cdr (assoc (match-string-no-properties 1) + flan-fln--declaration-words))) + ((looking-at "\\([^][ \t\n(){},;\":]+\\)(") + (match-string-no-properties 1))))) + (and head (member head (or heads flan--declaration-heads)) head)))))) + +;;; Sending + +(defun flan-fln--client () + (require 'flan)) + +(defun flan-fln--cut-indent (beg) + "The byte column of the statement BEG was cut out of, when BEG is mid-line. +A condition or an arm's value starts after `elif ' or `-> ', and a line that +continues it is indented past the statement's start, perhaps not past the +cut. The reader is told where the statement starts (`:indent'), so it +reads the text as the file has it, and every location it reports is still +the buffer's own." + (save-excursion + (goto-char beg) + (when (> (current-column) (current-indentation)) + (back-to-indentation) + (1+ (- (position-bytes (point)) + (position-bytes (line-beginning-position))))))) + +(defun flan-fln--eval-expression (beg end arg) + "Evaluate BEG..END as an expression, as `flan--eval-expression' does, and +with `:indent' when BEG is mid-line." + (flan--report + (flan--request + (let ((at (flan--text-at beg end)) + (indent (flan-fln--cut-indent beg))) + (append (list :op "eval-expr" :code (car at) + :file (or buffer-file-name "")) + (cdr at) + (when indent (list :indent indent)) + (when arg (list :pause t))))) + "expression" + end)) + +(defun flan-fln--send (beg end arg heads) + "Send BEG..END: installed when it is a declaration at column 0, else run. +HEADS says which heads count as declarations. ARG is a prefix: on a +declaration it marks the whole form (stop on entry), on an expression it is +the stop-here flag." + (let ((head (flan-fln--declaration-head-at beg heads))) + (if head + (flan--eval (flan--text beg end) head beg end (and arg (cons beg end))) + (prog1 (flan-fln--eval-expression beg end arg) + (pulse-momentary-highlight-region beg end))))) + +(defun flan-fln--pause-bounds (b arg) + "The form to mark inside the top-level form B for prefix ARG, or nil. +One `C-u': the innermost bracket group around point -- the form it belongs +to, from where the reader starts it -- else the statement on point's line. +Two: B itself, which stops on entry." + (cond + ((null arg) nil) + ((and (consp arg) (> (prefix-numeric-value arg) 4)) b) + (t (or (flan-fln--pause-target (point) b) b)))) + +(defun flan-fln--pause-target (pos b) + "The form to stop at for point at POS inside the top-level form B. +Its start is where the reader starts that form, which is all the daemon +matches on: a match arm's value or block, not its pattern, which is no +form; an elif's condition and an else's, on's or restart's block, which are +forms, where the clause line itself is not." + (let* ((l (flan-fln--line-statement pos)) + (arm (and l (flan-fln--arm l))) + (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))) + (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)) + (t (let ((s (flan-fln--statement-start-at pos))) + (and s (flan-fln--statement-bounds s))))))) + +(defun flan-fln--clause-target (l) + "What a clause line L stops at: an elif's condition, else its block." + (save-excursion + (goto-char (flan-fln--first-char l)) + (if (looking-at "elif[ \t]+") + (cons (match-end 0) (flan-fln--code-end (flan-fln--logical-end l))) + (flan-fln--body-bounds l)))) + +(defun flan-fln--condition (l) + "Bounds of the condition on the if, elif, while or until line L, or nil. +A loop's label, `while :outer c', is not part of it." + (save-excursion + (goto-char (flan-fln--first-char l)) + (when (looking-at "\\(?:if\\|elif\\|while\\|until\\)[ \t]+\\(?::[^ \t]+[ \t]+\\)?") + (let ((beg (match-end 0)) + (end (flan-fln--code-end (flan-fln--logical-end l)))) + (and (< beg end) (cons beg end)))))) + +(defun flan-fln--arm (l) + "The match arm whose joined line starts at L, or nil. +A plist: :arrow, where its ` -> ' starts; :value, the bounds of what follows +the arrow on the line, or of the block under it; :binds, non-nil when the +pattern names something, so the value cannot be evaluated alone." + (let ((parent (flan-fln--parent l))) + (when (and parent + (string-match-p "\\(?:\\`\\|[ \t]=[ \t]+\\)match\\(?:[ \t]\\|\\'\\)" + (buffer-substring-no-properties + (flan-fln--first-char parent) + (flan-fln--code-end parent)))) + (save-excursion + (let* ((start (flan-fln--first-char l)) + (last (flan-fln--code-end (flan-fln--logical-end l))) + (depth (car (syntax-ppss start))) + arrow) + (goto-char start) + (while (and (not arrow) + (re-search-forward "[ \t]\\(->\\)\\(?:[ \t]\\|$\\)" last t)) + (let ((ps (save-excursion (syntax-ppss (match-beginning 1))))) + (when (and (= (car ps) depth) (not (nth 8 ps))) + (setq arrow (match-beginning 1))))) + (when arrow + (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) + (flan-fln--body-bounds l)))) + (and value + (list :arrow arrow :value value + :binds (flan-fln--uses-any-p + (flan-fln--pattern-names pat) + (buffer-substring-no-properties + (car value) (cdr value)))))))))))) + +(defun flan-fln--names-in (text &optional skip-heads) + "Every name in TEXT, as the syntax table reads names, and each dotted part +of one: `p.x' gives `p.x', `p' and `x'. With SKIP-HEADS, not a name glued to +a `(' -- a pattern's constructor, which binds nothing." + (with-temp-buffer + (set-syntax-table flan-fln-mode-syntax-table) + (insert text) + (goto-char (point-min)) + (let (names) + (while (re-search-forward "\\(?:\\sw\\|\\s_\\)+" nil t) + (let ((n (match-string-no-properties 0))) + ;; A `-' glued to a letter, `$', `_' or `*' is negation, not part + ;; of the name (`is_neg_char' in lib/indent_reader.ml): `-n' is n. + (when (string-match "\\`-[a-zA-Z$_*]" n) + (setq n (substring n 1))) + (unless (and skip-heads (eq (char-after) ?\()) + (push n names) + (dolist (part (split-string n "\\." t)) + (push part names))))) + (delete-dups names)))) + +(defun flan-fln--pattern-names (pat) + "The names pattern PAT may bind: every name in it but a constructor head. +Numbers are not names. Generous otherwise -- a keyword or a constant counts +too -- because a name +wrongly counted only sends the whole match, and one missed sends a value +that reads a global of the same name and shows a wrong answer." + (seq-remove (lambda (n) (string-match-p "\\`[-+]?[0-9]" n)) + (flan-fln--names-in pat t))) + +(defun flan-fln--uses-any-p (names text) + "Non-nil if TEXT has any of NAMES as a name, or as part of a dotted one." + (seq-some (lambda (n) (member n names)) (flan-fln--names-in text))) + +(defun flan-fln--arm-to-send (l arm) + "What evaluating the match arm at L sends: its value, or, when its pattern +binds a name the value uses, the whole match." + (if (plist-get arm :binds) + (flan-fln--statement-bounds + (flan-fln--statement-start-at (flan-fln--parent l))) + (plist-get arm :value))) + +;;;###autoload +(defun flan-fln-eval-defun (&optional arg) + "Evaluate the top-level form at point in the running program. +A declaration is installed and anything else is evaluated, as `C-c C-c' does +in a .flan file. With one \\[universal-argument] on a declaration, also stop +at the innermost bracket group around point, or else at the statement on +point's line, when it next runs; with two, on entry." + (interactive "P") + (flan-fln--client) + (let ((b (flan-fln--toplevel-bounds (point)))) + (unless b (user-error "flan: no top-level form at point to evaluate")) + (let ((head (flan-fln--declaration-head-at (car b) flan--defun-heads))) + (if head + (flan--eval (flan--text (car b) (cdr b)) head (car b) (cdr b) + (flan-fln--pause-bounds b arg)) + (prog1 (flan--eval-expression (car b) (cdr b) arg) + (pulse-momentary-highlight-region (car b) (cdr b))))))) + +;;;###autoload +(defun flan-fln-step-defun () + "Install the top-level form at point so a call stops before each form. +`flan-step-defun' for a .fln buffer: the same stepper, over this syntax's +top-level form." + (interactive) + (flan-fln--client) + (let* ((b (flan-fln--toplevel-bounds (point))) + (head (and b (flan-fln--declaration-head-at (car b) flan--defun-heads)))) + (unless head (user-error "flan: no fn at point to step through")) + (flan--eval (flan--text (car b) (cdr b)) "form" (car b) (cdr b) nil t))) + +(defun flan-fln--point-for-last () + "Point, or under Evil's normal state the position after the cursor's char. +The cursor sits *on* the last character of a line, never after it." + (if (and (bound-and-true-p evil-local-mode) + (memq evil-state '(normal motion)) + (not (eolp))) + (1+ (point)) + (point))) + +(defun flan-fln--statement-ending-at (pos) + "Bounds of the innermost statement whose last line is POS's, or nil." + (let ((l (flan-fln--line-statement pos))) + (when (and l (not (flan-fln--blank-p pos)) (not (flan-fln--clause-line-p l)) + (= (flan-fln--statement-last l) (flan-fln--bol pos))) + (flan-fln--statement-bounds l)))) + +;;;###autoload +(defun flan-fln-eval-last (&optional arg) + "Evaluate the statement that ends at point, or the term before point. +At the end of a line, the innermost statement whose last line it is; a +declaration at column 0 is installed. Anywhere else, the term that ends +before point. With ARG, stop there instead, as \\[flan-eval-last-sexp] does." + (interactive "P") + (flan-fln--client) + (let* ((end (flan-fln--point-for-last)) + ;; A line the next one continues -- an operator at either side of + ;; the break, or a bracket left open -- ends where the whole joined + ;; line does, not at its last word. + (end (if (and (>= end (flan-fln--code-end end)) + (not (flan-fln--blank-p end)) + (let ((n (flan-fln--next-code end))) + (and n (flan-fln--continuation-p n)))) + (flan-fln--code-end + (flan-fln--logical-end (flan-fln--logical-start end))) + end)) + (at-end (and (>= end (flan-fln--code-end end)) + (not (flan-fln--blank-p end)))) + (l (and at-end (flan-fln--line-statement end))) + ;; Point ends the line L starts, joined lines included. + (l-end (and l (= (flan-fln--bol end) (flan-fln--logical-end l)))) + (arm (and l-end (flan-fln--arm l))) + (st (and at-end (not arm) (flan-fln--statement-ending-at end))) + (opens (and l-end (not st) (not arm) + (or (flan-fln--clause-line-p l) + (/= (flan-fln--statement-last l) + (flan-fln--logical-end l)))))) + (cond + (arm (let ((b (flan-fln--arm-to-send l arm))) + (flan-fln--send (car b) (cdr b) arg flan--declaration-heads))) + (st (flan-fln--send (car st) (cdr st) arg flan--declaration-heads)) + ;; The end of a line that opens a block: its last word is not what was + ;; meant. A condition is a value of its own; anything else is sent + ;; with its block, a clause with the statement it belongs to. + ((and opens (flan-fln--condition l)) + (let ((c (flan-fln--condition l))) + (flan-fln--eval-expression (car c) (cdr c) arg))) + (opens + (let ((b (flan-fln--statement-bounds (flan-fln--clause-header l)))) + (flan-fln--send (car b) (cdr b) arg flan--declaration-heads))) + (t + (let ((tb (flan-fln--term-before end))) + (unless tb (user-error "flan: no form before point to evaluate")) + (flan--eval-expression (car tb) (cdr tb) arg)))))) + +(defun flan-fln--snap-lines (beg end) + "BEG..END widened to whole lines, less leading blank lines and trailing space." + (let ((first (flan-fln--bol beg)) + (last (flan-fln--bol (if (and (> end beg) + (save-excursion (goto-char end) (bolp))) + (1- end) + end)))) + (when (flan-fln--blank-p first) (setq first (flan-fln--next-code first))) + (when (and first (<= first last)) + (cons (flan-fln--first-char first) + (save-excursion (goto-char last) (end-of-line) + (skip-chars-backward " \t\n" first) (point)))))) + +(defun flan-fln--bare-let-p (start) + "Non-nil if START's statement is a `let' with no block of its own." + (and (save-excursion (goto-char (flan-fln--first-char start)) + (looking-at "let[ \t]")) + (= (flan-fln--statement-last start) (flan-fln--logical-end start)))) + +(defun flan-fln--block-rest (start) + "START's statement and every statement after it in the same block." + (let* ((ind (flan-fln--indent-at start)) + (last (flan-fln--statement-last start)) + (next (flan-fln--next-code last))) + (while (and next (= (flan-fln--indent-at next) ind) + (not (flan-fln--clause-line-p next))) + (setq last (flan-fln--statement-last next) + next (flan-fln--next-code last))) + (flan-fln--span start last))) + +(defun flan-fln--statement-to-send (pos) + "The statement at POS as sent by `C-c C-e': with its body and clauses, and +for a bare `let x = v', with the rest of its block, which is its scope." + (let* ((l (flan-fln--line-statement pos)) + (arm (and l (flan-fln--arm l))) + (s (flan-fln--statement-start-at pos))) + (cond (arm (flan-fln--arm-to-send l arm)) + ((null s) nil) + ((flan-fln--bare-let-p s) (flan-fln--block-rest s)) + (t (flan-fln--statement-bounds s))))) + +;;;###autoload +(defun flan-fln-eval-statement (&optional arg) + "Evaluate the statement at point, with its body and clauses. +With an active region, the lines it touches instead. On a bare `let x = v', +the `let' and the rest of its block, which is what it is in scope for; the +span sent is flashed so the extent is visible. A declaration at column 0 is +installed. ARG is as for \\[flan-fln-eval-last]." + (interactive "P") + (flan-fln--client) + (let ((b (if (use-region-p) + (flan-fln--snap-lines (region-beginning) (region-end)) + (flan-fln--statement-to-send (point))))) + (unless b (user-error "flan: no statement at point to evaluate")) + (when (use-region-p) (deactivate-mark)) + (flan-fln--send (car b) (cdr b) arg flan--defun-heads))) + +;;;###autoload +(defun flan-fln-eval-statement-and-next () + "Evaluate the statement at point, then move to the statement after it." + (interactive) + (flan-fln--client) + (let ((b (flan-fln--statement-to-send (point)))) + (unless b (user-error "flan: no statement at point to evaluate")) + (prog1 (flan-fln--send (car b) (cdr b) nil flan--defun-heads) + (let ((n (flan-fln--next-code (cdr b)))) + (goto-char (if n (flan-fln--first-char n) (cdr b))))))) + +;;; Motion + +(defun flan-fln-backward-statement (&optional n) + "Move to the start of the statement at point, or to the one before it. +Before it at the same level, else out to the line that owns this block." + (interactive "^p") + (dotimes (_ (or n 1)) + (let* ((l (flan-fln--line-statement (point))) + (here (and l (flan-fln--first-char l)))) + (cond + ((null l)) + ((> (point) here) (goto-char here)) + (t + (let ((ind (flan-fln--indent-at l)) (p l) hit) + (while (and (not hit) (setq p (flan-fln--prev-code p))) + (setq p (flan-fln--logical-start p)) + (when (<= (flan-fln--indent-at p) ind) + (setq hit (if (= (flan-fln--indent-at p) ind) + (flan-fln--clause-header p) + p)))) + (when hit (goto-char (flan-fln--first-char hit))))))))) + +(defun flan-fln-forward-statement (&optional n) + "Move to the end of the statement at point, or of the one after it." + (interactive "^p") + (dotimes (_ (or n 1)) + (let* ((s (flan-fln--statement-start-at (point))) + (e (and s (cdr (flan-fln--statement-bounds s))))) + (cond + ((null s)) + ((< (point) e) (goto-char e)) + (t (let ((nx (flan-fln--next-code (flan-fln--statement-last s)))) + (when nx + (goto-char (cdr (flan-fln--statement-bounds + (flan-fln--clause-header + (flan-fln--logical-start nx)))))))))))) + +(defun flan-fln-next-statement (&optional n) + "Move to the start of the statement after the one at point." + (interactive "^p") + (dotimes (_ (or n 1)) + (let* ((s (flan-fln--statement-start-at (point))) + (nx (and s (flan-fln--next-code (flan-fln--statement-last s))))) + (when nx (goto-char (flan-fln--first-char nx)))))) + +(defun flan-fln-up (&optional n) + "Move up to the bracket around point, or to the line that owns its block." + (interactive "^p") + (dotimes (_ (or n 1)) + (let ((open (nth 1 (syntax-ppss)))) + (if open + (goto-char open) + (let* ((l (flan-fln--line-statement (point))) + (p (and l (flan-fln--parent l)))) + (if p (goto-char (flan-fln--first-char p)) + (user-error "flan: at top level"))))))) + +;;; Marking, for expand-region and anything else that marks + +(defun flan-fln--region () + (if (use-region-p) (cons (region-beginning) (region-end)) (cons (point) (point)))) + +(defun flan-fln--larger-p (b r) + "Non-nil if bounds B contain region R and are larger than it." + (and b (cdr b) (<= (car b) (car r)) (>= (cdr b) (cdr r)) + (> (- (cdr b) (car b)) (- (cdr r) (car r))))) + +(defun flan-fln--mark (b) + (when b + (goto-char (car b)) + (push-mark (cdr b) t t) + (activate-mark) + b)) + +(defun flan-fln-mark-term () + "Mark the term at point, or the one around the marked text." + (interactive) + (let* ((r (flan-fln--region)) + (b (flan-fln--term-bounds (car r)))) + ;; Out through the brackets until the term holds the region. + (while (and b (not (flan-fln--larger-p b r))) + (let ((g (flan-fln--group-bounds (car b)))) + (setq b (and g (flan-fln--term-bounds (car g)))))) + (flan-fln--mark b))) + +(defun flan-fln-mark-group () + "Mark the bracket group around point, or around the marked text." + (interactive) + (let* ((r (flan-fln--region)) + (b (flan-fln--group-bounds (car r)))) + (while (and b (not (flan-fln--larger-p b r))) + (setq b (flan-fln--group-bounds (car b)))) + (flan-fln--mark b))) + +(defun flan-fln--statement-around (r) + "The innermost statement that holds region R and is larger than it." + (let* ((s (flan-fln--statement-start-at (car r))) + (b (and s (flan-fln--statement-bounds s)))) + (while (and s (not (flan-fln--larger-p b r))) + ;; A clause's statement is its header's. + (let ((h (flan-fln--clause-header s))) + (setq s (if (/= h s) h (flan-fln--parent s))) + (setq b (and s (flan-fln--statement-bounds s))))) + b)) + +(defun flan-fln-mark-statement () + "Mark the statement at point, or the one around the marked text." + (interactive) + (flan-fln--mark (flan-fln--statement-around (flan-fln--region)))) + +(defun flan-fln-mark-clause () + "Mark the clause at point, or the one around the marked text." + (interactive) + (let* ((r (flan-fln--region)) + (c (flan-fln--clause-at (car r))) + (b (and c (flan-fln--clause-bounds c)))) + (while (and c (not (flan-fln--larger-p b r))) + (setq c (let ((p (flan-fln--parent c))) (and p (flan-fln--clause-at p)))) + (setq b (and c (flan-fln--clause-bounds c)))) + (flan-fln--mark b))) + +(defun flan-fln-mark-toplevel () + "Mark the top-level form at point." + (interactive) + (flan-fln--mark (flan-fln--toplevel-bounds (car (flan-fln--region))))) + +;; thing-at-point, so `(bounds-of-thing-at-point 'flan-fln-statement)' and +;; everything built on it knows the objects. +(put 'flan-fln-term 'bounds-of-thing-at-point + (lambda () (flan-fln--term-bounds (point)))) +(put 'flan-fln-group 'bounds-of-thing-at-point + (lambda () (flan-fln--group-bounds (point)))) +(put 'flan-fln-statement 'bounds-of-thing-at-point + (lambda () (let ((s (flan-fln--statement-start-at (point)))) + (and s (flan-fln--statement-bounds s))))) +(put 'flan-fln-body 'bounds-of-thing-at-point + (lambda () (let ((s (flan-fln--statement-start-at (point)))) + (and s (flan-fln--body-bounds s))))) +(put 'flan-fln-clause 'bounds-of-thing-at-point + (lambda () (let ((c (flan-fln--clause-at (point)))) + (and c (flan-fln--clause-bounds c))))) +(put 'flan-fln-toplevel 'bounds-of-thing-at-point + (lambda () (flan-fln--toplevel-bounds (point)))) + +;;; Indentation + +;; TAB offers the columns of the blocks open above the line, plus one level +;; deeper after a line that opens a block. The first TAB takes the deepest, +;; each repeated TAB steps out one. A block's column is the program's +;; meaning, so nothing here ever moves a line on its own: `indent-region' +;; shifts rigidly and only when the first line is at no valid column, and a +;; yank moves its lines together. + +(defun flan-fln--opener-p (start last) + "Non-nil if the joined line START..LAST opens a block on the lines under it." + (or (save-excursion + (goto-char start) + (back-to-indentation) + (and (looking-at (concat (regexp-opt flan-fln--opener-words t) + "\\([ \t]\\|$\\)")) + (let ((w (match-string-no-properties 1)) + (alone (string= (match-string-no-properties 2) "")) + (end (flan-fln--code-end last))) + (cond + ((member w '("defer" "quote")) alone) + ((member w '("fn" "fn-")) + (not (re-search-forward "[ \t]=[ \t]" end t))) + ((member w '("if" "elif")) + (not (re-search-forward "[ \t]then[ \t]" end t))) + (t t))))) + (save-excursion + (goto-char (flan-fln--code-end last)) + (let ((bol (line-beginning-position))) + (or (looking-back ":" bol) + (looking-back "[ \t]->" bol) + ;; `let x =' and `def colors =' with the value as a block, + ;; which the author's list leaves out and the reader reads. + (looking-back "[ \t]=" bol) + ;; `let f = fn(x)' with its body under it. + (looking-back "\\_ min 0) (flan-fln--prev-code p)))) + (unless (eql min 0) (push (cons 0 nil) out)) + (nreverse out))) + +(defun flan-fln--levels (pos) + "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))) + +(defun flan-fln--clause-columns (word pos) + "Columns of the lines above POS a clause WORD may sit under, deepest first." + (let ((heads (cdr (assoc word flan-fln--clause-headers)))) + (delq nil + (mapcar (lambda (e) + (and (cdr e) + (save-excursion + (goto-char (flan-fln--first-char (cdr e))) + (and (looking-at (concat (regexp-opt heads) + "\\(?:[ \t]\\|$\\)")) + (car e))))) + (flan-fln--stack pos))))) + +(defun flan-fln--bracket-column (open) + "The column a line inside the bracket at OPEN goes to. +Under the first element when one follows the bracket on its line, one level +in from that line when none does -- the paren mode's rule for data and +calls alike. A line that starts with the closing bracket goes to the +opening line's column." + (save-excursion + (let ((closing (save-excursion (back-to-indentation) (looking-at "\\s)")))) + (goto-char open) + (if closing + (current-indentation) + (forward-char 1) + (skip-chars-forward " \t") + (if (or (eolp) (eq (char-after) ?\;)) + (+ (current-indentation) flan-fln-indent-offset) + (current-column)))))) + +(defun flan-fln--indent-candidates (pos) + "Columns TAB offers POS's line, deepest first; nil to leave it alone." + (save-excursion + (goto-char (flan-fln--bol pos)) + (let ((s (syntax-ppss (point)))) + (cond + ((nth 3 s) nil) + ((> (car s) 0) (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)))) + ((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)))))))))) + +(defun flan-fln-indent-line () + "Indent the line to a block column. +A line with text at a valid column stays there: its column is its meaning, +and a TAB pressed to see where it goes must not change it. An empty line +goes to the deepest column. Each repeated TAB then steps out one." + (let* ((cands (flan-fln--indent-candidates (point))) + (cur (current-indentation)) + (target + (cond ((null cands) nil) + ((and (eq this-command 'indent-for-tab-command) + (eq last-command 'indent-for-tab-command) + (memq cur cands)) + (or (cadr (memq cur cands)) (car cands))) + ((and (memq cur cands) + (not (save-excursion (beginning-of-line) + (looking-at-p "[ \t]*$")))) + cur) + (t (car cands))))) + (if (null target) + 'noindent + (let ((from-end (and (> (current-column) cur) (- (point-max) (point))))) + (indent-line-to target) + (when from-end (goto-char (- (point-max) from-end))))))) + +(defun flan-fln-indent-region (start end) + "Shift START..END rigidly so its first line sits at a block column. +Never re-indents a line against the others: the columns are the program." + (save-excursion + (goto-char start) + (beginning-of-line) + (while (and (< (point) end) (flan-fln--blank-p (point)) + (zerop (forward-line 1)))) + (when (< (point) end) + (let ((cands (flan-fln--indent-candidates (point))) + (cur (current-indentation))) + (when (and cands (not (memq cur cands))) + (indent-rigidly (point) end (- (car cands) cur))))))) + +(defun flan-fln-dedent-or-delete (arg) + "In a line's indentation, drop it one block level; else delete a character." + (interactive "*p") + (if (and (= arg 1) (not (use-region-p)) + (> (current-column) 0) + (= (current-column) (current-indentation)) + (not (flan-fln--in-open-p (point)))) + (let ((cur (current-indentation))) + (indent-line-to (or (seq-find (lambda (c) (< c cur)) + (flan-fln--levels (point))) + 0))) + (let ((cmd (or (command-remapping 'delete-backward-char) + #'delete-backward-char))) + (setq this-command cmd) + (call-interactively cmd)))) + +(defun flan-fln--electric-clause () + "Snap an else, elif, on or restart line to its header as it is typed. +Run when the word is finished by a space or a newline." + (when (memq last-command-event '(?\s ?\n ?\r)) + (save-excursion + (let ((nl (not (eq last-command-event ?\s)))) + (when nl (forward-line -1)) + (let ((text (buffer-substring-no-properties + (line-beginning-position) + (if nl (line-end-position) (point))))) + (when (and (string-match "\\`[ \t]*\\(else\\|elif\\|on\\|restart\\)[ \t]*\\'" + text) + (not (flan-fln--in-open-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)))))))) + ;; The new line was indented from the old one before it moved. + (when (memq last-command-event '(?\n ?\r)) + (let ((c (car (flan-fln--indent-candidates (point))))) + (when (and c (= (current-column) (current-indentation))) + (indent-line-to c)))))) + +(defun flan-fln-shift-right (start end &optional n) + "Shift the lines of the region, or the line, right by N block levels." + (interactive (if (use-region-p) + (list (region-beginning) (region-end) (prefix-numeric-value current-prefix-arg)) + (list (line-beginning-position) (line-end-position) + (prefix-numeric-value current-prefix-arg)))) + (let ((deactivate-mark nil)) + (indent-rigidly (flan-fln--bol start) end (* (or n 1) flan-fln-indent-offset)))) + +(defun flan-fln-shift-left (start end &optional n) + "Shift the lines of the region, or the line, left by N block levels." + (interactive (if (use-region-p) + (list (region-beginning) (region-end) (prefix-numeric-value current-prefix-arg)) + (list (line-beginning-position) (line-end-position) + (prefix-numeric-value current-prefix-arg)))) + (let ((deactivate-mark nil)) + (indent-rigidly (flan-fln--bol start) end (- (* (or n 1) flan-fln-indent-offset))))) + +(defun flan-fln--yank-base (start end first-ws) + "The column the yanked text between START and END was written at. +FIRST-WS is the width of its first line's own indentation, when it kept one." + (if (> first-ws 0) + first-ws + ;; The first line was cut from its first character: read the column off + ;; the lines under it. A clause sits at the statement's column; failing + ;; one, the shallowest line is a body, one level in. + (let (min clause) + (save-excursion + (goto-char start) + (while (and (zerop (forward-line 1)) (< (point) end)) + (unless (flan-fln--blank-p (point)) + (let ((i (current-indentation))) + (when (or (null min) (< i min)) (setq min i clause nil)) + (when (and (= i min) + (save-excursion (back-to-indentation) + (looking-at flan-fln--clause-re))) + (setq clause t)))))) + (cond ((null min) 0) + (clause min) + (t (max 0 (- min flan-fln-indent-offset))))))) + +(defun flan-fln-yank (&optional arg) + "Yank, then move the lines after the first with it, rigidly. +The first line lands at point; every other line keeps its place relative to +it, so a block pasted at another depth stays one block." + (interactive "*P") + (let* ((col (current-column)) + (at-indent (<= col (current-indentation)))) + (setq this-command 'yank) + (yank arg) + (let ((end (copy-marker (max (point) (mark t)))) + (start (min (point) (mark t)))) + (save-excursion + (goto-char start) + (when (< (line-end-position) end) + (let* ((ws (save-excursion (skip-chars-forward " \t") (- (point) start))) + (base (flan-fln--yank-base start end ws))) + (when (and at-indent (> ws 0)) + (delete-region start (+ start ws))) + (forward-line 1) + (when (< (point) end) + (indent-rigidly (point) end (- col base)))))) + (set-marker end nil)))) + +;;; Block editing + +(defun flan-fln--ensure-final-newline () + (save-excursion + (goto-char (point-max)) + (unless (bolp) (insert "\n")))) + +(defun flan-fln--lines (start last) + "(BEG . END) of the whole lines START through LAST, final newline included." + (cons start (save-excursion (goto-char last) (line-beginning-position 2)))) + +(defun flan-fln--statement-lines (s) + (flan-fln--lines s (flan-fln--statement-last s))) + +(defun flan-fln-kill-statement () + "Kill the statement at point, whole lines, body and clauses included." + (interactive) + (flan-fln--ensure-final-newline) + (let ((s (flan-fln--statement-start-at (point)))) + (unless s (user-error "flan: no statement at point")) + (let ((l (flan-fln--statement-lines s))) + (kill-region (car l) (cdr l))))) + +(defun flan-fln--sibling (s dir) + "The statement next to S at its level: before it when DIR is -1, else after." + (let ((ind (flan-fln--indent-at s))) + (if (< dir 0) + (let ((p (flan-fln--prev-code s)) hit) + (while (and p (not hit)) + (setq p (flan-fln--logical-start p)) + (let ((i (flan-fln--indent-at p))) + (cond ((< i ind) (setq p nil)) + ((= i ind) (setq hit (flan-fln--clause-header p))) + (t (setq p (flan-fln--prev-code p)))))) + hit) + (let ((n (flan-fln--next-code (flan-fln--statement-last s)))) + (and n (= (flan-fln--indent-at n) ind) + (not (flan-fln--clause-line-p n)) + n))))) + +(defun flan-fln--swap (a b) + "Swap the whole-line statements A and B, A above B; keep point in its own." + (let* ((la (flan-fln--statement-lines a)) + (lb (flan-fln--statement-lines b)) + (ta (buffer-substring (car la) (cdr la))) + (gap (buffer-substring (cdr la) (car lb))) + (tb (buffer-substring (car lb) (cdr lb))) + (in-b (>= (point) (car lb))) + (off (- (point) (if in-b (car lb) (car la))))) + (goto-char (car la)) + (delete-region (car la) (cdr lb)) + (insert tb gap ta) + (goto-char (+ (car la) off (if in-b 0 (+ (length tb) (length gap))))))) + +(defun flan-fln-move-statement-up () + "Swap the statement at point with the one before it at its level." + (interactive) + (flan-fln--ensure-final-newline) + (let* ((s (flan-fln--statement-start-at (point))) + (p (and s (flan-fln--sibling s -1)))) + (unless p (user-error "flan: no statement above this one at its level")) + (flan-fln--swap p s))) + +(defun flan-fln-move-statement-down () + "Swap the statement at point with the one after it at its level." + (interactive) + (flan-fln--ensure-final-newline) + (let* ((s (flan-fln--statement-start-at (point))) + (n (and s (flan-fln--sibling s 1)))) + (unless n (user-error "flan: no statement below this one at its level")) + (flan-fln--swap s n))) + +(defun flan-fln--owner (pos) + "The line that owns the block POS is in or opens: a header with a body." + (let ((s (flan-fln--statement-start-at pos))) + (cond ((null s) nil) + ((flan-fln--body-bounds (flan-fln--line-statement pos)) + (flan-fln--line-statement pos)) + (t (flan-fln--parent (flan-fln--line-statement pos)))))) + +(defun flan-fln--shift-lines (l delta) + (indent-rigidly (car l) (cdr l) delta)) + +(defun flan-fln-slurp () + "Pull the statement after this block into it, as its last statement." + (interactive) + (flan-fln--ensure-final-newline) + (let* ((o (or (flan-fln--owner (point)) (user-error "flan: no block here"))) + (last (flan-fln--statement-last o t)) + (n (flan-fln--next-code last)) + (body (flan-fln--body-bounds o))) + (unless (and n (= (flan-fln--indent-at n) (flan-fln--indent-at o)) + (not (flan-fln--clause-line-p n))) + (user-error "flan: no statement after this block to pull in")) + (save-excursion + (flan-fln--shift-lines (flan-fln--statement-lines n) + (- (flan-fln--indent-at (car body)) + (flan-fln--indent-at o)))))) + +(defun flan-fln-barf () + "Push this block's last statement out, to follow the block." + (interactive) + (flan-fln--ensure-final-newline) + (let* ((o (or (flan-fln--owner (point)) (user-error "flan: no block here"))) + (body (flan-fln--body-bounds o)) + (ind (flan-fln--indent-at (car body))) + (last (flan-fln--statement-last o t)) + (after (flan-fln--next-code last)) + (child (flan-fln--bol (car body))) c) + (when (and after (= (flan-fln--indent-at after) (flan-fln--indent-at o)) + (flan-fln--clause-line-p after)) + (user-error "flan: a clause follows this block; its last statement cannot leave it")) + (while (setq c (flan-fln--sibling child 1)) (setq child c)) + (when (= (flan-fln--bol child) (flan-fln--bol (car body))) + (user-error "flan: this block has one statement; pushing it out would empty it")) + (save-excursion + (flan-fln--shift-lines (flan-fln--statement-lines child) + (- (flan-fln--indent-at o) ind))))) + +(defun flan-fln-raise-statement () + "Replace the statement that owns this block with the statement at point." + (interactive) + (flan-fln--ensure-final-newline) + (let* ((s (or (flan-fln--statement-start-at (point)) + (user-error "flan: no statement at point"))) + (o (or (flan-fln--parent s) (user-error "flan: at top level"))) + (h (flan-fln--clause-header o)) + (ls (flan-fln--statement-lines s)) + (lh (flan-fln--statement-lines h)) + (text (buffer-substring (car ls) (cdr ls))) + (delta (- (flan-fln--indent-at h) (flan-fln--indent-at s)))) + (goto-char (car lh)) + (delete-region (car lh) (cdr lh)) + (let ((beg (point))) + (insert text) + (indent-rigidly beg (point) delta) + (goto-char beg) + (back-to-indentation)))) + +;;; Font lock + +(defconst flan-fln--name-re "\\([^][ \t\n(){},;\":]+\\)" + "A declared name: a run up to a bracket, a space, or the colon of `x: T'.") + +(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]\\|$\\)") + 1 font-lock-keyword-face) + (,(concat "^\\(fn-?\\)[ \t]+" flan-fln--name-re) + 2 font-lock-function-name-face) + (,(concat "^\\(?:struct\\|data\\|union\\|enum\\)[ \t]+" flan-fln--name-re) + 1 font-lock-type-face) + (,(concat "^\\(?:def\\|once\\|const\\)[ \t]+" flan-fln--name-re) + 1 font-lock-variable-name-face) + ;; The words inside a line: `for i in range(n)', `if c then a else b', a + ;; `where' constraint. + ("[ \t]\\(then\\|else\\|in\\|where\\)[ \t]" 1 font-lock-keyword-face) + ;; The operator words. + ("\\_<\\(and\\|or\\|not\\)\\_>" 1 font-lock-keyword-face) + (,(concat "\\_<" (regexp-opt flan--constants t) "\\_>") + 1 font-lock-constant-face) + ;; A keyword. `x:' is a name with a colon glued on, not one. + ("\\(?:^\\|[][ \t(){},]\\)\\(:[^][ \t\n(){},;\":]+\\)" 1 font-lock-constant-face) + ;; A type: after the `: ' of an annotation and after `-> '. + ("[^ \t\n:]:[ \t]+\\([$a-zA-Z][^][ \t\n(){},;\"=]*\\)" 1 font-lock-type-face) + ("[ \t]->[ \t]+\\([$a-zA-Z][^][ \t\n(){},;\"=]*\\)" 1 font-lock-type-face) + ;; The package half of a qualified name, as `flan-mode' draws it. + ("\\_<\\([a-zA-Z][a-zA-Z0-9!?*+=<>._-]*/\\)" 1 font-lock-type-face) + ("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|string\\|dyn\\|Never\\|Allocator\\|Ptr\\|Option\\|Vec\\|Map\\|C?Fn\\)\\_>" + . font-lock-type-face) + ("\\_<\\$[^][ \t\n(){},;\":]*" . font-lock-type-face) + ;; A character literal, `\c' or `\space'. + ("\\\\\\(?:space\\|newline\\|tab\\|return\\|[^ \t\n]\\)" . font-lock-string-face) + ("\\_<-?[0-9][0-9a-fA-FxX_.]*\\_>" . 'font-lock-number-face)) + "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)) + "Imenu index for `flan-fln-mode'.") + +(defun flan-fln-current-defun-name () + "The name the top-level form at point declares, or nil." + (let ((s (flan-fln--toplevel-start (point)))) + (when s + (save-excursion + (goto-char s) + (and (looking-at (concat "\\(?:fn-?\\|def\\|once\\|const\\|struct\\|data\\|union\\|enum\\)[ \t]+" + flan-fln--name-re)) + (match-string-no-properties 1)))))) + +;;; The mode + +(defvar flan-fln-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map flan-base-mode-map) + (define-key map (kbd "C-c C-c") #'flan-fln-eval-defun) + (define-key map (kbd "C-M-x") #'flan-fln-eval-defun) + (define-key map (kbd "C-x C-e") #'flan-fln-eval-last) + (define-key map (kbd "C-c C-e") #'flan-fln-eval-statement) + (define-key map (kbd "C-c C-n") #'flan-fln-eval-statement-and-next) + (define-key map (kbd "C-c C-s") #'flan-fln-step-defun) + ;; The sentence keys, because a statement is this syntax's sentence. + ;; M-e shadows a global binding of the same key, as any mode's M-e would. + (define-key map (kbd "M-a") #'flan-fln-backward-statement) + (define-key map (kbd "M-e") #'flan-fln-forward-statement) + (define-key map (kbd "M-k") #'flan-fln-kill-statement) + (define-key map (kbd "C-M-u") #'flan-fln-up) + (define-key map (kbd "M-") #'flan-fln-move-statement-up) + (define-key map (kbd "M-") #'flan-fln-move-statement-down) + (define-key map (kbd "M-") #'flan-fln-slurp) + (define-key map (kbd "M-") #'flan-fln-barf) + (define-key map (kbd "M-r") #'flan-fln-raise-statement) + (define-key map (kbd "C-c <") #'flan-fln-shift-left) + (define-key map (kbd "C-c >") #'flan-fln-shift-right) + (define-key map (kbd "DEL") #'flan-fln-dedent-or-delete) + (define-key map [remap yank] #'flan-fln-yank) + map) + "Keymap for `flan-fln-mode'.") + +;;;###autoload +(define-derived-mode flan-fln-mode flan-base-mode "Fln" + "Major mode for Flan in the indented syntax, a .fln file. + +\\{flan-fln-mode-map}" + :syntax-table flan-fln-mode-syntax-table + (setq-local font-lock-defaults '(flan-fln-font-lock-keywords)) + (setq-local indent-line-function #'flan-fln-indent-line) + (setq-local indent-region-function #'flan-fln-indent-region) + ;; Never re-indent the line RET leaves: its column is its meaning. + (setq-local electric-indent-inhibit t) + (add-hook 'post-self-insert-hook #'flan-fln--electric-clause -50 t) + (setq-local beginning-of-defun-function #'flan-fln--beginning-of-defun) + (setq-local end-of-defun-function #'flan-fln--end-of-defun) + ;; Left nil on purpose: C-M-f and C-M-b stay bracket and term motion, and + ;; everything built on sexps keeps meaning brackets. + (setq-local forward-sexp-function nil) + (setq-local parse-sexp-ignore-comments t) + (setq-local comment-use-syntax t) + (setq-local imenu-generic-expression flan-fln-imenu-generic-expression) + (setq-local er/try-expand-list + '(flan-fln-mark-term flan-fln-mark-group flan-fln-mark-statement + flan-fln-mark-clause flan-fln-mark-toplevel)) + (setq-local evil-shift-width flan-fln-indent-offset) + (add-hook 'which-func-functions #'flan-fln-current-defun-name nil t) + (when (and flan-fln-smartparens (require 'smartparens nil t)) + (smartparens-mode 1)) + (flan-fln--smartparens-keys)) + +(defvar smartparens-mode-map) + +(defun flan-fln--smartparens-keys () + "Keep the top-level and up motions this mode's when smartparens is on. +A minor mode's map is looked up before the major mode's, and a common +smartparens setup puts sexp commands on C-M-a, C-M-e and C-M-u. In a .fln +buffer those keys mean forms and blocks, so smartparens gets a map here +that says so and otherwise is its own." + (when (boundp 'smartparens-mode-map) + (let ((map (make-sparse-keymap))) + (set-keymap-parent map smartparens-mode-map) + (define-key map (kbd "C-M-a") #'beginning-of-defun) + (define-key map (kbd "C-M-e") #'end-of-defun) + (define-key map (kbd "C-M-h") #'mark-defun) + (define-key map (kbd "C-M-u") #'flan-fln-up) + (setq-local minor-mode-overriding-map-alist + (cons (cons 'smartparens-mode map) + (assq-delete-all 'smartparens-mode + (copy-sequence minor-mode-overriding-map-alist))))))) + +(with-eval-after-load 'smartparens + ;; `'x' quotes a name and `'(a b)' a list; neither has a closing quote. + (sp-local-pair 'flan-fln-mode "'" nil :actions nil) + (sp-local-pair 'flan-fln-mode "`" nil :actions nil)) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.fln\\'" . flan-fln-mode)) + +;;; Evil + +(defun flan-fln--evil (b type) + ;; A line range is handed over already whole -- from a line's start to the + ;; start of the line after it -- and marked expanded, so Evil neither + ;; stretches it to one more line nor leaves the last newline behind. + (cond ((null b) (error "No object here")) + ((eq type 'line) (evil-range (car b) (cdr b) 'line :expanded t)) + (t (evil-range (car b) (cdr b) type)))) + +(defun flan-fln--whole-lines (b) + "B's lines, from the start of the first to the start of the line after." + (and b (flan-fln--lines (flan-fln--bol (car b)) (flan-fln--bol (cdr b))))) + +(defun flan-fln--empty-line-p (pos) + (save-excursion (goto-char (flan-fln--bol pos)) (looking-at-p "[ \t]*$"))) + +(defun flan-fln--comment-line-p (pos) + (and (flan-fln--blank-p pos) (not (flan-fln--empty-line-p pos)))) + +(defun flan-fln--with-comments (b) + "Whole lines B, and the comments that belong to them. +Above: a comment block at B's own column with no blank line under it. +Below: comment lines deeper than B's column straight after it, which close +its block. A comment at another column belongs to the block it lines up +with." + (and b (save-excursion + (let ((col (flan-fln--indent-at (car b)))) + (goto-char (car b)) + (while (and (zerop (forward-line -1)) + (flan-fln--comment-line-p (point)) + (= (current-indentation) col)) + (setq b (cons (point) (cdr b)))) + (goto-char (cdr b)) + (while (and (not (eobp)) + (flan-fln--comment-line-p (point)) + (> (current-indentation) col)) + (forward-line 1) + (setq b (cons (car b) (point)))) + b)))) + +(defun flan-fln--commented-toplevel (pos) + "The top-level form at POS, whole lines with its comment block. +On a comment block that sits directly on a form, that form." + (let ((pos (save-excursion + (goto-char pos) + (beginning-of-line) + (while (and (flan-fln--comment-line-p (point)) + (zerop (current-indentation)) + (zerop (forward-line 1)))) + (if (flan-fln--toplevel-start-p (point)) (point) pos))) + (orig pos)) + ;; A lone comment -- a blank line away from every form -- belongs to + ;; none, and the form above it is not what was pointed at. + (let ((b (flan-fln--with-comments + (flan-fln--whole-lines (flan-fln--toplevel-bounds pos))))) + (and b + (or (not (flan-fln--comment-line-p orig)) + (and (<= (car b) orig) (< orig (cdr b)))) + b)))) + +(defun flan-fln--with-trailing-blanks (b) + "Whole lines B and the empty lines after them. +When nothing follows -- the last form -- the empty lines before it as well, +as Vim's `dap' does, so the buffer does not end in empty lines. A comment +below a form is not taken: it belongs to what follows." + (and b (save-excursion + (goto-char (cdr b)) + (while (and (not (eobp)) (flan-fln--empty-line-p (point)) + (zerop (forward-line 1)))) + (let ((end (point))) + (if (< end (point-max)) + (cons (car b) end) + (goto-char (car b)) + (while (and (zerop (forward-line -1)) + (flan-fln--empty-line-p (point)))) + (cons (if (flan-fln--empty-line-p (point)) + (point) + (min (car b) (line-beginning-position 2))) + end)))))) + +(defun flan-fln--term-around (b) + "B and the spaces after it, or before it when none follow." + (and b (save-excursion + (goto-char (cdr b)) + (let ((e (progn (skip-chars-forward " \t") (point)))) + (if (> e (cdr b)) + (cons (car b) e) + (goto-char (car b)) + (skip-chars-backward " \t") + (cons (point) (cdr b))))))) + +(with-eval-after-load 'evil + ;; Evaluated here rather than written at top level: the macro is Evil's, and + ;; this file loads and compiles without Evil installed. + (eval + '(progn + (evil-define-text-object flan-fln-inner-term (count &optional _beg _end _type) + "A term." + (flan-fln--evil (flan-fln--term-bounds (point)) 'exclusive)) + (evil-define-text-object flan-fln-a-term (count &optional _beg _end _type) + "A term and the spaces after it." + (flan-fln--evil (flan-fln--term-around (flan-fln--term-bounds (point))) + 'exclusive)) + (evil-define-text-object flan-fln-inner-statement (count &optional _beg _end _type) + "A statement, from its first character to its last." + (flan-fln--evil (bounds-of-thing-at-point 'flan-fln-statement) 'exclusive)) + (evil-define-text-object flan-fln-a-statement (count &optional _beg _end _type) + "A statement's whole lines, with the comment block on it." + (flan-fln--evil (flan-fln--with-comments + (flan-fln--whole-lines + (bounds-of-thing-at-point 'flan-fln-statement))) + 'line)) + (evil-define-text-object flan-fln-inner-body (count &optional _beg _end _type) + "A statement's block, its lines." + (flan-fln--evil (flan-fln--whole-lines + (bounds-of-thing-at-point 'flan-fln-body)) + 'line)) + (evil-define-text-object flan-fln-a-body (count &optional _beg _end _type) + "The whole statement the block belongs to, its lines." + (flan-fln--evil (flan-fln--whole-lines + (bounds-of-thing-at-point 'flan-fln-statement)) + 'line)) + (evil-define-text-object flan-fln-inner-clause (count &optional _beg _end _type) + "A clause's block, its lines; a match arm's value when it is on the line." + (let* ((c (flan-fln--clause-at (point))) + (arm (and c (flan-fln--arm c))) + (v (and arm (plist-get arm :value)))) + (if (and v (= (flan-fln--bol (car v)) c)) + (flan-fln--evil v 'exclusive) + (flan-fln--evil (flan-fln--whole-lines (and c (flan-fln--body-bounds c))) + 'line)))) + (evil-define-text-object flan-fln-a-clause (count &optional _beg _end _type) + "A clause: its line and its block." + (flan-fln--evil (flan-fln--whole-lines + (bounds-of-thing-at-point 'flan-fln-clause)) + 'line)) + (evil-define-text-object flan-fln-inner-toplevel (count &optional _beg _end _type) + "A top-level form, its lines, with the comment block on it." + (flan-fln--evil (flan-fln--commented-toplevel (point)) 'line)) + (evil-define-text-object flan-fln-a-toplevel (count &optional _beg _end _type) + "A top-level form with its comment block, and the empty lines after it." + (flan-fln--evil (flan-fln--with-trailing-blanks + (flan-fln--commented-toplevel (point))) + 'line))) + t) + (evil-define-key* '(operator visual) flan-fln-mode-map + "ie" 'flan-fln-inner-term "ae" 'flan-fln-a-term + "is" 'flan-fln-inner-statement "as" 'flan-fln-a-statement + "ii" 'flan-fln-inner-body "ai" 'flan-fln-a-body + "ik" 'flan-fln-inner-clause "ak" 'flan-fln-a-clause + "id" 'flan-fln-inner-toplevel "ad" 'flan-fln-a-toplevel) + ;; Evil's sentence motions, for the statement ones M-a and M-e are. + (evil-define-key* '(normal motion visual) flan-fln-mode-map + "(" #'flan-fln-backward-statement + ")" #'flan-fln-next-statement) + ;; Normal state's own M- bindings would otherwise win over the mode's. + (evil-define-key* 'normal flan-fln-mode-map + (kbd "M-r") #'flan-fln-raise-statement + (kbd "M-k") #'flan-fln-kill-statement + (kbd "M-") #'flan-fln-move-statement-up + (kbd "M-") #'flan-fln-move-statement-down + (kbd "M-") #'flan-fln-slurp + (kbd "M-") #'flan-fln-barf)) + +(provide 'flan-fln-mode) +;;; flan-fln-mode.el ends here diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 3c5b9080..1efff081 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -439,19 +439,16 @@ For `syntax-propertize-function'." ;; not. Either way forward, which is the whole of why this terminates. (goto-char (or fin from))))) -(defvar flan-mode-map +;; The keys both syntaxes share: everything that talks to the running program +;; about a name, a value or the session rather than about a piece of the text. +;; The keys that pick text out of the buffer -- which form C-c C-c means -- +;; are each child mode's own, because what a form is differs between them. +(defvar flan-base-mode-map (let ((map (make-sparse-keymap))) ;; Autoloaded from flan.el, so the client loads on first use. - (define-key map (kbd "C-c C-c") #'flan-eval-defun) - ;; The same command on the binding SLIME and CIDER put it on. Emacs binds - ;; C-M-x to eval-defun only in `emacs-lisp-mode-map', so a mode derived - ;; from `lisp-mode' inherits nothing and the key is undefined — which - ;; reads as the client being broken rather than as the key being free. - (define-key map (kbd "C-M-x") #'flan-eval-defun) (define-key map (kbd "C-c C-k") #'flan-eval-buffer) ;; The stepper: the defn at point, installed to stop before each form. (define-key map (kbd "C-c C-s") #'flan-step-defun) - (define-key map (kbd "C-x C-e") #'flan-eval-last-sexp) (define-key map (kbd "C-c C-z") #'flan-connect) (define-key map (kbd "C-c C-q") #'flan-disconnect) (define-key map (kbd "C-c C-d") #'flan-describe) @@ -502,23 +499,45 @@ For `syntax-propertize-function'." ;; because it is the one that works from any state. (define-key map (kbd "C-c C-M-x") #'flan-rerun) map) + "Keymap for every Flan source buffer, `flan-mode' and `flan-fln-mode'.") + +(defvar flan-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map flan-base-mode-map) + (define-key map (kbd "C-c C-c") #'flan-eval-defun) + ;; The same command on the binding SLIME and CIDER put it on. Emacs binds + ;; C-M-x to eval-defun only in `emacs-lisp-mode-map', so a mode derived + ;; from `lisp-mode' inherits nothing and the key is undefined — which + ;; reads as the client being broken rather than as the key being free. + (define-key map (kbd "C-M-x") #'flan-eval-defun) + (define-key map (kbd "C-x C-e") #'flan-eval-last-sexp) + map) "Keymap for `flan-mode'.") +;; The parent of both source modes. Everything the dev loop asks of a buffer +;; -- is this Flan, set up eldoc and completion, draw the program's names, +;; paint watched values -- asks it of this mode, so a .fln buffer gets it the +;; same way a .flan buffer does. What each syntax reads as a form is its +;; child's business. +(define-derived-mode flan-base-mode prog-mode "Flan" + "Parent mode of the Flan source modes, `flan-mode' and `flan-fln-mode'." + (setq-local comment-start ";") + (setq-local comment-start-skip ";+ *") + (setq-local comment-add 1) + ;; Spaces. The whole corpus is written with them, and alignment that is + ;; correct here is alignment under a specific *column* — a tab makes that + ;; depend on a setting the file cannot carry. In a .fln file a tab in the + ;; indentation is an error besides. + (setq-local indent-tabs-mode nil)) + ;;;###autoload -(define-derived-mode flan-mode prog-mode "Flan" +(define-derived-mode flan-mode flan-base-mode "Flan" "Major mode for editing Flan. \\{flan-mode-map}" :syntax-table flan-mode-syntax-table - (setq-local comment-start ";") - (setq-local comment-start-skip ";+ *") - (setq-local comment-add 1) (setq-local font-lock-defaults '(flan-font-lock-keywords)) (setq-local indent-line-function #'lisp-indent-line) - ;; Spaces. The whole corpus is written with them, and alignment that is - ;; correct here is alignment under a specific *column* — a tab makes that - ;; depend on a setting the file cannot carry. - (setq-local indent-tabs-mode nil) (setq-local lisp-indent-function #'flan-indent-function) (setq-local outline-regexp ";;;;+[ \t]*") (setq-local imenu-generic-expression flan-imenu-generic-expression) @@ -800,6 +819,12 @@ decision to `calculate-lisp-indent'." ;;;###autoload (add-to-list 'auto-mode-alist '("\\.flan\\'" . flan-mode)) +;; The indented syntax's mode lives in its own file; a buffer of it is the first +;; thing that loads it. +;;;###autoload +(autoload 'flan-fln-mode "flan-fln-mode" nil t) +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.fln\\'" . flan-fln-mode)) ;;; The other Flan buffers under Evil diff --git a/emacs/flan-watch.el b/emacs/flan-watch.el index d0701a0a..4eff5e7e 100644 --- a/emacs/flan-watch.el +++ b/emacs/flan-watch.el @@ -249,15 +249,19 @@ inline it has no modeline beside it to say so.") (let (bufs) (dolist (w (window-list-1 nil 'nomini t)) (let ((b (window-buffer w))) - (when (and (eq (buffer-local-value 'major-mode b) 'flan-mode) + (when (and (provided-mode-derived-p (buffer-local-value 'major-mode b) + 'flan-base-mode) (not (memq b bufs))) (push b bufs)))) bufs)) (defun flan-watch--ghost-sites () "Watch call sites in the current buffer, as a list of (NAME . END-OF-LINE)." - (let ((re (concat "(\\s-*" flan-watch-ghost-call-regexp - "\\s-+\"\\([^\"\n]*\\)\"")) + ;; Either syntax: `(watch "name" v)' in a .flan file, `watch("name", v)' in + ;; a .fln one, where the call is the name glued to its parenthesis. + (let ((re (concat "\\(?:(\\s-*\\(?:" flan-watch-ghost-call-regexp "\\)\\s-+" + "\\|\\_<\\(?:" flan-watch-ghost-call-regexp "\\)(\\s-*\\)" + "\"\\([^\"\n]*\\)\"")) (sites nil)) (save-excursion (goto-char (point-min)) diff --git a/emacs/flan.el b/emacs/flan.el index ad175078..6f5f576e 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -986,7 +986,7 @@ refuses: callers cannot silently discard a running program's state." ;; buffer is not what you meant — and it still only changes what is asked, ;; never which file the unasked case picks. (let ((file (and buffer-file-name - (string-suffix-p ".flan" buffer-file-name) + (string-match-p "\\.fla?n\\'" buffer-file-name) (expand-file-name buffer-file-name)))) (list (if (and file (not current-prefix-arg)) file @@ -1176,7 +1176,7 @@ the state with something to answer in it." (defun flan-mode-line () "The Flan connection indicator, for `mode-line-misc-info'." - (when (derived-mode-p 'flan-mode 'flan-repl-mode) + (when (derived-mode-p 'flan-base-mode 'flan-repl-mode) (pcase (flan-state) ;; First, and it names the condition: a stopped program looks exactly ;; like a running one from anywhere else in Emacs, and the whole reason @@ -2141,7 +2141,7 @@ Leaves its face in `flan--dynamic-face' for the rule that calls this." (defun flan--dynamic-install () "Add or remove the dynamic rules in the current buffer, and redraw it. Called for its effect on one buffer; `flan--dynamic-sync' does every buffer." - (when (derived-mode-p 'flan-mode) + (when (derived-mode-p 'flan-base-mode) ;; Removed first in both branches, because adding is not idempotent: a ;; second install would put the rule in twice and every refresh after that ;; would add another. @@ -2160,7 +2160,7 @@ Called for its effect on one buffer; `flan--dynamic-sync' does every buffer." ;; A file opened while a session is already up: the two moments the table is ;; rebuilt are both in the past by then, so the buffer has to ask on its way in. -(add-hook 'flan-mode-hook #'flan--dynamic-install) +(add-hook 'flan-base-mode-hook #'flan--dynamic-install) (defun flan--forget-defs () "Drop what is known about the program's names." @@ -2447,14 +2447,14 @@ someone editing Flan with no program running and this file never loaded." (setq-local mode-line-misc-info (append mode-line-misc-info '((:eval (flan-mode-line))))))) -(add-hook 'flan-mode-hook #'flan-setup) +(add-hook 'flan-base-mode-hook #'flan-setup) ;; Buffers that were already in flan-mode when this file loaded: the client is ;; autoloaded on first use, so by the time it arrives the file being edited has ;; long since had its mode hooks run. (dolist (b (buffer-list)) (with-current-buffer b - (when (derived-mode-p 'flan-mode) (flan-setup)))) + (when (derived-mode-p 'flan-base-mode) (flan-setup)))) ;;; Evaluating @@ -2667,7 +2667,8 @@ columns already were, because a top-level form starts at column 1." .fln file, the paren reader's for anything else. Sent explicitly because the daemon cannot tell from `:file' — an expansion shown in parens is sent back under the name of the .fln file it came from." - (if (and buffer-file-name (string-suffix-p ".fln" buffer-file-name)) + (if (or (derived-mode-p 'flan-fln-mode) + (and buffer-file-name (string-suffix-p ".fln" buffer-file-name))) "indented" "paren")) diff --git a/emacs/test-flan-cider.el b/emacs/test-flan-cider.el index 16f65fc8..c158bdbb 100644 --- a/emacs/test-flan-cider.el +++ b/emacs/test-flan-cider.el @@ -2034,6 +2034,11 @@ stopped program, which is the case where it should fire." (file-name-directory load-file-name)) nil t) +;; The .fln mode: its objects, keys, indentation and text objects, from text. +(load (expand-file-name "test-flan-fln.el" + (file-name-directory load-file-name)) + nil t) + ;; Ghost text, which is the same kind of thing: rows in, overlays out, and the ;; buffer it reads is a fixture like any other reply here. Loaded for the same ;; reason. diff --git a/emacs/test-flan-fln-live.el b/emacs/test-flan-fln-live.el new file mode 100644 index 00000000..078ded7c --- /dev/null +++ b/emacs/test-flan-fln-live.el @@ -0,0 +1,250 @@ +;;; test-flan-fln-live.el --- The .fln keys against a real daemon -*- lexical-binding: t; -*- + +;; Loaded by test-flan.el, near its end, with `flan-command' already the +;; compiler under test. test-flan-fln.el checks which text each key picks; +;; this checks the reader accepts that text as a whole form, on both backends: +;; a daemon of its own on a .fln file with no main, once on x86 and once on +;; LLVM, each key sent at least once, and a pause mark the daemon must find. + +;;; Code: + +(require 'flan-fln-mode) + +(declare-function test-flan--check "test-flan" (name ok)) +(declare-function test-flan--result "test-flan" ()) +(defvar test-flan-fln-live-dir) +(defvar test-flan-fln-live-socket) + +(defconst test-flan-fln-live--program + "fn fib(n: i64) -> i64 + if n < 2 + n + else + fib(n - 1) + fib(n - 2) + +fn twice(n: i64) -> i64 = n * 2 + +fn total(n: i32) -> i32 + let t = 0 + for i in range(n) + t = t + i + t + +fn sign(n: i64) -> i64 + if n < 0 + -1 + elif n == 0 + 0 + else + 1 + +enum Dir + north + south + east + +fn pick(d: Dir) -> i64 + match d + :north -> 10 + :south -> 20 + :east -> 1 + + 2 + _ -> + twice(3) + +comment(): + if 1 < 2 and + 3 < 4 + twice(1) + elif 1 > 2 or + 3 > 4 + 0 + twice(4) + if 2 > 1 + twice(2) + else + 0 + let x = 3 + twice(x) + 1 +") + +(defun test-flan-fln-live--run (backend args) + (let* ((sock (concat test-flan-fln-live-socket "-fln-" backend)) + (file (expand-file-name (format "fln-live-%s.fln" backend) + test-flan-fln-live-dir)) + (flan-daemon-args args) + (name (lambda (s) (format "%s: %s" backend s))) + (value (lambda (code) + (plist-get (flan--request + (list :op "eval-expr" :code code :file "")) + :value))) + (goto (lambda (needle &optional after) + (goto-char (point-min)) + (search-forward needle) + (unless after (goto-char (match-beginning 0))))) + (shows (lambda (v) + (let ((r (test-flan--result))) + (prog1 (and r (string-match-p (concat "=> " (regexp-quote v) "\\'") + (string-trim r))) + (flan-clear-result)))))) + (with-temp-file file (insert test-flan-fln-live--program)) + (ignore-errors (delete-file sock)) + (flan file sock) + (test-flan--check (funcall name "a daemon starts on a .fln file") + (process-live-p flan--connection)) + (unwind-protect + (with-current-buffer (find-file-noselect file) + (test-flan--check (funcall name "which opens in flan-fln-mode") + (eq major-mode 'flan-fln-mode)) + + ;; C-c C-c, from inside a fn changed in the buffer. + (funcall goto "n * 2") + (delete-char 5) + (insert "n * 3") + (flan-fln-eval-defun) + (test-flan--check (funcall name "C-c C-c installs the fn at point") + (equal (funcall value "(twice 7)") "21")) + + ;; C-x C-e at the end of a column-0 declaration. + (funcall goto "n * 3") + (delete-char 5) + (insert "n * 4") + (flan-fln-eval-last) + (test-flan--check (funcall name "C-x C-e at the end of a column-0 fn installs it") + (equal (funcall value "(twice 7)") "28")) + + ;; C-x C-e at the end of an inner statement: text from column 3. + (funcall goto "twice(4)" t) + (flan-fln-eval-last) + (test-flan--check (funcall name "C-x C-e sends the statement ending at point") + (funcall shows "16")) + + ;; ...and inside a line, the term before point. + (funcall goto "twice(4)") + (let ((at (point))) + (insert "fib(10) + ") + (goto-char (+ at (length "fib(10)"))) + (flan-fln-eval-last) + (test-flan--check (funcall name "C-x C-e inside a line sends the term before point") + (funcall shows "55")) + (delete-region at (+ at (length "fib(10) + ")))) + + ;; C-c C-e on a clause: the whole if, from column 3, clauses and all. + (funcall goto "else\n 0") + (flan-fln-eval-statement) + (test-flan--check (funcall name "C-c C-e on a clause sends its if, and it reads") + (funcall shows "8")) + + ;; A bare let: it and the rest of its block, which is its scope. + (funcall goto "let x = 3") + (flan-fln-eval-statement) + (test-flan--check (funcall name "C-c C-e on a bare let sends its scope with it") + (funcall shows "13")) + + ;; A region of several statements reads as one (do ...). + (funcall goto "twice(4)") + (transient-mark-mode 1) + (set-mark (point)) + (funcall goto "else\n 0" t) + (flan-fln-eval-statement) + (test-flan--check (funcall name "C-c C-e on a region of statements evaluates them in order") + (funcall shows "8")) + + ;; C-c C-n sends and moves on. + (funcall goto "twice(4)") + (flan-fln-eval-statement-and-next) + (test-flan--check (funcall name "C-c C-n sends the statement") + (funcall shows "16")) + (test-flan--check (funcall name "and moves to the next") + (looking-at "if 2 > 1")) + + ;; 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 + ;; a column off to show the answer can be no. + ;; C-x C-e on an arm's value, and on a condition line. + (funcall goto ":south -> 20" t) + (flan-fln-eval-last) + (test-flan--check (funcall name "C-x C-e at the end of a match arm evaluates its value") + (funcall shows "20")) + (funcall goto ":east -> 1 +" t) + (flan-fln-eval-last) + (test-flan--check (funcall name "C-x C-e on an arm's wrapped value evaluates all of it") + (funcall shows "3")) + (funcall goto "if 1 < 2 and" t) + (flan-fln-eval-last) + (test-flan--check (funcall name "C-x C-e on a wrapped condition evaluates all of it") + (funcall shows "true")) + ;; An error on the wrapped line of a condition or value cut out + ;; mid-line is reported where it is in the buffer, column and all. + (let ((refused + (lambda (needle bad fix) + (funcall goto needle t) + (let ((line (1+ (line-number-at-pos))) col) + (save-excursion + (forward-line 1) + (search-forward fix (line-end-position)) + (replace-match bad t t) + (setq col (1+ (- (point) (line-beginning-position) + (length (car (last (split-string bad " ")))))))) + (prog1 (list (condition-case err (progn (flan-fln-eval-last) nil) + (user-error (error-message-string err))) + (format ":%d:%d)" line col)) + (save-excursion + (goto-char (point-min)) + (search-forward bad) + (replace-match fix t t))))))) + (pcase-dolist (`(,what ,needle ,bad ,fix) + '(("an if condition" "if 1 < 2 and" "3 < 4 4" "3 < 4") + ("an elif condition" "elif 1 > 2 or" "3 > 4 4" "3 > 4") + ("an arm's value" ":east -> 1 +" "2 2" "2"))) + (let ((r (funcall refused needle bad fix))) + (test-flan--check + (funcall name (format "an error on the wrapped line of %s is reported at its column" what)) + (and (car r) (string-suffix-p (cadr r) (car r)))) + (unless (and (car r) (string-suffix-p (cadr r) (car r))) + (message " want ...%s\n got %S" (cadr r) (car r)))))) + (flan-clear-errors) + (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") + (funcall shows "true")) + (dolist (c '(("n - 1)" "a call, from its name") + ("elif n" "an elif, at its condition") + ("else\n 1" "an else, at its block") + (":south -> 20" "a match arm, at its value") + ("_ ->" "a match arm, at its block") + ("n < 2" "an if statement") + ("t = 0" "a let") + ("i in range" "a for") + ("+ i" "an assignment"))) + (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" + (cadr c))) + (plist-get reply :pause))) + (flan-fln-eval-defun)) + (test-flan--check (funcall name "and a plain C-c C-c takes the mark down") + (null (flan--pause-overlays))) + (funcall goto "fib(n - 1)") + (let* ((b (flan-fln--toplevel-bounds (point))) + (off (condition-case nil + (plist-get (flan--eval (flan--text (car b) (cdr b)) "defn" + nil nil (cons (1+ (point)) (+ 3 (point)))) + :pause) + (user-error nil)))) + (test-flan--check (funcall name "a position one column off the call is not taken") + (null off))) + (flan-fln-eval-defun) + (set-buffer-modified-p nil) + (kill-buffer)) + ;; Stopped whatever happened above: a daemon this started is its own to end. + (flan-quit)) + (ignore-errors (delete-file sock)) + (ignore-errors (delete-file file)))) + +(message "\nthe .fln keys, against a daemon on each backend") +(test-flan-fln-live--run "x86" nil) +(test-flan-fln-live--run "llvm" '("--llvm")) + +;;; test-flan-fln-live.el ends here diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el new file mode 100644 index 00000000..cd114184 --- /dev/null +++ b/emacs/test-flan-fln.el @@ -0,0 +1,952 @@ +;;; test-flan-fln.el --- The .fln mode, from written-out text -*- lexical-binding: t; -*- + +;; Loaded by test-flan-cider.el, which runs under `dune test', for the reason +;; test-flan-mode.el is: `emacs/*.el' is already that stanza's dependency. +;; Nothing here needs a daemon; what the daemon makes of what these commands +;; send is test-flan-fln-live.el's, run from test-flan.el. +;; +;; Every snippet is text; each check says where point is with a `|' written +;; into it, which is removed before the check runs. + +;;; Code: + +(require 'flan-fln-mode) +(require 'flan) + +(declare-function test-flan--check "test-flan-cider" (name ok)) + +(defmacro test-flan-fln--in (text &rest body) + "Run BODY in a .fln buffer holding TEXT, point where TEXT has its `|'." + (declare (indent 1)) + `(with-temp-buffer + (insert ,text) + (flan-fln-mode) + (goto-char (point-min)) + (when (search-forward "|" nil t) + (delete-char -1)) + ,@body)) + +(defun test-flan-fln--text (b) + (and b (cdr b) (buffer-substring-no-properties (car b) (cdr b)))) + +(defun test-flan-fln--is (name got want) + (test-flan--check name (equal got want)) + (unless (equal got want) + (message " want %S\n got %S" want got))) + +(defun test-flan-fln--thing (thing) + (test-flan-fln--text (bounds-of-thing-at-point thing))) + +(message "\nthe .fln mode") + +;;; One parent + +(test-flan--check "flan-mode is a flan-base-mode" + (provided-mode-derived-p 'flan-mode 'flan-base-mode)) +(test-flan--check "flan-fln-mode is a flan-base-mode" + (provided-mode-derived-p 'flan-fln-mode 'flan-base-mode)) +(test-flan--check ".fln opens in flan-fln-mode" + (eq (cdr (assoc "\\.fln\\'" auto-mode-alist)) 'flan-fln-mode)) +(test-flan-fln--in "fn f() -> i32 = 1\n" + (test-flan--check "a .fln buffer sends the indented syntax" + (equal (flan--syntax) "indented")) + (test-flan--check "and gets the client's completion, as a .flan one does" + (memq #'flan-completion-at-point completion-at-point-functions)) + (test-flan--check "and the modeline indicator" + (member '(:eval (flan-mode-line)) mode-line-misc-info)) + (test-flan--check "the shared keys reach it through the parent's map" + (eq (key-binding (kbd "C-c C-b")) 'flan-cnr-show)) + (test-flan--check "and its own keys pick .fln forms" + (and (eq (key-binding (kbd "C-c C-c")) 'flan-fln-eval-defun) + (eq (key-binding (kbd "C-x C-e")) 'flan-fln-eval-last)))) +(require 'flan-watch) +(let ((b (generate-new-buffer "ghost.fln"))) + (with-current-buffer b (flan-fln-mode)) + (switch-to-buffer b) + (test-flan--check "watch paints ghost text in a shown .fln buffer" + (memq b (flan-watch--ghost-buffers))) + (with-current-buffer b + (insert "fn f() -> ()\n watch-i64(\"x\", 1)\n") + (test-flan--check "and finds a watch call written as a .fln call" + (equal (mapcar #'car (flan-watch--ghost-sites)) '("x")))) + (kill-buffer b)) +(require 'flan-dape) +(test-flan--check "dape offers its config in any Flan buffer" + (equal (plist-get flan-dape-config 'modes) '(flan-base-mode))) +(let ((buffer-file-name "/tmp/x.fln")) + (test-flan--check "and debugs the .fln file it was started from" + (equal (flan-dape--source) "/tmp/x.fln"))) + +;;; The objects + +(defconst test-flan-fln--settle + "fn settle(row: i32, col: i32) -> () + let vel = f32(gravity) + velocity[row, col] + while y > row + if 0 == grid[y, col] + grid[y, col] = grid[row, col] + ; a comment inside the body + + return + let left? = col > 0 + if left? or right? + let side = + if not left? + 1 + elif not right? + -1 + else + if f32(rand()) < 0.5 then 1 else -1 + grid[y, col + side] = grid[row, col] + y = y - 1 + velocity[row, col] = 0.0 + + ; trailing comment, not part of the function + +fn step() -> () + paint-at(i32(m.y) / cell-size, + i32(m.x) / cell-size) + if r >= 0 and r < rows - 1 + and c >= 0 + grid[r, c] = 1 + step() +") + +(defun test-flan-fln--at (text needle) + "TEXT with a `|' before the first NEEDLE." + (let ((i (string-search needle text))) + (concat (substring text 0 i) "|" (substring text i)))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "if 0 == grid") + (test-flan-fln--is "a statement takes its body, through blank and comment lines" + (test-flan-fln--thing 'flan-fln-statement) + "if 0 == grid[y, col] + grid[y, col] = grid[row, col] + ; a comment inside the body + + return") + (test-flan-fln--is "its body is the lines under its first" + (test-flan-fln--thing 'flan-fln-body) + "grid[y, col] = grid[row, col] + ; a comment inside the body + + return")) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "elif not right") + (test-flan-fln--is "on a clause, the statement is its header's, clauses and all" + (test-flan-fln--thing 'flan-fln-statement) + "if not left? + 1 + elif not right? + -1 + else + if f32(rand()) < 0.5 then 1 else -1") + (test-flan-fln--is "and the clause is its own line and block" + (test-flan-fln--thing 'flan-fln-clause) + "elif not right? + -1")) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "1\n elif") + (test-flan-fln--is "a clause is not found from the header's own block" + (test-flan-fln--thing 'flan-fln-clause) nil)) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "if f32(rand())") + (test-flan-fln--is "from inside else's block, the clause is the else" + (test-flan-fln--thing 'flan-fln-clause) + "else + if f32(rand()) < 0.5 then 1 else -1")) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "let side =") + (test-flan-fln--is "let x = with the value as a block" + (test-flan-fln--thing 'flan-fln-statement) + "let side = + if not left? + 1 + elif not right? + -1 + else + if f32(rand()) < 0.5 then 1 else -1")) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "i32(m.x)") + (test-flan-fln--is "a line inside a bracket is part of its statement" + (test-flan-fln--thing 'flan-fln-statement) + "paint-at(i32(m.y) / cell-size, + i32(m.x) / cell-size)") + (test-flan-fln--is "a term is glued, brackets and all" + (test-flan-fln--thing 'flan-fln-term) "i32(m.x)") + (test-flan-fln--is "a group is a bracket pair" + (test-flan-fln--thing 'flan-fln-group) + "(i32(m.y) / cell-size, + i32(m.x) / cell-size)")) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "and c >= 0") + (test-flan-fln--is "an operator continuation line is part of its statement" + (test-flan-fln--thing 'flan-fln-statement) + "if r >= 0 and r < rows - 1 + and c >= 0 + grid[r, c] = 1") + (test-flan-fln--is "and not the start of the body" + (test-flan-fln--thing 'flan-fln-body) "grid[r, c] = 1")) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "velocity[row, col] = 0.0") + (test-flan-fln--is "a top-level form ends before trailing comment lines" + (test-flan-fln--thing 'flan-fln-toplevel) + (substring test-flan-fln--settle 0 + (+ (string-search "= 0.0" test-flan-fln--settle) 5)))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "trailing comment") + (test-flan--check "a comment between forms belongs to the form above" + (string-prefix-p "fn settle" + (test-flan-fln--thing 'flan-fln-toplevel)))) + +(test-flan-fln--in "; a header comment\n|\nfn f() -> i32 = 1\n" + (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--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))) + "fnx() -> i32 = 1")) + +(test-flan-fln--in "x = 1\nhandler-case\n f()\non E(c)\n nil\n|restart y\n" + (test-flan--check "on and restart at column 0 are clauses, not forms" + (progn (beginning-of-defun) + (looking-at "handler-case")))) + +(test-flan-fln--in "let on = 3\nfoo(x):|\n bar()\n" + (test-flan-fln--is "a term ends before the trailing colon of a call's block" + (test-flan-fln--text (flan-fln--term-before (point))) "foo(x)")) + +(test-flan-fln--in "f(\\(, \\) , x.y)|\n" + (test-flan-fln--is "a character literal is not a bracket" + (test-flan-fln--text (flan-fln--term-before (point))) + "f(\\(, \\) , x.y)")) + +;;; Top-level motion + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle " y = y - 1") + (beginning-of-defun) + (test-flan--check "C-M-a goes to the form's first line" (looking-at "fn settle")) + (end-of-defun) + (test-flan--check "C-M-e goes past its last code line, not its trailing comment" + (save-excursion (forward-line -1) + (looking-at " velocity\\[row, col\\] = 0.0"))) + (end-of-defun) + (test-flan--check "and the next C-M-e ends the next form" + (= (point) (point-max))) + (goto-char (point-max)) + (beginning-of-defun) + (test-flan--check "C-M-a from the end reaches the last form" (looking-at "fn step")) + (mark-defun) + (test-flan--check "C-M-h marks the form" + (let ((m (buffer-substring (region-beginning) (region-end)))) + (and (string-prefix-p "fn step" (string-trim-left m "\n")) + (string-suffix-p " step()\n" m))))) + +;;; Statement motion + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle " let left?") + (flan-fln-backward-statement) + (test-flan--check "M-a at a statement's start goes to the one before at its level" + (looking-at "if 0 == grid")) + (flan-fln-backward-statement) + (test-flan--check "and out to the owner when there is none" (looking-at "while y")) + (flan-fln-forward-statement) + (test-flan--check "M-e goes to the end of the statement, body and all" + (looking-back "y = y - 1" (line-beginning-position))) + (flan-fln-forward-statement) + (test-flan--check "and again, to the end of the next" + (looking-back "= 0.0" (line-beginning-position)))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle " -1") + (flan-fln-up) + (test-flan--check "C-M-u goes to the line that owns the block" + (looking-at "elif not right")) + (flan-fln-up) + (test-flan--check "and from there to its owner's" (looking-at "let side"))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "m.x)") + (flan-fln-up) + (test-flan--check "C-M-u inside a bracket goes to the bracket" (looking-at "(m.x)"))) + +;;; What each key sends + +;; The request, captured where it leaves: the daemon's answer is the live +;; test's business, and here what matters is which text went out and where +;; it said the text starts. +(defvar test-flan-fln--sent nil) + +(defmacro test-flan-fln--sending (&rest body) + `(progn + (setq test-flan-fln--sent nil) + (cl-letf (((symbol-function 'flan--request) + (lambda (form) (push form test-flan-fln--sent) + (list :status "ok" :value "0"))) + ((symbol-function 'pulse-momentary-highlight-region) #'ignore)) + ,@body) + (car test-flan-fln--sent))) + +(defun test-flan-fln--sent-code (req) + (plist-get req :code)) + +(defconst test-flan-fln--prog + "fn fib(n: i64) -> i64 + if n < 2 + n + else + fib(n - 1) + fib(n - 2) + +comment(): + twice(4) + let x = 3 + if x > 2 + twice(x) + else + 0 + +fn twice(n: i64) -> i64 = n * 2 +") + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "fib(n - 1)") + (test-flan--check "C-c C-s is the .fln stepper" + (eq (key-binding (kbd "C-c C-s")) 'flan-fln-step-defun)) + (let ((r (test-flan-fln--sending (flan-fln-step-defun)))) + (test-flan--check "which installs the fn at point to step through" + (and (equal (plist-get r :op) "eval") + (eq (plist-get r :step) t) + (string-prefix-p "fn fib" (test-flan-fln--sent-code r)) + (string-suffix-p "fib(n - 2)" (test-flan-fln--sent-code r)))))) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "twice(4)") + (test-flan--check "and refuses what is not a declaration" + (condition-case nil + (progn (test-flan-fln--sending (flan-fln-step-defun)) nil) + (user-error t)))) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "fib(n - 1)") + (let ((r (test-flan-fln--sending (flan-fln-eval-defun)))) + (test-flan--check "C-c C-c inside a fn installs the whole fn" + (and (equal (plist-get r :op) "eval") + (string-suffix-p "fib(n - 1) + fib(n - 2)" + (test-flan-fln--sent-code r)) + (string-prefix-p "fn fib" (test-flan-fln--sent-code r)) + (equal (plist-get r :syntax) "indented"))))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "twice(4)") + (let ((r (test-flan-fln--sending (flan-fln-eval-defun)))) + (test-flan--check "C-c C-c on a column-0 call evaluates it" + (equal (plist-get r :op) "eval-expr")))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "\n let x") + (let ((r (test-flan-fln--sending (flan-fln-eval-last)))) + (test-flan--check "C-x C-e at a line's end sends the statement ending there" + (and (equal (plist-get r :op) "eval-expr") + (equal (test-flan-fln--sent-code r) "twice(4)") + (equal (plist-get r :line) 8) + (equal (plist-get r :col) 3))))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "\n else\n 0") + (let ((r (test-flan-fln--sending (flan-fln-eval-last)))) + (test-flan--check "the innermost one: the last line of a block, not the if" + (equal (test-flan-fln--sent-code r) "twice(x)")))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "(4)") + (let ((r (test-flan-fln--sending (flan-fln-eval-last)))) + (test-flan--check "C-x C-e inside a line sends the term before point" + (and (equal (test-flan-fln--sent-code r) "twice") + (equal (plist-get r :col) 3))))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "\n\ncomment") + (let ((r (test-flan-fln--sending (flan-fln-eval-last)))) + (test-flan--check "at the end of a fn's last line, the innermost statement, not the fn" + (equal (test-flan-fln--sent-code r) "fib(n - 1) + fib(n - 2)")))) + +(test-flan-fln--in test-flan-fln--prog + (goto-char (point-max)) + (skip-chars-backward "\n") + (let ((r (test-flan-fln--sending (flan-fln-eval-last)))) + (test-flan--check "a column-0 one-line fn at its end is installed" + (and (equal (plist-get r :op) "eval") + (string-suffix-p "fn twice(n: i64) -> i64 = n * 2" + (test-flan-fln--sent-code r)))))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "else\n 0") + (let ((r (test-flan-fln--sending (flan-fln-eval-statement)))) + (test-flan--check "C-c C-e on a clause sends its whole statement" + (and (equal (test-flan-fln--sent-code r) + "if x > 2\n twice(x)\n else\n 0") + (equal (plist-get r :line) 10) + (equal (plist-get r :col) 3))))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "let x = 3") + (let ((r (test-flan-fln--sending (flan-fln-eval-statement)))) + (test-flan--check "C-c C-e on a bare let sends it with the rest of its block" + (equal (test-flan-fln--sent-code r) + "let x = 3\n if x > 2\n twice(x)\n else\n 0")))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "twice(4)") + (let ((r (test-flan-fln--sending (flan-fln-eval-statement-and-next)))) + (test-flan--check "C-c C-n sends the statement" + (equal (test-flan-fln--sent-code r) "twice(4)")) + (test-flan--check "and moves to the next" (looking-at "let x = 3")))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "twice(4)") + (transient-mark-mode 1) + (set-mark (point)) + (search-forward "twice(x)") + (forward-char -3) + (let ((r (test-flan-fln--sending (flan-fln-eval-statement)))) + (test-flan--check "C-c C-e sends the region's whole lines" + (equal (test-flan-fln--sent-code r) + "twice(4)\n let x = 3\n if x > 2\n twice(x)")))) + +;; The pause target. The position sent is where the reader starts the form, +;; which for a call is its name and not its parenthesis; the live test checks +;; the daemon finds a form there. +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "n - 1)") + (let ((r (test-flan-fln--sending (flan-fln-eval-defun '(4))))) + (test-flan-fln--is "C-u C-c C-c in a call marks the call, from its name" + (plist-get r :pause) '(5 5)))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "< 2") + (let ((r (test-flan-fln--sending (flan-fln-eval-defun '(4))))) + (test-flan-fln--is "outside a bracket, the statement on point's line" + (plist-get r :pause) '(2 3)))) + +(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "n < 2") + (let ((r (test-flan-fln--sending (flan-fln-eval-defun '(16))))) + (test-flan-fln--is "C-u C-u, the fn: stop on entry" + (plist-get r :pause) '(1 1)))) + +(test-flan-fln--in "fn f(x: i64) -> i64\n g((x| + 1), [x 2])\n" + (let ((r (test-flan-fln--sending (flan-fln-eval-defun '(4))))) + (test-flan-fln--is "a free parenthesis marks the value inside it" + (plist-get r :pause) '(2 6)))) + +;;; Match arms, header lines, clauses, comments + +(defconst test-flan-fln--arms + "fn pick(n: i64) -> i64 + let r = match n + 0 -> 10 + 1 -> + twice(1) + twice(2) + k -> k + 1 + if n < 0 + -1 + elif n == 0 + or n == 1 + 0 + else + r + handler-case + f() + on Error(e) + nil + ; a comment, twice(9) + r + +comment: + twice(4) +") + +(defun test-flan-fln--last-at (needle &optional fn) + "What FN, C-x C-e by default, sends with point at the end of NEEDLE's line." + (test-flan-fln--in (test-flan-fln--at test-flan-fln--arms needle) + (end-of-line) + (let ((r (test-flan-fln--sending (funcall (or fn #'flan-fln-eval-last))))) + (and r (list (plist-get r :op) (test-flan-fln--sent-code r)))))) + +(test-flan-fln--is "C-x C-e at the end of a match arm sends its value" + (test-flan-fln--last-at "0 -> 10") '("eval-expr" "10")) +(test-flan-fln--is "at the end of an arm with a block, the block" + (test-flan-fln--last-at "1 ->") + '("eval-expr" "twice(1)\n twice(2)")) +(test-flan-fln--is "an arm whose pattern binds a name sends the whole match" + (cadr (test-flan-fln--last-at "k -> k")) + (substring test-flan-fln--arms (string-search "let r" test-flan-fln--arms) + (+ (string-search "k + 1" test-flan-fln--arms) 5))) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--arms "twice(2)") + (test-flan-fln--is "C-c C-e on an arm's block sends the block, not the arm" + (test-flan-fln--sent-code + (test-flan-fln--sending + (search-backward "1 ->") (flan-fln-eval-statement))) + "twice(1)\n twice(2)")) +(test-flan-fln--is "C-x C-e at the end of an if line sends its condition" + (test-flan-fln--last-at "if n < 0") '("eval-expr" "n < 0")) +(test-flan-fln--is "and of an elif, all of its condition" + (test-flan-fln--last-at "or n == 1") + '("eval-expr" "n == 0\n or n == 1")) +(test-flan-fln--is "and the same from the end of the condition's first line" + (test-flan-fln--last-at "elif n == 0") + '("eval-expr" "n == 0\n or n == 1")) + +(defconst test-flan-fln--wrapped + "fn f(o: Option(i64)) -> i64 + match o + Some(_) -> 5 + Some(n) -> n + 1 + Some(m) -> 7 + None -> 1 + + 2 + if 1 < 2 and + 3 < 4 + x = 1 + + 2 +") + +(defun test-flan-fln--wrapped-at (needle fn) + (test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped needle) + (end-of-line) + (let ((r (test-flan-fln--sending (funcall fn)))) + (and r (test-flan-fln--sent-code r))))) + +(test-flan-fln--is "an arm's value wrapped onto a second line goes whole" + (test-flan-fln--wrapped-at "None" #'flan-fln-eval-last) + "1 +\n 2") +(test-flan-fln--is "from its second line too" + (test-flan-fln--wrapped-at " 2\n if" #'flan-fln-eval-last) + "1 +\n 2") +(test-flan-fln--is "and C-c C-e on it sends the same" + (test-flan-fln--wrapped-at "None" #'flan-fln-eval-statement) + "1 +\n 2") +(test-flan-fln--is "a wrapped if condition, from the end of its first line" + (test-flan-fln--wrapped-at "if 1 < 2" #'flan-fln-eval-last) + "1 < 2 and\n 3 < 4") +(test-flan-fln--is "a statement wrapped by an operator, from the end of its first line" + (test-flan-fln--wrapped-at "x = 1 +" #'flan-fln-eval-last) + "x = 1 +\n 2") +(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped "None") + (end-of-line) + (let ((r (test-flan-fln--sending (flan-fln-eval-last)))) + (test-flan-fln--is "a value cut mid-line says where its statement starts" + (list (plist-get r :line) (plist-get r :col) (plist-get r :indent)) + '(6 13 5)))) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped "x = 1") + (end-of-line) + (let ((r (test-flan-fln--sending (flan-fln-eval-last)))) + (test-flan--check "a whole statement does not" (null (plist-member r :indent))))) +(test-flan-fln--is "an arm whose pattern binds nothing sends its value" + (test-flan-fln--wrapped-at "Some(_)" #'flan-fln-eval-last) "5") +(test-flan-fln--is "nor one whose value does not use what it binds" + (test-flan-fln--wrapped-at "Some(m)" #'flan-fln-eval-last) "7") +(dolist (c '(("Some(_v) -> _v + 1" "a name starting with _") + ("Some(éé) -> éé" "a name that is not ASCII") + ("Some(N) -> N" "a capitalised name") + ("Some(p) -> p.x" "a name used as a field's base") + ("Some(n) -> -n" "a name its value negates") + ("Some(p) -> -p.x" "a name whose field its value negates"))) + (test-flan-fln--in (concat "fn f(o: Option(i64)) -> i64\n match o\n " (car c) "\n") + (goto-char (point-max)) + (skip-chars-backward "\n") + (test-flan--check (format "an arm binding %s its value uses sends the match" (cadr c)) + (string-prefix-p "match o" + (test-flan-fln--sent-code + (test-flan-fln--sending (flan-fln-eval-last))))))) +(test-flan--check "one whose value uses its binding sends the match" + (string-prefix-p "match o" + (test-flan-fln--wrapped-at "Some(n)" #'flan-fln-eval-last))) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped "5") + (test-flan-fln--is "a match arm is a clause" + (test-flan-fln--thing 'flan-fln-clause) "Some(_) -> 5")) +(test-flan-fln--is "at the end of an else line, the whole if" + (cadr (test-flan-fln--last-at "else\n r")) + "if n < 0\n -1\n elif n == 0\n or n == 1\n 0\n else\n r") +(test-flan-fln--is "at the end of an on line, the whole handler-case" + (cadr (test-flan-fln--last-at "on Error")) + "handler-case\n f()\n on Error(e)\n nil") +(test-flan-fln--is "at the end of a fn header, the fn, installed" + (car (test-flan-fln--last-at "fn pick")) "eval") +(test-flan-fln--is "at the end of comment:, the whole block" + (test-flan-fln--last-at "comment:") + '("eval-expr" "comment:\n twice(4)")) +(test-flan-fln--in (test-flan-fln--at test-flan-fln--arms "; a comment") + (end-of-line) + (let ((r (test-flan-fln--sending + (condition-case nil (flan-fln-eval-last) (user-error nil))))) + (test-flan--check "C-x C-e on a comment line sends nothing from the comment" + (null r)))) + +(defun test-flan-fln--pause-at (needle) + (test-flan-fln--in (test-flan-fln--at test-flan-fln--arms needle) + (plist-get (test-flan-fln--sending (flan-fln-eval-defun '(4))) :pause))) + +(test-flan-fln--is "C-u C-c C-c on an arm's pattern stops at its value" + (test-flan-fln--pause-at "0 -> 10") '(3 10)) +(test-flan-fln--is "on an arm with a block, at the block" + (test-flan-fln--pause-at "1 ->") '(5 7)) +(test-flan-fln--is "on an elif line, at its condition" + (test-flan-fln--pause-at "elif") '(10 8)) +(test-flan-fln--is "and from its condition's continuation line too" + (test-flan-fln--pause-at "or n == 1") '(10 8)) +(test-flan-fln--is "on an else line, at its block" + (test-flan-fln--pause-at "else\n r") '(14 5)) +(test-flan-fln--is "on an on line, at its block" + (test-flan-fln--pause-at "on Error") '(18 5)) + +;;; Names and colours + +(test-flan-fln--in "fn f(x: i64) -> i64\n comment:\n g(:key-r, x)\n 0x1F + 12\n" + (search-forward "x:") + (backward-char 1) + (test-flan-fln--is "a colon glued to a name is not part of it" + (thing-at-point 'symbol t) "x") + (search-forward "comment") + (test-flan-fln--is "nor to a name that takes a block" + (thing-at-point 'symbol t) "comment") + (test-flan--check "font-lock draws the buffer without an error" + (condition-case nil (progn (font-lock-ensure) t) (error nil))) + (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 number is drawn as one" (funcall face "12") 'font-lock-number-face) + (test-flan-fln--is "a keyword as a constant" (funcall face ":key-r") 'font-lock-constant-face) + (test-flan-fln--is "a header word as a keyword" (funcall face "fn") 'font-lock-keyword-face) + (test-flan-fln--is "a type after its colon" (funcall face "i64") 'font-lock-type-face) + (test-flan--check "and the name before the colon is not a keyword" + (null (funcall face "x:"))))) + +;;; Indentation + +(defun test-flan-fln--tabs (text n) + "The column TEXT's `|' line reaches after N TABs." + (test-flan-fln--in text + (let ((last-command nil) (this-command 'indent-for-tab-command)) + (dotimes (_ n) + (indent-for-tab-command) + (setq last-command 'indent-for-tab-command))) + (current-indentation))) + +(defconst test-flan-fln--nest + "fn f() -> () + while a + if b + c() +|") + +(test-flan-fln--is "the first TAB after a block goes to its column" + (test-flan-fln--tabs test-flan-fln--nest 1) 6) +(test-flan-fln--is "each TAB after that steps out one" + (list (test-flan-fln--tabs test-flan-fln--nest 2) + (test-flan-fln--tabs test-flan-fln--nest 3) + (test-flan-fln--tabs test-flan-fln--nest 4) + (test-flan-fln--tabs test-flan-fln--nest 5)) + '(4 2 0 6)) +(test-flan-fln--is "a line of code already at a valid column stays there" + (test-flan-fln--tabs "fn f() -> ()\n if a\n b\n| let b = 2" 1) 2) +(test-flan-fln--is "and a second TAB steps it out" + (test-flan-fln--tabs "fn f() -> ()\n if a\n b\n| let b = 2" 2) 0) +(test-flan-fln--is "a line of code at no valid column goes to the deepest" + (test-flan-fln--tabs "fn f() -> ()\n if a\n b\n| let b = 2" 1) 4) +(test-flan-fln--is "after a header, one level deeper first" + (test-flan-fln--tabs "fn f() -> ()\n while a\n|" 1) 4) +(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--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" + (test-flan-fln--tabs "if a\n if b\n c\n |else" 1) 2) +(test-flan-fln--is "and a second TAB to the outer if's" + (test-flan-fln--tabs "if a\n if b\n c\n |else" 2) 0) +(test-flan-fln--is "on goes to its handler-case's" + (test-flan-fln--tabs " handler-case\n f()\n |on E(c)" 1) 2) +(test-flan-fln--is "inside a call, under its first argument" + (test-flan-fln--tabs " paint-at(i32(m.y) / cell-size,\n|i32(m.x))" 1) 11) +(test-flan-fln--is "inside a bracket with nothing after it, one level in" + (test-flan-fln--tabs " let v = [\n|1 2]" 1) 4) +(test-flan-fln--is "a closing bracket, at its opening line's column" + (test-flan-fln--tabs " let v = [\n 1 2\n|]" 1) 2) +(test-flan-fln--is "after a line ending in an operator, deeper than its statement" + (test-flan-fln--tabs " if a and\n|b" 1) 4) + +(test-flan-fln--in "fn f() -> ()\n while a\n b()\n |" + (flan-fln-dedent-or-delete 1) + (test-flan-fln--is "backspace in the indentation drops one level" + (current-indentation) 2) + (flan-fln-dedent-or-delete 1) + (test-flan-fln--is "and another" (current-indentation) 0)) + +(test-flan-fln--in "fn f() -> ()\n ab|" + (flan-fln-dedent-or-delete 1) + (test-flan-fln--is "backspace after text deletes a character" + (buffer-substring (line-beginning-position) (point)) " a")) + +(test-flan-fln--in "if a\n if b\n c\n els|" + (let ((last-command-event ?e)) + (insert "e") + (run-hooks 'post-self-insert-hook)) + (let ((last-command-event ?\s)) + (insert " ") + (run-hooks 'post-self-insert-hook)) + (test-flan-fln--is "else snaps to its if as it is typed" + (current-indentation) 2)) + +(test-flan-fln--in "fn f() -> ()\n if a\n b\n| c\n d\n" + (indent-region (point) (point-max)) + (test-flan-fln--is "indent-region moves a block rigidly" + (buffer-substring (point) (point-max)) + " c\n d\n")) + +(test-flan-fln--in "fn f() -> ()\n if a\n b\n |c\n" + (indent-region (point-min) (point-max)) + (test-flan-fln--is "and leaves lines at valid columns alone" + (buffer-string) "fn f() -> ()\n if a\n b\n c\n")) + +(test-flan-fln--in "fn f() -> ()\n if a\n b\n |\n" + (kill-new "if x\n y\n else\n z") + (flan-fln-yank) + (test-flan-fln--is "a statement cut from its first character yanks as one block" + (buffer-string) + "fn f() -> ()\n if a\n b\n if x\n y\n else\n z\n")) + +(test-flan-fln--in "fn f() -> ()\n if a\n |\n" + (kill-new " while x\n y\n") + (flan-fln-yank) + (test-flan-fln--is "whole lines yank at point's column" + (buffer-string) + "fn f() -> ()\n if a\n while x\n y\n\n")) + +;;; Block editing + +(test-flan-fln--in "fn f() -> ()\n if a\n |b()\n c()\n d()\n" + (flan-fln-slurp) + (test-flan-fln--is "slurp pulls the next statement into the block" + (buffer-string) "fn f() -> ()\n if a\n b()\n c()\n d()\n") + (flan-fln-barf) + (test-flan-fln--is "barf pushes the last one out again" + (buffer-string) "fn f() -> ()\n if a\n b()\n c()\n d()\n")) + +(test-flan-fln--in "fn f() -> ()\n |a()\n if x\n y\n b()\n" + (flan-fln-move-statement-down) + (test-flan-fln--is "a statement moves down past its sibling's whole block" + (buffer-string) "fn f() -> ()\n if x\n y\n a()\n b()\n") + (test-flan--check "and point moves with it" (looking-at "a()")) + (flan-fln-move-statement-up) + (test-flan-fln--is "and back up" + (buffer-string) "fn f() -> ()\n a()\n if x\n y\n b()\n")) + +(test-flan-fln--in "fn f() -> ()\n when(c):\n if x\n |y\n" + (flan-fln-raise-statement) + (test-flan-fln--is "raise replaces the owner with the statement" + (buffer-string) "fn f() -> ()\n when(c):\n y\n")) + +(test-flan-fln--in "fn f() -> ()\n |if x\n y\n else\n z\n b()\n" + (flan-fln-kill-statement) + (test-flan-fln--is "kill takes the whole statement's lines" + (buffer-string) "fn f() -> ()\n b()\n") + (test-flan-fln--is "into the kill ring" (current-kill 0) + " if x\n y\n else\n z\n")) + +;;; expand-region, where it is installed + +(let* ((dirs (append (file-expand-wildcards "~/.config/emacs/elpa/expand-region-[0-9]*") + (file-expand-wildcards "~/.emacs.d/elpa/expand-region-[0-9]*"))) + (load-path (append dirs load-path))) + (if (not (require 'expand-region nil t)) + (message " skip expand-region (not installed)") + (test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "right?\n -1") + (transient-mark-mode 1) + (let ((steps nil)) + (dotimes (_ 6) + (er/expand-region 1) + (push (buffer-substring-no-properties (region-beginning) (region-end)) steps)) + (setq steps (nreverse steps)) + (test-flan-fln--is "term, then statement's clause, then statement, then out" + (mapcar (lambda (s) (car (split-string s "\n"))) steps) + '("right?" "elif not right?" "if not left?" "let side =" + "if left? or right?" "let left? = col > 0")))))) + +;;; smartparens, where it is installed + +(let* ((dirs (append (file-expand-wildcards "~/.config/emacs/elpa/smartparens-[0-9]*") + (file-expand-wildcards "~/.emacs.d/elpa/smartparens-[0-9]*") + (file-expand-wildcards "~/.config/emacs/elpa/dash-[0-9]*") + (file-expand-wildcards "~/.emacs.d/elpa/dash-[0-9]*"))) + (load-path (append dirs load-path))) + (if (not (require 'smartparens nil t)) + (message " skip smartparens (not installed)") + ;; A setup that puts sexp commands on the top-level keys, as the + ;; author's does. + (define-key smartparens-mode-map (kbd "C-M-a") 'sp-backward-down-sexp) + (define-key smartparens-mode-map (kbd "C-M-u") 'sp-backward-up-sexp) + (let ((b (generate-new-buffer "sp.fln"))) + (switch-to-buffer b) + (flan-fln-mode) + (test-flan--check "smartparens is on in a .fln buffer" smartparens-mode) + (test-flan--check "and C-M-a and C-M-u stay the mode's" + (and (eq (key-binding (kbd "C-M-a")) 'beginning-of-defun) + (eq (key-binding (kbd "C-M-u")) 'flan-fln-up))) + (execute-kbd-macro "f(") + (test-flan-fln--is "it pairs a bracket" (buffer-string) "f()") + (erase-buffer) + (execute-kbd-macro "'a") + (test-flan-fln--is "and not a quote" (buffer-string) "'a") + (set-buffer-modified-p nil) + (kill-buffer b)) + (define-key smartparens-mode-map (kbd "C-M-a") nil) + (define-key smartparens-mode-map (kbd "C-M-u") nil))) + +;;; Under Evil + +(let* ((dirs (append (file-expand-wildcards "~/.config/emacs/elpa/evil-[0-9]*") + (file-expand-wildcards "~/.emacs.d/elpa/evil-[0-9]*") + (file-expand-wildcards "~/.config/emacs/elpa/goto-chg-*") + (file-expand-wildcards "~/.emacs.d/elpa/goto-chg-*"))) + (load-path (append dirs load-path))) + (if (not (require 'evil nil t)) + (message " skip the .fln text objects (Evil is not installed)") + (evil-mode 1) + (unwind-protect + (let ((yanked + (lambda (text keys) + (let ((b (generate-new-buffer "objects.fln"))) + (switch-to-buffer b) + (insert text) + (flan-fln-mode) + (evil-initialize-state) + (evil-normal-state) + (goto-char (point-min)) + (search-forward "|") + (delete-char -1) + (execute-kbd-macro keys) + (prog1 (substring-no-properties (current-kill 0)) + (set-buffer-modified-p nil) + (kill-buffer b)))))) + (dolist (c `(("yiw" ,(test-flan-fln--at test-flan-fln--settle "settle(") + "settle") + ("yie" ,(test-flan-fln--at test-flan-fln--settle "i32(m.x)") + "i32(m.x)") + ("yis" ,(test-flan-fln--at test-flan-fln--settle "and c >= 0") + "if r >= 0 and r < rows - 1\n and c >= 0\n grid[r, c] = 1") + ("yas" ,(test-flan-fln--at test-flan-fln--settle "and c >= 0") + " if r >= 0 and r < rows - 1\n and c >= 0\n grid[r, c] = 1\n") + ("yii" ,(test-flan-fln--at test-flan-fln--settle "if not left") + " 1\n") + ("yik" ,(test-flan-fln--at test-flan-fln--settle "if f32(rand())") + " if f32(rand()) < 0.5 then 1 else -1\n") + ("yak" ,(test-flan-fln--at test-flan-fln--settle "if f32(rand())") + " else\n if f32(rand()) < 0.5 then 1 else -1\n") + ("yik" ,(test-flan-fln--at test-flan-fln--wrapped "Some(_)") + "5") + ("yak" ,(test-flan-fln--at test-flan-fln--wrapped "Some(_)") + " Some(_) -> 5\n") + ("yid" ,(test-flan-fln--at test-flan-fln--settle "paint-at") + ,(substring test-flan-fln--settle + (string-search "fn step" test-flan-fln--settle))))) + (test-flan-fln--is (format "under Evil, %s" (car c)) + (funcall yanked (nth 1 c) (car c)) (nth 2 c))) + (let ((deleted + (lambda (keys needle) + (let ((b (generate-new-buffer "delete.fln"))) + (switch-to-buffer b) + (insert "fn f(x: i64) -> i64\n if x > 0\n a\n else\n 3\n x\n\nfn g() -> i32 = 1\n") + (flan-fln-mode) + (evil-initialize-state) + (evil-normal-state) + (goto-char (point-min)) + (search-forward needle) + (goto-char (match-beginning 0)) + (execute-kbd-macro keys) + (prog1 (buffer-string) + (set-buffer-modified-p nil) + (kill-buffer b)))))) + (dolist (c '(("dak" "else" + "fn f(x: i64) -> i64\n if x > 0\n a\n x\n\nfn g() -> i32 = 1\n") + ("das" "else" + "fn f(x: i64) -> i64\n x\n\nfn g() -> i32 = 1\n") + ("dii" "if x" + "fn f(x: i64) -> i64\n if x > 0\n else\n 3\n x\n\nfn g() -> i32 = 1\n") + ("dad" "else" "fn g() -> i32 = 1\n") + ("dad" "fn g" "fn f(x: i64) -> i64\n if x > 0\n a\n else\n 3\n x\n"))) + (test-flan-fln--is (format "under Evil, %s leaves no blank line" (car c)) + (funcall deleted (car c) (nth 1 c)) (nth 2 c)))) + ;; Comments: a block directly on a form is the form's; one a + ;; blank line away, or below it, is not. + (let ((deleted + (lambda (keys needle) + (let ((b (generate-new-buffer "comments.fln"))) + (switch-to-buffer b) + (insert "; loose\n\n; on f\nfn f() -> ()\n a()\n; after f\n\n" + "; on g\nfn g() -> ()\n ; on b\n b()\n") + (flan-fln-mode) + (evil-initialize-state) + (evil-normal-state) + (goto-char (point-min)) + (search-forward needle) + (goto-char (match-beginning 0)) + (execute-kbd-macro keys) + (prog1 (buffer-string) + (set-buffer-modified-p nil) + (kill-buffer b)))))) + (dolist (c '(("dad" "a()" + "; loose\n\n; after f\n\n; on g\nfn g() -> ()\n ; on b\n b()\n") + ("dad" "b()" + "; loose\n\n; on f\nfn f() -> ()\n a()\n; after f\n") + ("did" "on g" + "; loose\n\n; on f\nfn f() -> ()\n a()\n; after f\n\n") + ("das" "b()" + "; loose\n\n; on f\nfn f() -> ()\n a()\n; after f\n\n; on g\nfn g() -> ()\n"))) + (test-flan-fln--is (format "under Evil, %s on %s keeps comments with their forms" + (car c) (nth 1 c)) + (funcall deleted (car c) (nth 1 c)) (nth 2 c))) + ;; A lone comment belongs to no form: id and ad find nothing. + (dolist (text '("fn a() -> i64\n 1\n\n; lone\n\nfn b() -> i64\n 2\n" + "fn a() -> i64\n 1\n\n; lone\n")) + (dolist (keys '("did" "dad")) + (let ((b (generate-new-buffer "lone.fln"))) + (switch-to-buffer b) + (insert text) + (flan-fln-mode) + (evil-initialize-state) + (evil-normal-state) + (goto-char (point-min)) + (search-forward "; lone") + (goto-char (match-beginning 0)) + (ignore-errors (execute-kbd-macro keys)) + (test-flan-fln--is (format "under Evil, %s on a lone comment%s changes nothing" + keys (if (string-suffix-p "lone\n" text) " at the end" "")) + (buffer-string) text) + (set-buffer-modified-p nil) + (kill-buffer b)))) + ;; A comment deeper than a form, at the end of its block, is + ;; that block's, not the next form's. + (dolist (c '(("dad" "fn a" "fn a() -> i64\n 1\n ; end of a\nfn b() -> i64\n 2\n" + "fn b() -> i64\n 2\n") + ("dad" "fn b" "fn a() -> i64\n 1\n ; end of a\nfn b() -> i64\n 2\n" + "fn a() -> i64\n 1\n ; end of a\n") + ("das" "let y" + "fn a(x: i64) -> i64\n if x > 0\n 1\n ; end of the if\n let y = 2\n y\n" + "fn a(x: i64) -> i64\n if x > 0\n 1\n ; end of the if\n y\n"))) + (let ((b (generate-new-buffer "owned.fln"))) + (switch-to-buffer b) + (insert (nth 2 c)) + (flan-fln-mode) + (evil-initialize-state) + (evil-normal-state) + (goto-char (point-min)) + (search-forward (nth 1 c)) + (goto-char (match-beginning 0)) + (execute-kbd-macro (car c)) + (test-flan-fln--is (format "under Evil, %s on %s: a deeper comment stays with the block above" + (car c) (nth 1 c)) + (buffer-string) (nth 3 c)) + (set-buffer-modified-p nil) + (kill-buffer b)))) + (let ((b (generate-new-buffer "keys.fln"))) + (switch-to-buffer b) + (insert "fn f() -> i32 = 1\n") + (flan-fln-mode) + (evil-initialize-state) + (test-flan--check "under Evil, C-x C-e is still the mode's" + (eq (key-binding (kbd "C-x C-e")) 'flan-fln-eval-last)) + (goto-char (point-min)) + (end-of-line) + (backward-char) + (test-flan--check "under Evil, C-x C-e counts the cursor's character" + (= (flan-fln--point-for-last) (line-end-position))) + (kill-buffer b))) + (evil-mode -1)))) + +;;; test-flan-fln.el ends here diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 952cb01f..7712e455 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -2492,6 +2492,13 @@ already rely on it — so nothing here is a stand-in for the real thing." (ignore-errors (delete-file socket6)) (ignore-errors (delete-file scratch))) + ;; ── The .fln keys, against daemons of their own ──────────────────────── + (setq test-flan-fln-live-dir (file-name-directory file) + test-flan-fln-live-socket socket) + (load (expand-file-name "test-flan-fln-live.el" + (file-name-directory load-file-name)) + nil t) + (if (zerop test-flan--failures) (message "flan.el: all tests passed") (message "\n%d failure(s)" test-flan--failures) diff --git a/lib/ast.ml b/lib/ast.ml index 362537c6..7ab6b16e 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -24,6 +24,9 @@ and texpr_kind = them identically — the difference is a fact about the value, and it is [Check.resolve] that turns it into one. *) | Tfn of bool * texpr list * texpr + (* An integer written as a generic struct's argument, the 8 in + (Small 8 i32). Parsed only there; it is not a type anywhere else. *) + | Tlen of int64 (* An array length is an integer or a compile-time constant's name. *) and len = @@ -219,6 +222,10 @@ and arm = { pat : pattern; body : expr list; aloc : Loc.t } and pattern = | Pctor of string * string list (* (Some e) (Rect w h) None *) | Pkw of string (* :north — an enum member *) + (* 5 -2.5 \a "go" — an Int, UInt, Float, Byte or Str expr, compared as + (= t lit). An expr and not a literal type of its own, so the checker + types it against the scrutinee as any literal is typed against its site. *) + | Plit of expr | Pwild (* _ :else *) (* ── Declarations ──────────────────────────────────────────────────── *) diff --git a/lib/check.ml b/lib/check.ml index 10d65729..c51d5690 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -79,6 +79,54 @@ let rec slot_text = function | Sclass c -> c | Sopt s -> "(Option " ^ slot_text s ^ ")" +(* Where [sep] first occurs in [m]. *) +let find_sub m sep = + let n = String.length m and k = String.length sep in + let rec go i = + if i + k > n then None + else if String.sub m i k = sep then Some i + else go (i + 1) + in + go 0 + +(* ── Generic structs ───────────────────────────────────────────────── + [(defstruct Small [items [$n $t] count i32])] is a template, not a type. + Its parameters are the sigil names its fields introduce, in the order + first written — [$n] then [$t] here, so the type is spelled + [(Small 8 i32)] — and each is a length or a type by where it stands: in an + array's length slot, or in a generic struct's length argument, it is a + length; anywhere else a type. + + Each application at concrete arguments is a copy: an ordinary struct under + a symbol-safe key, [Small-8-i32], so layout, both backends, the renderer + and DWARF see a struct and nothing else — the same arrangement a generic + function's copy has. [struct_apps] is how the checker still knows what a + copy was applied to, which is what binding [(defn push [s (Ptr (Small $n + $t))] ...)] against an argument needs. An application at variables is a + copy too, under a key with the variables in it, whose array lengths are + [abstract_len]; it exists for the abstract pass over a generic body and is + left out of the program. *) +type gstruct = { + gparams : (string * bool) list; (* name, and whether it is a length *) + gfields : Ast.field list; + gloc : Loc.t; +} + +(* Key -> the generic struct and the arguments it was applied to; a length + argument is [Types.Len], a variable one [Types.Var]. Global for the reason + [Types.display] is: [bind_ty] and [subst_ty] are called from places with no + env in hand. The key is made from exactly these, so an entry can only + mislead where a later program in the same process declares a struct under + a copy's key by hand, and [struct_copy] refuses that name the moment the + program asks for the copy itself. *) +let struct_apps : (string, string * Types.t list) Hashtbl.t = Hashtbl.create 16 + +(* The length every length variable has inside a generic body's abstract + pass. Large so that no constant index into such an array is refused as out + of bounds there, and within i32 so that [(length a)] is an ordinary index. + Every length is answered again, exactly, per copy. *) +let abstract_len = 2147483647L + type env = { structs : (string, Tast.structure) Hashtbl.t; datas : (string, Tast.data) Hashtbl.t; @@ -192,13 +240,47 @@ type env = { chain. Odin has no cap of its own to copy, so there was nothing to borrow. *) mutable chain : (string * Types.t list * Loc.t) list; + (* Generics whose abstract pass was refused and recorded, in a whole-file + check that goes on after a refusal. A call site still gets a copy's + signature, but its body is not checked again: every refusal the abstract + pass made would come back from the copy, at the same line, once per type + it was called at. *) + refused_generics : (string, unit) Hashtbl.t; + (* The generic structs, by name; see [gstruct]. *) + gstructs : (string, gstruct) Hashtbl.t; + (* The struct copies this env made, by key, and whether each is one at + variables — those are left out of the program. *) + copies : (string, bool) Hashtbl.t; + (* Templates whose own check was refused while [deferred] was collecting: + a use of one is a copy with no fields, so the refusal is said once, at + the defstruct, and nothing downstream repeats it. *) + broken : (string, unit) Hashtbl.t; + (* While a whole-file check collects every error, the refusals [collect] + can go on past — a generic struct's template, a where clause over a + length — are kept here instead of ending the pass. [None] everywhere + else, where they raise as before. *) + mutable deferred : Loc.diag list option; + (* A generic defn's length variables, by name: the ones of its [gsigs] + variables that are lengths. *) + glens : (string, string list) Hashtbl.t; + (* Which of [tyvars] are lengths. A length variable is also a value inside + the body — [n] reads as the integer it was bound to. *) + mutable lenvars : string list; + (* Set while a generic body is checked abstractly, and while a struct copy + at variables is laid out: a length variable's array is then + [abstract_len] long rather than the [Types.LArray] a signature pattern + needs. *) + mutable len_placeholder : bool; + (* The struct copies being laid out, innermost last, so a template that + asks for a copy of itself at a bigger type is refused rather than + followed forever. *) + mutable schain : (string * Types.t list) list; (* Set while a struct, data-case or union field's type is being resolved, and only then. It exists for one message: an unknown lowercase name in a type slot is told to introduce a type variable with [$name] in the - parameter vector, and a field has no parameter vector — only a defn - signature binds, and a field is built at one type for every value. The - flag is what lets [resolve_name] say the honest thing in each place - instead of a suggestion that cannot be followed. *) + parameter vector, and a field has no parameter vector — a defstruct's + field introduces one where it stands. The flag is what lets + [resolve_name] say the honest thing in each place. *) mutable in_field : bool; (* Every [defclass], by name: its slots in constructor order, each with the type a value stored in it must have — [Types.Dyn] for a slot written @@ -253,6 +335,15 @@ let new_env () = { subst = []; tvpreds = []; chain = []; + refused_generics = Hashtbl.create 4; + gstructs = Hashtbl.create 4; + copies = Hashtbl.create 8; + broken = Hashtbl.create 2; + deferred = None; + glens = Hashtbl.create 8; + lenvars = []; + len_placeholder = false; + schain = []; in_field = false; classes = Hashtbl.create 8; tracks = Hashtbl.create 16; @@ -263,6 +354,13 @@ let new_env () = { guard_next = false; } +(* A refusal [collect] can go on past: kept while a whole-file check is + collecting, in the order found, and raised otherwise. *) +let defer_or_raise env (d : Loc.diag) = + match env.deferred with + | Some l -> env.deferred <- Some (d :: l) + | None -> Loc.raise_diag d + (* Where a named type was declared, and what it has, as a note. This is the second half of the two-place messages: a refusal that says @@ -273,6 +371,13 @@ let new_env () = { the name is not one this environment placed, so it degrades to the message alone rather than to a wrong pointer. *) let declared_note env name = + (* A generic struct's copy is declared where its template is, and is + spoken of by the template's name there. *) + let shown = + match Hashtbl.find_opt struct_apps name with + | Some (g, _) when Hashtbl.mem env.copies name -> g + | _ -> name + in match Hashtbl.find_opt env.locs name with | None -> [] | Some at -> @@ -288,8 +393,8 @@ let declared_note env name = | None -> []) in let what = - if names = [] then name ^ " is declared here" - else name ^ " is declared here, with " ^ String.concat ", " names + if names = [] then shown ^ " is declared here" + else shown ^ " is declared here, with " ^ String.concat ", " names in [ Loc.note at what ] @@ -1227,6 +1332,119 @@ let rec unfillable env seen (t : Types.t) : Types.t option = | None -> Some t) | _ -> Some t +(* How a concrete type is spelled inside an instantiation's name. The prelude + already writes this by hand — [filter-i32], [sum-f32], [append-i64] — so a + generated name reads like the handwritten one it replaces, which is what a + backtrace, a [Reach] edge and a dev-build cell all end up showing. + [Types.to_string] cannot serve: [[i32]] and [(Vec i32)] are not symbols. *) +let rec mangle_ty (t : Types.t) = + match t with + | Types.Unit -> "unit" + | Types.Slice (Types.Mut, e) -> "slice-" ^ mangle_ty e + | Types.Slice (Types.Const, e) -> "cslice-" ^ mangle_ty e + | Types.Array (n, e) -> Printf.sprintf "arr%Ld-%s" n (mangle_ty e) + | Types.Map (k, v) -> Printf.sprintf "map-%s-%s" (mangle_ty k) (mangle_ty v) + | Types.Ptr (Types.Mut, e) -> "ptr-" ^ mangle_ty e + | Types.Ptr (Types.Const, e) -> "cptr-" ^ mangle_ty e + | Types.Vec e -> "vec-" ^ mangle_ty e + | Types.Option e -> "opt-" ^ mangle_ty e + | Types.Fn (ps, r) -> + Printf.sprintf "fn-%s-to-%s" + (String.concat "-" (List.map mangle_ty ps)) (mangle_ty r) + | Types.CFn (ps, r) -> + Printf.sprintf "cfn-%s-to-%s" + (String.concat "-" (List.map mangle_ty ps)) (mangle_ty r) + (* Bare, because [Types.to_string] spells a variable with its [$] for the + reader and a symbol has no room for one. *) + | Types.Var n -> n + (* The key, not [Types.to_string]'s [(Small 8 i32)], which is a reader's + spelling and not a symbol. *) + | Types.Named n -> n + | t -> Types.to_string t + +let rec occurs_in ~needle (t : Types.t) = + Types.equal needle t + || + match t with + | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e + | Types.Option e -> occurs_in ~needle e + | Types.Map (k, v) -> occurs_in ~needle k || occurs_in ~needle v + | Types.Fn (ps, r) | Types.CFn (ps, r) -> + List.exists (occurs_in ~needle) ps || occurs_in ~needle r + | Types.LArray (_, e) -> occurs_in ~needle e + (* Through a struct copy's arguments, or [(Node (Node $t))] would not be + seen to contain [(Node $t)]. *) + | Types.Named k -> + (match Hashtbl.find_opt struct_apps k with + | Some (_, args) -> List.exists (occurs_in ~needle) args + | None -> false) + | _ -> false + +(* [b] is [a] with something built around it: same shape, strictly bigger. *) +let grows ~from_:a ~to_:b = + List.length a = List.length b + && List.for_all2 (fun x y -> occurs_in ~needle:x y) a b + && not (List.for_all2 Types.equal a b) + +(* A generic struct's copy at [args], by key: [Small-8-i32], or + [Small-$n-$t] at variables. Recorded in [struct_apps] and [Types.display] + as the key is made; the copy's fields are [struct_copy]'s business. *) +let struct_app g args = + let key = + g ^ "-" + ^ String.concat "-" + (List.map + (function + | Types.Var v -> "$" ^ v + | Types.Len n -> Int64.to_string n + | t -> mangle_ty t) + args) + in + if not (Hashtbl.mem struct_apps key) then begin + Hashtbl.replace struct_apps key (g, args); + Hashtbl.replace Types.display key + (Printf.sprintf "(%s %s)" g + (String.concat " " (List.map Types.to_string args))) + end; + key + +(* Does [name] contain itself by value? [check_finite] asks it of every + declared type once they are all collected, and a generic struct's copy asks + it of itself when it is made, which is after that. *) +let finite_from env name0 = + let rec walk seen name = + if List.mem name seen then + (let shown = Types.to_string (Types.Named name) in + fail (Option.value (Hashtbl.find_opt env.locs name) ~default:Loc.unknown) + "%s contains itself by value, so it has no size — go through (Ptr %s)" + shown shown); + let seen = name :: seen in + match Hashtbl.find_opt env.structs name with + | Some s -> List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) s.Tast.fields + | None -> + match Hashtbl.find_opt env.datas name with + | Some u -> + List.iter + (fun (c : Tast.variant) -> + List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) c.Tast.vfields) + u.Tast.cases + | None -> + (* A union whose member is itself is the same infinite type a struct's + is — the size is the largest member and the largest member is the + whole thing. Nothing about overlaying storage makes the recursion + finite, so it is on the same walk rather than left to hang the + layout calculator. *) + match Hashtbl.find_opt env.unions name with + | None -> () + | Some u -> + List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) u.Tast.fields + and ty seen = function + | Types.Named n -> walk seen n + | Types.Array (_, e) | Types.Option e -> ty seen e + | _ -> () + in + walk [] name0 + (* The name under the sigil. [$t] is how a defn signature introduces a type variable and [t] is how the body spells the same one, so the tables that record which variables are in scope — [env.tyvars] and [env.subst] — are @@ -1259,7 +1477,17 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t = | Ast.Tarray (l, e) -> let e = resolve env ~seen e in no_zeroed_fn loc "a fixed array's element" e; - Types.Array (array_len env loc l, e) + (match l with + | Ast.Lname n + when (not env.len_placeholder) + && List.mem (tyvar_bare n) env.lenvars + && not (List.mem_assoc (tyvar_bare n) env.subst) -> + Types.LArray (tyvar_bare n, e) + | _ -> Types.Array (array_len env loc l, e)) + | Ast.Tlen n -> + fail loc + "%Ld is not a type. An integer stands only where a generic struct takes \ + a length, as in (Small 8 i32)" n (* {K V} is the type spelling. There is no map *literal*: a bare map form in expression position is a struct literal's field list, and giving the same braces two meanings is what the colon-to-dot change was for. A map is @@ -1323,15 +1551,10 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t = (resolve env ~seen v) | "Map", _ -> fail loc "(Map K V) takes exactly two types" | "Result", _ -> unimplemented loc "(Result T E)" 6 + | _ when Hashtbl.mem env.gstructs name -> apply_struct env ~seen loc name args | _ -> - (* Not generics, which are here: a *function* is generic over [$t] and - instantiated per call site. This is a parameterised named type — - [(Pair i32 f64)] — and that is a different thing and is not built. - [Types.Named] is a bare string with no parameters, so there is - nowhere to put the arguments, and giving it some is a change to - [Types.t] and therefore to the layout calculator, both backends, - [Render] and the DWARF path. docs/SPIKE-GENERICS.md, question 4, - prices it and leaves it out. *) + (* No type of this name takes arguments: a generic struct is caught + by the arm above, and [Ptr], [Option], [Vec] and [Map] further up. *) (* A head that is not a type at all but one edit from one is the typo [(Vect i32)], and the generics sentence would answer a question nobody asked. *) @@ -1348,9 +1571,169 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t = "unknown type %s — did you mean %s?" name m | _ -> ()); fail loc - "%s takes no type arguments. A generic function is written with $t \ - in its parameter vector; a generic type is not there yet" - name) + "%s takes no type arguments. A generic struct is one whose fields \ + introduce $t, as in (defstruct %s [x $t]), and a generic function \ + one whose parameter vector does" + name name) + +(* [(Small 8 i32)]: each argument read as the parameter it stands for — a + length or a type — and the copy made, or found. *) +and apply_struct env ~seen loc name args = + let g = Hashtbl.find env.gstructs name in + let spelled = + Printf.sprintf "(%s %s)" name + (String.concat " " (List.map (fun (p, _) -> "$" ^ p) g.gparams)) + in + let n = List.length g.gparams in + if List.length args <> n then + Loc.failk "check/generic-struct-arity" loc + ~notes:[ Loc.note g.gloc (name ^ " is declared here") ] + "%s takes %d argument%s, %s, and this gives %d" + name n (if n = 1 then "" else "s") spelled (List.length args); + let targs = + List.map2 + (fun (p, is_len) (a : Ast.texpr) -> + if is_len then struct_len_arg env name p a + else + match a.Ast.t with + | Ast.Tlen k -> + fail a.Ast.tloc + "%s's $%s is a type, and %Ld is a length — %s" name p k spelled + | _ -> resolve env ~seen a) + g.gparams args + in + Types.Named (struct_copy env loc name targs) + +and struct_len_arg env name p (a : Ast.texpr) = + let not_one what = + fail a.Ast.tloc + "%s's $%s is a length: an integer, a constant's name or a length \ + variable, and %s is %s" name p (Cimport.ty_source a) what + in + match a.Ast.t with + | Ast.Tlen k when Int64.compare k 0L < 0 -> + fail a.Ast.tloc "%s's $%s is a length, and %Ld is negative" name p k + | Ast.Tlen k -> Types.Len k + | Ast.Tname n -> + let bare = tyvar_bare n in + (match List.assoc_opt bare env.subst with + | Some (Types.Len _ as l) -> l + | Some (Types.Var v) -> Types.Var v + | Some t -> not_one ("the type " ^ Types.to_string t) + | None -> + if List.mem bare env.lenvars then Types.Var bare + else if List.mem bare env.tyvars then not_one "a type variable" + else + match Hashtbl.find_opt env.consts n with + | Some k -> Types.Len k + | None -> not_one "none of them") + | _ -> not_one "a type" + +(* The copy of generic struct [name] at [targs], made on first use and + registered as an ordinary struct under its key. *) +and struct_copy ?(at_definition = false) env loc name targs = + let key = struct_app name targs in + if Hashtbl.mem env.copies key then key + else if Hashtbl.mem env.broken name then begin + Hashtbl.replace env.copies key (List.exists generic_arg targs); + Hashtbl.replace env.structs key { Tast.sname = key; fields = [] }; + key + end + else begin + if Hashtbl.mem env.structs key || Hashtbl.mem env.datas key + || Hashtbl.mem env.unions key then + fail loc + "%s at these arguments is called %s, and %s is already defined — \ + rename one" name key key; + let g = Hashtbl.find env.gstructs name in + (* A copy that asks for a copy of its own template at a type built around + its own arguments — [(defstruct Grow [next (Ptr (Grow [$t]))])] — asks + forever, and pointers do not stop it: each copy is made the moment it + is named. *) + let chain_text () = + String.concat "\n " + (List.map + (fun (h, a) -> + Printf.sprintf "(%s %s)" h + (String.concat " " (List.map Types.to_string a))) + (env.schain @ [ (name, targs) ])) + in + if List.exists + (fun (h, a) -> String.equal h name && grows ~from_:a ~to_:targs) + env.schain + || List.length env.schain >= 64 then + Loc.failk "check/runaway-instantiation" loc + ~notes:[ Loc.note g.gloc (name ^ " is declared here") ] + "%s names a copy of itself at a type built around its own \ + arguments, and that copy names another, without end:\n %s\n\ + Name the same arguments, or smaller ones" name (chain_text ()); + let generic = List.exists generic_arg targs in + (* In before its fields, so a field that names the same copy through a + pointer — [(defstruct Node [next (Ptr (Node $t))])] — finds it. *) + Hashtbl.replace env.copies key generic; + Hashtbl.replace env.structs key { Tast.sname = key; fields = [] }; + Hashtbl.replace env.locs key g.gloc; + let saved = + (env.subst, env.tyvars, env.lenvars, env.tvpreds, env.len_placeholder, + env.in_field, env.schain) + in + let restore () = + let s, t, l, p, lp, f, c = saved in + env.subst <- s; env.tyvars <- t; env.lenvars <- l; env.tvpreds <- p; + env.len_placeholder <- lp; env.in_field <- f; env.schain <- c + in + env.subst <- List.map2 (fun (p, _) a -> (p, a)) g.gparams targs; + env.tyvars <- []; + env.lenvars <- []; + env.tvpreds <- []; + env.len_placeholder <- generic; + env.in_field <- true; + env.schain <- env.schain @ [ (name, targs) ]; + match + List.map + (fun (f : Ast.field) -> + let fty = resolve env f.Ast.fty in + no_zeroed_fn f.Ast.fty.Ast.tloc + (Printf.sprintf "the field %s" f.Ast.fname) fty; + { Tast.fname = f.Ast.fname; fty }) + g.gfields + with + | fields -> + restore (); + Hashtbl.replace env.structs key { Tast.sname = key; fields }; + finite_from env key; + key + | exception e -> + restore (); + Hashtbl.remove env.copies key; + Hashtbl.remove env.structs key; + (* A field refused inside the template says nothing about which use + asked for this copy; the note names it, one per level of copies. *) + (match e with + | Loc.Error d when d.Loc.dloc <> loc && not at_definition -> + Loc.raise_diag + { d with + Loc.notes = + d.Loc.notes + @ [ Loc.note loc + (Types.to_string (Types.Named key) ^ " is made here") ] } + | e -> raise e) + end + +(* Does a struct argument still mention a variable? *) +and generic_arg (t : Types.t) = + match t with + | Types.Var _ | Types.LArray _ -> true + | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e + | Types.Option e -> generic_arg e + | Types.Map (k, v) -> generic_arg k || generic_arg v + | Types.Fn (ps, r) | Types.CFn (ps, r) -> + List.exists generic_arg ps || generic_arg r + | Types.Named k -> + (match Hashtbl.find_opt struct_apps k with + | Some (_, a) -> List.exists generic_arg a + | None -> false) + | _ -> false (* One edit away from a type that exists — a substitution, an insertion, a deletion or a transposition of neighbours. Bounded at one, because two edits @@ -1384,12 +1767,19 @@ and resolve_name env ~seen loc n = one, a mistyped type name silently became a type parameter and made the function more permissive than it was written to be. *) let bare = tyvar_bare n in + let a_length () = + fail loc + "%s is a length, not a type — it stands where an array's length does, \ + as in [%s T], or as a generic struct's length argument" n n + in match List.assoc_opt bare env.subst with + | Some (Types.Len _) -> a_length () (* Inside an instantiation: the variable is this concrete type, and every node checked under it is as concrete as if it had been written out. *) | Some t -> t | None -> - if List.mem bare env.tyvars then Types.Var bare + if List.mem bare env.lenvars then a_length () + else if List.mem bare env.tyvars then Types.Var bare else if n <> bare then (* A sigil on a name nothing binds. Two different mistakes wear the same spelling, and which one it is turns on whether any variable is in scope @@ -1408,17 +1798,17 @@ and resolve_name env ~seen loc n = (match (match env.tyvars with [] -> List.map fst env.subst | vs -> vs) with | [] -> Loc.failk "check/unbound-type-variable" loc - "%s introduces a type variable, and only a defn signature can — write \ - the concrete type here" n + "%s introduces a type variable, and only a defn signature or a \ + defstruct's fields can — write the concrete type here" n | [ v ] -> Loc.failk "check/unbound-type-variable" loc "nothing binds the type variable %s — this signature introduces %s, \ - so write %s here, or a concrete type" n v v + so write %s here, or a concrete type" n ("$" ^ v) ("$" ^ v) | vars -> Loc.failk "check/unbound-type-variable" loc "nothing binds the type variable %s — this signature introduces %s, \ so write one of those here, or a concrete type" - n (String.concat " and " vars)) + n (String.concat " and " (List.map (fun v -> "$" ^ v) vars))) else match Types.ikind_of_name n with | Some k -> Types.Int k @@ -1448,6 +1838,21 @@ and resolve_name env ~seen loc n = if List.mem n seen then fail loc "the type alias %s is defined in terms of itself" n else resolve env ~seen:(n :: seen) (Hashtbl.find env.aliases n) + | _ when Hashtbl.mem env.gstructs n -> + let g = Hashtbl.find env.gstructs n in + Loc.failk "check/generic-struct-arity" loc + ~notes:[ Loc.note g.gloc (n ^ " is declared here") ] + "%s is generic, and a type only once it is given its arguments: \ + write (%s %s)" n n + (* Variables are only an answer where a signature binds them; in + ordinary code the example is concrete. *) + (String.concat " " + (List.map + (fun (p, is_len) -> + if env.tyvars <> [] then "$" ^ p + else if is_len then "8" + else "i32") + g.gparams)) | _ when Hashtbl.mem env.structs n -> Types.Named n (* A data type is [Named] exactly as a struct is: one case in [Types.t] covers both, and which table the name is in is what tells them apart. @@ -1485,18 +1890,16 @@ and resolve_name env ~seen loc n = permissive than it was written to be. *) | _ when n <> "" && n.[0] = Char.lowercase_ascii n.[0] -> (* The parameter-vector suggestion is only followable where a - parameter vector exists. A field has none and never will — only a - defn signature binds a variable, and a field is built at one type - for every value — so at a field the message offers the two things - that can actually be written there. *) + parameter vector exists. A field has none: a defstruct's field + introduces the variable where it stands, so at a field the message + says that instead. *) if env.in_field then Loc.failk "check/unknown-type" loc - "unknown type %s. A lowercase name is a type variable, and a \ - field cannot hold one: only a defn signature introduces type \ - variables, and a field is built at one type for every value — \ - generic types are not there. Write a concrete type here, or dyn \ - to hold any value" - n + "unknown type %s. A lowercase name is a type variable only where \ + it is introduced with $%s, and in a defstruct's fields that makes \ + the struct generic over it. Write $%s, a concrete type, or dyn to \ + hold any value" + n n n else Loc.failk "check/unknown-type" loc "unknown type %s. A lowercase name is a type variable only where a \ @@ -1508,7 +1911,19 @@ and resolve_name env ~seen loc n = and array_len env loc = function | Ast.Lint n -> n | Ast.Lname n -> - (match Hashtbl.find_opt env.consts n with + let bare = tyvar_bare n in + (match List.assoc_opt bare env.subst with + | Some (Types.Len k) -> k + | Some (Types.Var _) -> abstract_len + | Some t -> + fail loc "%s is the type %s here, and an array length is an integer, a \ + constant or a length variable" n (Types.to_string t) + | None when List.mem bare env.lenvars -> abstract_len + | None when List.mem bare env.tyvars -> + fail loc "%s is a type variable, and an array length is an integer, a \ + constant or a length variable" n + | None -> + match Hashtbl.find_opt env.consts n with | Some v -> v | None -> fail loc "%s is not a compile-time integer constant, so it cannot be \ @@ -1539,6 +1954,7 @@ let is_type_name env n = || List.mem n [ "bool"; "string"; "dyn"; "Unit"; "Never"; "Allocator" ] || Hashtbl.mem env.aliases n || Hashtbl.mem env.structs n + || Hashtbl.mem env.gstructs n || Hashtbl.mem env.datas n || Hashtbl.mem env.unions n || Hashtbl.mem env.enums n @@ -1911,6 +2327,7 @@ let defvar_reads_as_type env (t : Ast.texpr) = | Ast.Tname n -> is_type_name env n | Ast.Tapp (head, _) -> List.mem head [ "Ptr"; "Option"; "Vec"; "Map"; "Result" ] + || Hashtbl.mem env.gstructs head (* A slice, a fixed array, a map type or an (Fn ...): [Parse] only carries one of these over when it read as a type and had no value reading, so there is nothing here to decide. *) @@ -2087,9 +2504,9 @@ let settle_defvars env (decls : Ast.decl list) : Ast.decl list = (* The variables a signature introduces: every [$t] written in it, in the order written, once each. Only a [defn] signature is scanned, which is what makes the binding site a *place* and not merely a spelling. *) -let signature_tyvars (fn : Ast.fn) = +let sigil_vars ~kinds_of (ts : Ast.texpr list) = let acc = ref [] in - let name loc n = + let add loc n is_len = if n <> "" && n.[0] = '$' then begin let bare = String.sub n 1 (String.length n - 1) in if bare = "" then fail loc "$ on its own does not name a type variable"; @@ -2099,24 +2516,59 @@ let signature_tyvars (fn : Ast.fn) = || Types.ikind_of_name bare <> None || Types.fkind_of_name bare <> None then fail loc "%s is a type, so $%s cannot be a type variable" bare bare; - if not (List.mem bare !acc) then acc := bare :: !acc + match List.assoc_opt bare !acc with + | None -> acc := (bare, is_len) :: !acc + | Some k when k = is_len -> () + | Some _ -> + fail loc + "$%s stands for a length in one place here and a type in another — \ + a length goes in an array's length slot, [$%s T], and a type \ + everywhere else. Give the two different names" bare bare end in let rec ty (t : Ast.texpr) = match t.Ast.t with - | Ast.Tname n -> name t.Ast.tloc n + | Ast.Tname n -> add t.Ast.tloc n false | Ast.Tslice (_, e) -> ty e + | Ast.Tarray (Ast.Lname n, e) -> add t.Ast.tloc n true; ty e | Ast.Tarray (_, e) -> ty e | Ast.Tmap (k, v) -> ty k; ty v - (* The head of an application is a constructor — [Ptr], [Option], [Vec] — - and a variable cannot stand there: this spike is generic over types, - not over type constructors. A [$t] inside the arguments is ordinary. *) - | Ast.Tapp (_, args) -> List.iter ty args + (* The head of an application is a constructor — [Ptr], [Option], [Vec], + a generic struct — and a variable cannot stand there: this is generic + over types, not over type constructors. A [$t] inside the arguments is + ordinary, and a generic struct's length argument is a length. *) + | Ast.Tapp (h, args) -> + (match kinds_of h with + | Some ks when List.length ks = List.length args -> + List.iter2 + (fun is_len (a : Ast.texpr) -> + match a.Ast.t with + | Ast.Tname n when is_len -> add a.Ast.tloc n true + | _ -> ty a) + ks args + | _ -> List.iter ty args) | Ast.Tfn (_, ps, r) -> List.iter ty ps; ty r + | Ast.Tlen _ -> () in - List.iter (fun (p : Ast.field) -> ty p.Ast.fty) fn.Ast.params; - (match fn.Ast.ret with Some r -> ty r | None -> ()); - List.rev !acc + List.iter ty ts; + let vs = List.rev !acc in + (List.map fst vs, List.filter_map (fun (v, l) -> if l then Some v else None) vs, + vs) + +let struct_kinds env h = + Option.map (fun g -> List.map snd g.gparams) (Hashtbl.find_opt env.gstructs h) + +(* The variables a signature introduces: every [$t] written in it, in the + order written, once each, and which of them are lengths. Only a [defn] + signature and a [defstruct]'s fields are scanned, which is what makes the + binding site a *place* and not merely a spelling. *) +let signature_tyvars env (fn : Ast.fn) = + let vars, lens, _ = + sigil_vars ~kinds_of:(struct_kinds env) + (List.map (fun (p : Ast.field) -> p.Ast.fty) fn.Ast.params + @ Option.to_list fn.Ast.ret) + in + vars, lens (* Bind the variables in a parameter's written type from the type an argument turned out to have. Odin's [is_polymorphic_type_assignable], structurally @@ -2185,6 +2637,17 @@ let rec bind_ty ?(widen = false) ?(ro = true) subst (pat : Types.t) | Types.Fn (ps, r), Types.CFn (ps', r') when widen -> List.length ps = List.length ps' && List.for_all2 inner ps ps' && inner r r' + (* A length variable's array against a concrete one: the length is bound + the way a type variable is, to a [Types.Len]. *) + | Types.LArray (v, p), Types.Array (n, a) -> + bind_ty ~ro:false subst (Types.Var v) (Types.Len n) && inner p a + (* A struct copy at variables against a copy of the same template: each + argument against its own. *) + | Types.Named p, Types.Named a -> + (match Hashtbl.find_opt struct_apps p, Hashtbl.find_opt struct_apps a with + | Some (g, ps), Some (h, as_) when String.equal g h -> + List.length ps = List.length as_ && List.for_all2 inner ps as_ + | _ -> Types.fits ~expected:pat ~actual:arg) (* Nothing generic left on the pattern side: this is ordinary type equality, and [Never] fits anywhere exactly as it does elsewhere. *) | p, a -> Types.fits ~expected:p ~actual:a @@ -2201,12 +2664,29 @@ let rec subst_ty subst (t : Types.t) = | Types.Fn (ps, r) -> Types.Fn (List.map (subst_ty subst) ps, subst_ty subst r) | Types.CFn (ps, r) -> Types.CFn (List.map (subst_ty subst) ps, subst_ty subst r) + | Types.LArray (v, e) -> + (match List.assoc_opt v subst with + | Some (Types.Len n) -> Types.Array (n, subst_ty subst e) + | Some (Types.Var w) -> Types.LArray (w, subst_ty subst e) + | _ -> Types.LArray (v, subst_ty subst e)) + (* A struct copy at variables becomes the copy at what they are bound to. + Only its key is made here — there is no env to lay it out in — and + [realise] makes the copy itself before anything reads its fields. *) + | Types.Named k -> + (match Hashtbl.find_opt struct_apps k with + | Some (g, args) when List.exists open_ty args -> + let args = List.map (subst_ty subst) args in + Types.Named (struct_app g args) + | _ -> t) | t -> t -(* Does this resolved type still mention a variable? *) -let rec generic_ty (t : Types.t) = +(* Does this resolved type still mention a variable? Not through a struct + copy's arguments: an operator over a [(Pair $t)] is refused as one over a + struct, not as one over a type variable. [open_ty] is the question that + does look through, for binding and substituting. *) +and generic_ty (t : Types.t) = match t with - | Types.Var _ -> true + | Types.Var _ | Types.LArray _ -> true | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e | Types.Option e -> generic_ty e | Types.Map (k, v) -> generic_ty k || generic_ty v @@ -2214,6 +2694,37 @@ let rec generic_ty (t : Types.t) = List.exists generic_ty ps || generic_ty r | _ -> false +and open_ty (t : Types.t) = + match t with + | Types.Var _ | Types.LArray _ -> true + | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e + | Types.Option e -> open_ty e + | Types.Map (k, v) -> open_ty k || open_ty v + | Types.Fn (ps, r) | Types.CFn (ps, r) -> List.exists open_ty ps || open_ty r + | Types.Named k -> + (match Hashtbl.find_opt struct_apps k with + | Some (_, args) -> List.exists open_ty args + | None -> false) + | _ -> false + +(* Make every struct copy [t] names that [subst_ty] only named. A copy has + to exist in [env.structs] before a field of it is read, and [subst_ty] has + no env to make one in. *) +let rec realise env loc (t : Types.t) = + match t with + | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e + | Types.Option e | Types.LArray (_, e) -> realise env loc e + | Types.Map (k, v) -> realise env loc k; realise env loc v + | Types.Fn (ps, r) | Types.CFn (ps, r) -> + List.iter (realise env loc) ps; realise env loc r + | Types.Named k when not (Hashtbl.mem env.structs k) -> + (match Hashtbl.find_opt struct_apps k with + | Some (g, args) when Hashtbl.mem env.gstructs g -> + List.iter (realise env loc) args; + ignore (struct_copy env loc g args) + | _ -> ()) + | _ -> () + (* Does a type a call site bound a variable to reach a [dyn] anywhere? See the refusal in [generic_call]: [dyn] is a concrete type and substitutes like any other, so nothing stopped a copy being made at it, and the copies walked @@ -2247,36 +2758,12 @@ let unconstrained env loc op ~needs (t : Types.t) = | _ -> Loc.failk "check/unconstrained-type-variable" loc "%s over the type variable %s: nothing declares %s %s. Write \ - {:where (%s $%s)} at the head of the body, or take the operation as \ + {:where (%s %s)} at the head of the body, or take the operation as \ a parameter, a (Fn [%s %s] ...), and call it here" op (Types.to_string t) (Types.to_string t) needs needs (Types.to_string t) (Types.to_string t) (Types.to_string t) -(* How a concrete type is spelled inside an instantiation's name. The prelude - already writes this by hand — [filter-i32], [sum-f32], [append-i64] — so a - generated name reads like the handwritten one it replaces, which is what a - backtrace, a [Reach] edge and a dev-build cell all end up showing. - [Types.to_string] cannot serve: [[i32]] and [(Vec i32)] are not symbols. *) -let rec mangle_ty (t : Types.t) = - match t with - | Types.Unit -> "unit" - | Types.Slice (Types.Mut, e) -> "slice-" ^ mangle_ty e - | Types.Slice (Types.Const, e) -> "cslice-" ^ mangle_ty e - | Types.Array (n, e) -> Printf.sprintf "arr%Ld-%s" n (mangle_ty e) - | Types.Map (k, v) -> Printf.sprintf "map-%s-%s" (mangle_ty k) (mangle_ty v) - | Types.Ptr (Types.Mut, e) -> "ptr-" ^ mangle_ty e - | Types.Ptr (Types.Const, e) -> "cptr-" ^ mangle_ty e - | Types.Vec e -> "vec-" ^ mangle_ty e - | Types.Option e -> "opt-" ^ mangle_ty e - | Types.Fn (ps, r) -> - Printf.sprintf "fn-%s-to-%s" - (String.concat "-" (List.map mangle_ty ps)) (mangle_ty r) - | Types.CFn (ps, r) -> - Printf.sprintf "cfn-%s-to-%s" - (String.concat "-" (List.map mangle_ty ps)) (mangle_ty r) - | t -> Types.to_string t - (* ── The runaway instantiation, refused by name rather than by depth ──── [(defn grow [x $t] () (grow [x x]))] asks for a copy at [[t]], which asks for one at [[[t]]], forever. Before this the checker did not fail, it @@ -2303,23 +2790,6 @@ let rec mangle_ty (t : Types.t) = is the whole design. The depth backstop below stays as a backstop only: it catches a growth this test does not recognise, and it is never the thing the message is about. *) -let rec occurs_in ~needle (t : Types.t) = - Types.equal needle t - || - match t with - | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e - | Types.Option e -> occurs_in ~needle e - | Types.Map (k, v) -> occurs_in ~needle k || occurs_in ~needle v - | Types.Fn (ps, r) | Types.CFn (ps, r) -> - List.exists (occurs_in ~needle) ps || occurs_in ~needle r - | _ -> false - -(* [b] is [a] with something built around it: same shape, strictly bigger. *) -let grows ~from_:a ~to_:b = - List.length a = List.length b - && List.for_all2 (fun x y -> occurs_in ~needle:x y) a b - && not (List.for_all2 Types.equal a b) - let runaway env loc gname cparams = let chain_text () = String.concat "\n " @@ -3399,7 +3869,8 @@ let box loc (e : Tast.expr) : Tast.expr = caller that starts doing that gets a sentence instead of a silent mis-lowering. *) | Types.Named _ | Types.Enum _ | Types.Option _ | Types.Ptr _ - | Types.Alloc | Types.Fn _ | Types.CFn _ | Types.Var _ -> + | Types.Alloc | Types.Fn _ | Types.CFn _ | Types.Var _ | Types.Len _ + | Types.LArray _ -> no_dyn_yet loc ~into:true e.Tast.ty "" let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr = @@ -3564,6 +4035,11 @@ let box_option ctx loc (t : Types.t) (got : Tast.expr) : Tast.expr = (Tast.Let ([ (s, got) ], [ mk loc Types.Dyn (Tast.If (is_some, some_dyn, none_dyn)) ])) +(* [=] over a dyn pair, answering a bool. Shared by the [=] builtin and a + literal [match] over a dyn, which is (= t lit) by definition. *) +let dyn_eq loc u v = + unbox loc Types.Bool (rt loc Types.Dyn "flan_dyn_eq" [ box loc u; box loc v ]) + let unbox_option ctx loc (t : Types.t) (got : Tast.expr) : Tast.expr = let oty = Types.Option t in match t with @@ -4060,7 +4536,7 @@ let rec key_pair env loc (k : Types.t) : Tast.fnref * Tast.fnref = to that is still a refusal rather than a guessed pair. *) | Types.Var v -> Loc.failk "check/generic-map-key" loc - "a map keyed by the type variable %s has no hash and no equality here. \ + "a map keyed by the type variable $%s has no hash and no equality here. \ Write {:where (hashable? $%s)} at the head of the body, or write the \ operation in a function over the concrete key type and call that" v v | Types.String -> Tast.Rtfn "flan_hash_str", Tast.Rtfn "flan_eq_str" @@ -4796,14 +5272,14 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = | Some (Types.Int k) -> not (Types.signed k) | _ -> false) -> let t = Option.get want in - let gname, _, at = List.nth ctx.env.chain (List.length ctx.env.chain - 1) in + (* [instantiate] adds the note naming the call that asked for this copy. *) + let gname, _, _ = List.nth ctx.env.chain (List.length ctx.env.chain - 1) in let var = match List.find_opt (fun (_, u) -> Types.equal u t) ctx.env.subst with | Some (v, _) -> Printf.sprintf "$%s = %s" v (Types.to_string t) | None -> Types.to_string t in Loc.failk literal_at_want loc - ~notes:[ Loc.note at (Printf.sprintf "%s is instantiated at %s here" gname var) ] "%Ld does not fit in %s, which holds no negative number, and %s is called \ at %s — the body has to work at every type it is called at, so write \ it with no negative literal, as in (- x %Ld) in place of (+ x %Ld)" @@ -4947,6 +5423,23 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = (mk loc Types.Dyn (Tast.Let ([ (m, empty) ], sets @ [ mval ]))) | Ast.Quote _ -> unimplemented loc "a quoted symbol (restart names)" 6 + (* A length variable read as a value is the integer it was bound to, as a + literal — so it takes its width from where it stands, the way a written + 8 would. In the abstract pass it is a 1: a literal that fits every + integer type, since the real one is answered again per copy. A local of + the same name shadows it. *) + | Ast.Var name + when (not (List.mem_assoc name ctx.scope)) + && (List.mem name ctx.env.lenvars + || (match List.assoc_opt name ctx.env.subst with + | Some (Types.Len _) -> true + | _ -> false)) -> + let n = + match List.assoc_opt name ctx.env.subst with + | Some (Types.Len n) -> n + | _ -> 1L + in + check ctx ?want { e with Ast.e = Ast.Int n } | Ast.Var name -> var ctx loc ~want name | Ast.Do body -> ctx.tail <- tail; block ctx ?want loc body (* [defer_ok] rides through: a [let] at the top level of a function body has @@ -5087,7 +5580,7 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = (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" sname name + "%s has no field %s" (Types.to_string (Types.Named sname)) name | Some i -> let fty = (List.nth s.Tast.fields i).Tast.fty in expect ctx loc ~want (mk loc fty (Tast.Field (target, i)))) @@ -6812,6 +7305,167 @@ and callable ctx name = | Some b -> (match b.bty with Types.Fn _ -> true | _ -> false) | None -> false) +(* [(Pair 1 2)] and [(Pair {.a 1 .b 2})]: which copy of a generic struct a + value builds. The position says, when a copy of this struct is wanted + there; otherwise the fields do, each given one's type binding the + template's variables the way a generic call's arguments bind its own. The + fields are only probed here — each check is abandoned — and the ordinary + constructor checks them again against the copy it is handed. *) +and generic_ctor ctx ~want loc name given = + let env = ctx.env in + let g = Hashtbl.find env.gstructs name in + match want with + | Some (Types.Named k) + when (match Hashtbl.find_opt struct_apps k with + | Some (h, _) -> String.equal h name + | None -> false) -> + realise env loc (Types.Named k); k + | _ -> + let open_key = + struct_copy env loc name (List.map (fun (p, _) -> Types.Var p) g.gparams) + in + let fields = (Hashtbl.find env.structs open_key).Tast.fields in + let pairs = + match given with + | `Positional args when List.length args = List.length fields -> + List.combine fields args + (* The wrong number of fields: the copy at variables is handed on, and + the constructor says what is wrong with the count in its own words. *) + | `Positional _ -> [] + | `Named kvs -> + List.filter_map + (fun (f, v) -> + List.find_opt + (fun (fl : Tast.field) -> String.equal fl.Tast.fname f) fields + |> Option.map (fun fl -> (fl, v))) + kvs + in + let subst = ref [] and unsure = ref [] in + (* An untyped literal has no type of its own to bring, so the fields that + do have one bind first: [(Node 2 (addr c))] over a [(Node i64)] [c] is + a [(Node i64)], and the 2 takes its width from that. *) + let literal (a : Ast.expr) = + match a.Ast.e with + | Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ -> true + | _ -> false + in + (* A literal's own type, the one it has with nothing expected of it. *) + let literal_type (a : Ast.expr) = + match a.Ast.e with + | Ast.Float _ -> Types.Float Types.F64 + | Ast.UInt _ -> Types.Int Types.U64 + | Ast.Byte _ -> Types.Int Types.U8 + | _ -> Types.Int Types.I32 + in + let pairs = + List.filter (fun (_, a) -> not (literal a)) pairs + @ List.filter (fun (_, a) -> literal a) pairs + in + (* Variables only literals have bound so far: a later literal may widen + them, as a generic call's literal arguments meet at the wider type — + [(Pair 1 2.5)] is a [(Pair f64)]. *) + let lit_only = ref [] in + (* Which field's value decided each variable, for the refusal of a + literal that does not fit what it decided. *) + let decided_by = ref [] in + List.iter + (fun ((f : Tast.field), (a : Ast.expr)) -> + match f.Tast.fty with + (* A literal at a variable a typed field already decided: it has to + be usable at that type, and when it is not the refusal names the + field that decided it. *) + | Types.Var v + when literal a && List.mem_assoc v !subst + && not (List.mem v !lit_only) -> + let b = List.assoc v !subst in + (match a.Ast.e, b with + | Ast.Float x, Types.Int _ -> + let notes = + match List.assoc_opt v !decided_by with + | Some (fname, at) -> + [ Loc.note at + (Printf.sprintf ".%s is %s here, which decides $%s" fname + (Types.to_string b) v) ] + | None -> [] + in + Loc.failk "check/generic-struct-field" a.Ast.loc ~notes + "%s's .%s is $%s, which is %s here, and %g is a float literal. \ + Write .%s as an integer, or give .%s a float type" + name f.Tast.fname v (Types.to_string b) x f.Tast.fname + (match List.assoc_opt v !decided_by with + | Some (fname, _) -> fname + | None -> f.Tast.fname) + | _ -> ()) + | Types.Var v when literal a && not (List.mem_assoc v !subst && not (List.mem v !lit_only)) -> + let t = (literal_type a) in + (match List.assoc_opt v !subst with + | None -> subst := (v, t) :: !subst; lit_only := v :: !lit_only + | Some b -> + (match Types.join b t with + | Some j -> subst := (v, j) :: List.remove_assoc v !subst + | None -> + fail a.Ast.loc "%s's .%s is %s here, and this is %s" + (Types.to_string (Types.Named open_key)) f.Tast.fname + (Types.to_string b) (Types.to_string t))) + | _ -> + if open_ty f.Tast.fty + && not (literal a && subst_ty !subst f.Tast.fty |> open_ty |> not) + then begin + let seen = ref None in + let probe () = + seen := Some (check ctx a).Tast.ty; + Loc.fail a.Ast.loc "probe" + in + let refusal = match trial ctx probe with Error d -> Some d | Ok _ -> None in + match !seen with + (* No type of its own — [None], a bare {.field v} — is no + evidence; the constructor checks it against the copy the other + fields decide, and its refusal is the one given if they decide + nothing. *) + | None -> Option.iter (fun d -> unsure := d :: !unsure) refusal + | Some t -> + let before = !subst in + if bind_ty subst f.Tast.fty t then + List.iter + (fun (v, _) -> + if not (List.mem_assoc v before) then + decided_by := (v, (f.Tast.fname, a.Ast.loc)) :: !decided_by) + !subst + else + fail a.Ast.loc "%s's .%s is %s here, and this is %s" + (Types.to_string (Types.Named open_key)) f.Tast.fname + (Types.to_string (subst_ty !subst f.Tast.fty)) + (Types.to_string t) + end) + pairs; + (match given with + | `Positional args when List.length args <> List.length fields -> open_key + | _ -> + let targs = + List.map + (fun (p, _) -> + match List.assoc_opt p !subst with + | Some t -> t + | None -> + (match List.rev !unsure with + | d :: _ -> Loc.raise_diag d + | [] -> ()); + Loc.failk "check/generic-struct-undetermined" loc + ~notes:[ Loc.note g.gloc (name ^ " is declared here") ] + "%s's $%s is not decided by the fields given here. Name the \ + type where the value goes, as in (the (%s %s) ...)" + name p name + (String.concat " " + (List.map + (fun (q, is_len) -> + if env.tyvars <> [] then "$" ^ q + else if is_len then "8" + else "i32") + g.gparams))) + g.gparams + in + struct_copy env loc name targs) + (* [(Cell 1 2)] — a struct built from its fields in declaration order. The parser cannot make this one either, and for a sharper reason than the @@ -6841,6 +7495,14 @@ and positional_struct ctx ~want loc name args = let n = List.length fields in let given = List.length args in let note = declared_note ctx.env name in + (* The constructor is written with the template's name for a generic + struct's copy, and the copy is spoken of as [(Pair i32)]. *) + let ctor = + match Hashtbl.find_opt struct_apps name with + | Some (g, _) when Hashtbl.mem ctx.env.copies name -> g + | _ -> name + in + let shown = Types.to_string (Types.Named name) in if given < n then begin let missing = List.nth fields given in Loc.failk "check/positional-too-few" loc ~notes:note @@ -6848,15 +7510,15 @@ and positional_struct ctx ~want loc name args = Positional construction gives every field, in declaration order; to \ give some of them and zero the rest, a struct value is written (%s \ {.field value ...})" - name n (if n = 1 then "" else "s") given - (if given = 1 then "was" else "were") missing.Tast.fname name + shown n (if n = 1 then "" else "s") given + (if given = 1 then "was" else "were") missing.Tast.fname ctor end; if given > n then begin let extra = List.nth args n in Loc.failk "check/positional-too-many" extra.Ast.loc ~notes:note "%s has %d field%s, and this is argument %d — a struct value is written \ (%s {.field value ...}) or (%s %s)" - name n (if n = 1 then "" else "s") (n + 1) name name + shown n (if n = 1 then "" else "s") (n + 1) ctor ctor (String.concat " " (List.map (fun (f : Tast.field) -> f.Tast.fname) fields)) end; (* Left to right, each against its own field's type, exactly as the argument @@ -6881,7 +7543,7 @@ and positional_struct ctx ~want loc name args = Loc.notes = d.Loc.notes @ [ Loc.note a.Ast.loc - (Printf.sprintf "this is %s's field .%s" name + (Printf.sprintf "this is %s's field .%s" shown f.Tast.fname) ] @ note }) | Loc.Error d -> refuse_or_poison ctx.env a.Ast.loc d) @@ -6942,6 +7604,9 @@ and check_bare ctx ~want loc kvs = what lets the decision be made against the tables, exactly. *) and check_struct ctx ~want loc name kvs = match Hashtbl.find_opt ctx.env.structs name with + | None when Hashtbl.mem ctx.env.gstructs name -> + check_struct ctx ~want loc + (generic_ctor ctx ~want loc name (`Named kvs)) kvs | None when Hashtbl.mem ctx.env.unions name -> check_union ctx ~want loc name kvs | None -> @@ -7017,7 +7682,7 @@ and check_struct ctx ~want loc name kvs = if Tast.field_index s k = None then Loc.failk "check/unknown-field" v.Ast.loc ~notes:(declared_note ctx.env name) - "%s has no field %s" name k) + "%s has no field %s" (Types.to_string (Types.Named name)) k) in let fields = zii_fill ctx loc seen s.Tast.fields in expect ctx loc ~want (mk loc (Types.Named name) (Tast.Make (name, fields))) @@ -7353,9 +8018,7 @@ and mixed_refusal : 'a. ctx -> Ast.expr list -> Loc.diag -> 'a = (Printf.sprintf "this array's first element is %s, so every \ element is" - (match first.Tast.ty with - | Types.Var v -> "$" ^ v - | t -> Types.to_string t)) ] })) + (Types.to_string first.Tast.ty)) ] })) rest; raise (Loc.Error d) @@ -7669,10 +8332,52 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = "%s is a union, and nothing in one records which member was written, \ so there is nothing to match on. Read the member you mean with \ (.member u), or use a defdata" n + (* A number, a string or a dyn: the arms are literals, and each is the + test (= t lit) over one temporary — the enum's chain, with [=]'s own + two lowerings for the test, so a match over a dyn means what [=] over + it means. [is_equatable]'s set minus the enums, which are above. *) + | (Types.Int _ | Types.Float _ | Types.String | Types.Dyn) as t -> `Lit t | other -> - fail loc "match works on an Option, a data type or an enum, not on %s" + fail loc + "match works on an Option, a data type, an enum, a number, a string \ + or a dyn, not on %s" (Types.to_string other) in + (* A literal arm, spelled as it was written, for the refusals that name one. *) + let spell (e : Ast.expr) = + match e.Ast.e with + | Ast.Int n -> Int64.to_string n + | Ast.UInt (_, t) -> t + | Ast.Float x when Float.is_integer x && Float.abs x < 1e15 -> + Printf.sprintf "%.1f" x + | Ast.Float x -> + (* The shortest spelling that reads back as the same float. *) + let rec go p = + let t = Printf.sprintf "%.*g" p x in + if p >= 17 || float_of_string t = x then t else go (p + 1) + in + go 1 + | Ast.Byte b when b > 32 && b < 127 -> Printf.sprintf "\\%c" (Char.chr b) + | Ast.Byte b -> string_of_int b + | Ast.Str t -> Printf.sprintf "%S" t + | _ -> "this literal" + in + let what_ty t = match t with Types.Dyn -> "a dyn" | t -> Types.to_string t in + (* A literal match that compiles, over the scrutinee's own name where it + has one, for the refusals that need to show the shape. *) + let lit_arms_fix t = + let name = + match scrutinee.Ast.e with Ast.Var n -> n | _ -> "t" + in + Printf.sprintf "(match %s %s)" name + (match t with + | Types.String -> "\"yes\" 1 _ 0" + | Types.Float _ -> "0.5 1 _ 0" + | _ -> "5 1 _ 0") + in + (* The checked literal of each literal arm, by the key [resolve_pat] gave it. *) + let lits : (string, Tast.expr) Hashtbl.t = Hashtbl.create 8 in + let lit_values = ref [] in (* Which case each arm names, and the type of each name it binds. This is the whole of what differs between the two subjects; everything below it is shared. *) @@ -7704,6 +8409,114 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = "this match is over the enum %s, and %s is not one of its members. An \ arm names a member as a keyword: %s" n c (String.concat " " (List.map (fun (m, _) -> ":" ^ m) members)) + | `Lit t, Ast.Plit e -> + let v = + (* [=]'s dyn pair checks its literal at dyn, which boxes it. *) + match trial ctx (fun () -> check ctx ~want:t e) with + | Ok v -> v + | Error _ -> + (* A literal that does not fit is refused, where [=] would widen + the pair and let the arm quietly never match. The literal's + own refusal is not repeated: its fixes are casts, and a cast + is not a pattern. *) + (match t with + | Types.Dyn -> + fail a.Ast.aloc + "this match is over a dyn, which holds a number as an i64 or \ + an f64, and %s fits in neither. Change the arm to a value an \ + i64 holds, or remove it" (spell e) + | _ -> ()); + let tn = Types.to_string t in + let an = + match tn.[0] with + | 'a' | 'e' | 'f' | 'i' | 'o' -> "an " ^ tn + | _ -> "a " ^ tn + in + let why = + match e.Ast.e, t with + | Ast.Str _, _ -> "is a string" + | _, Types.String -> "is a number" + | Ast.Float x, Types.Int _ when not (Float.is_integer x) -> + "is not a whole number" + | Ast.Float _, Types.Int _ -> "is a float" + | _ -> "does not fit in one" + in + fail a.Ast.aloc + "this match is over %s, so each arm has to be %s, and %s %s. \ + Change the arm to a value %s holds, or remove it" + tn an (spell e) why an + in + (* The arm's value at the scrutinee's type, and a second arm [=] could + not tell from an earlier one is refused, since it can never be + reached: 97 and \a are one u8, 0.1 and 0.10000000001 are one f32, + and over a dyn 1 and 1.0 are equal. Compared pairwise rather than + hashed, because dyn = between an integer and a float goes through + the float and is not transitive past 2^53. *) + let value = + let f32 x = Int32.float_of_bits (Int32.bits_of_float x) in + let num x = + match t with Types.Float Types.F32 -> `F (f32 x) | _ -> `F x + in + match e.Ast.e, t with + | Ast.Str s, _ -> `S s + | (Ast.Int n | Ast.UInt (n, _)), Types.Float _ -> num (Int64.to_float n) + | Ast.Byte b, Types.Float _ -> num (float_of_int b) + | (Ast.Int n | Ast.UInt (n, _)), _ -> `I n + | Ast.Byte b, _ -> `I (Int64.of_int b) + | Ast.Float x, _ -> num x + | _ -> assert false + in + let same x y = + match x, y with + | `I a, `I b -> Int64.equal a b + | `F a, `F b -> a = b + | `I a, `F b | `F b, `I a -> Int64.to_float a = b + | `S a, `S b -> String.equal a b + | _ -> false + in + (match List.find_opt (fun (w, _) -> same value w) !lit_values with + | Some (_, earlier) when earlier = spell e -> + fail a.Ast.aloc "this match has two %s arms" earlier + | Some (_, earlier) -> + fail a.Ast.aloc + "this match has two %s arms — %s equals it as %s, so this arm is \ + never reached. Remove it" + earlier (spell e) + (match t with + | Types.Dyn -> "a dyn" + | t -> + let tn = Types.to_string t in + (match tn.[0] with + | 'a' | 'e' | 'f' | 'i' | 'o' -> "an " ^ tn + | _ -> "a " ^ tn)) + | None -> ()); + lit_values := (value, spell e) :: !lit_values; + let key = string_of_int (Hashtbl.length lits) in + Hashtbl.replace lits key v; + Some key, [] + | `Lit t, Ast.Pkw k -> + fail a.Ast.aloc + ":%s is an enum member, and this match is over %s, whose arms are \ + literals, as in %s" k (what_ty t) (lit_arms_fix t) + | `Lit t, Ast.Pctor (c, _) -> + fail a.Ast.aloc + "%s names a case, and this match is over %s, whose arms are \ + literals, as in %s" c (what_ty t) (lit_arms_fix t) + | `Option _, Ast.Plit e -> + fail a.Ast.aloc + "%s is a literal, and this match is over an Option, whose arms are \ + (Some x) and None" (spell e) + | `Enum (n, members), Ast.Plit e -> + fail a.Ast.aloc + "%s is a literal, and this match is over the enum %s, whose arms \ + name its members as keywords: %s" (spell e) n + (String.concat " " (List.map (fun (m, _) -> ":" ^ m) members)) + | `Data u, Ast.Plit e -> + fail a.Ast.aloc + "%s is a literal, and this match is over the data type %s, whose \ + arms name its cases: %s" (spell e) u.Tast.dname + (String.concat ", " + (List.map (fun (v : Tast.variant) -> v.Tast.vname) u.Tast.cases)) | `Option _, Ast.Pkw k -> fail a.Ast.aloc ":%s is an enum member, and this match is over an Option, whose arms \ @@ -7764,7 +8577,10 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = | Some c -> if Hashtbl.mem seen c then fail a.Ast.aloc "this match has two %s arms" - (match subject with `Enum _ -> ":" ^ c | _ -> c); + (match subject, a.Ast.pat with + | `Enum _, _ -> ":" ^ c + | _, Ast.Plit e -> spell e + | _ -> c); Hashtbl.add seen c ()); (a, ctor, binds)) arms @@ -7866,7 +8682,19 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = List.filter_map (fun (m, _) -> if Hashtbl.mem seen m then None else Some (":" ^ m)) members + | `Lit _ -> [] in + (* No list of literals covers a number, a string or a dyn, so a literal + match always needs its [_]. Refused here, before the chain below, which + would otherwise run a lone last arm untested as the enum's does. *) + (match subject with + | `Lit t when not !saw_wild -> + Loc.failk "check/non-exhaustive-match" loc + "this match is not exhaustive — its arms are literals, and no list of \ + them covers every %s. Add a _ arm for the rest, as in %s" + (match t with Types.Dyn -> "dyn value" | t -> Types.to_string t) + (lit_arms_fix t) + | _ -> ()); if not !saw_wild && missing <> [] then (* The data type's declaration, because that is where the case list this match failed to cover actually lives, and because adding a case there is what @@ -7875,7 +8703,7 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = ~notes:(match subject with | `Data u -> declared_note ctx.env u.Tast.dname | `Enum (n, _) -> declared_note ctx.env n - | `Option _ -> []) + | `Option _ | `Lit _ -> []) "this match is not exhaustive — %s %s no arm. Add %s, or a _ arm for \ the rest" (String.concat ", " missing) @@ -7884,7 +8712,7 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = let ty = match !want with Some t -> t | None -> Types.Never in match subject with | `Option _ | `Data _ -> mk loc ty (Tast.Match (s, arms)) - | `Enum (_, members) -> + | `Enum _ | `Lit _ -> (* The scrutinee once, into a temporary, and then an [if] per arm in the order written. A [_] arm ends the chain, and so does the last arm of a match with none: it is exhaustive by the check above, so the last @@ -7900,10 +8728,18 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = | ({ Tast.acase = None; _ } as a) :: _ -> body a | [ a ] -> body a | ({ Tast.acase = Some m; _ } as a) :: rest -> - let v = mk loc s.Tast.ty (Tast.Int (List.assoc m members, Types.I32)) in - mk loc ty - (Tast.If (mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; v ])), - body a, chain rest)) + let test = + match subject with + | `Enum (_, members) -> + let v = + mk loc s.Tast.ty (Tast.Int (List.assoc m members, Types.I32)) + in + mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; v ])) + | `Lit Types.Dyn -> dyn_eq loc local (Hashtbl.find lits m) + | _ -> + mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; Hashtbl.find lits m ])) + in + mk loc ty (Tast.If (test, body a, chain rest)) in mk loc ty (Tast.Let ([ (slot, s) ], [ chain arms ])) @@ -8224,7 +9060,7 @@ and check_place ?(store = true) ctx loc (p : Ast.place) : Tast.place * Types.t = (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" sname name + "%s has no field %s" (Types.to_string (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) @@ -8841,7 +9677,7 @@ and global_value ctx n = (* An argument written as a type: a type expression, or a bare name that is a type and not a local or a global of the same spelling. *) and type_arg ctx (a : Ast.expr) = - type_of_expr a <> None + type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a <> None || (match a.Ast.e with | Ast.Var n -> lookup ctx n = None && not (global_value ctx n) && type_named ctx n @@ -8874,8 +9710,11 @@ and type_named ctx n = and vec_new_elem ctx ~want loc args = let named = match args with - | a :: rest when type_of_expr a <> None -> - Some (resolve ctx.env (Option.get (type_of_expr a)), rest) + | a :: rest when type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a <> None -> + Some + (resolve ctx.env + (Option.get (type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a)), + rest) | { Ast.e = Ast.Var n; _ } :: rest when lookup ctx n = None && not (global_value ctx n) && type_named ctx n -> Some (resolve_name ctx.env ~seen:[] loc n, rest) @@ -8896,12 +9735,12 @@ and vec_new_elem ctx ~want loc args = brackets — an allocator is never an array — or a parenthesised Ptr, Option, Vec, Map, Fn or CFn. A bare name is not one of them, because there it may be an allocator's name; the callers ask about that themselves. *) -and type_of_expr (e : Ast.expr) : Ast.texpr option = +and type_of_expr ?(generic = fun _ -> false) (e : Ast.expr) : Ast.texpr option = let mk t = { Ast.t; tloc = e.Ast.loc } in let inner (e : Ast.expr) = match e.Ast.e with | Ast.Var s -> Some { Ast.t = Ast.Tname s; tloc = e.Ast.loc } - | _ -> type_of_expr e + | _ -> type_of_expr ~generic e in let all es = let ts = List.filter_map inner es in @@ -8925,6 +9764,18 @@ and type_of_expr (e : Ast.expr) : Ast.texpr option = | Ast.Call ({ Ast.e = Ast.Var (("Ptr" | "Option" | "Vec" | "Map") as c); _ }, (_ :: _ as args)) -> Option.map (fun ts -> mk (Ast.Tapp (c, ts))) (all args) + (* A generic struct applied to its arguments, [(vec-new (Small 8 i32))]: + the caller says which heads are ones, since only the env knows. An + integer argument is a length. *) + | Ast.Call ({ Ast.e = Ast.Var c; _ }, (_ :: _ as args)) when generic c -> + let arg (a : Ast.expr) = + match a.Ast.e with + | Ast.Int n -> Some { Ast.t = Ast.Tlen n; tloc = a.Ast.loc } + | _ -> inner a + in + let ts = List.filter_map arg args in + if List.length ts = List.length args then Some (mk (Ast.Tapp (c, ts))) + else None | _ -> None (* The key and value types, or the reason this is not a Map. *) @@ -8945,7 +9796,7 @@ and map_new_types ctx ~want loc args = (* A type position holds a bare name or a type expression Parse has read as one, as [vec-new]'s does. *) let as_type (a : Ast.expr) = - match a.Ast.e, type_of_expr a with + match a.Ast.e, type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a with | _, Some t -> Some (resolve ctx.env t) | Ast.Var n, None when is_type n -> Some (resolve_name ctx.env ~seen:[] loc n) | _ -> None @@ -8953,7 +9804,7 @@ and map_new_types ctx ~want loc args = match args with | k :: v :: rest when as_type k <> None && as_type v <> None -> Option.get (as_type k), Option.get (as_type v), rest - | a :: _ when type_of_expr a <> None -> + | a :: _ when type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a <> None -> fail loc "(map-new) names a key and no value — write both, as (map-new string \ i32)" @@ -9263,7 +10114,8 @@ and named_call ?(qualified = false) ctx ~want loc name args = and the negation is an [i1] flip the backend folds away. *) let link u v = let cmp = - unbox loc Types.Bool (rt loc Types.Dyn sym ([ u; v ] @ site)) + if String.equal sym "flan_dyn_eq" then dyn_eq loc u v + else unbox loc Types.Bool (rt loc Types.Dyn sym ([ u; v ] @ site)) in if String.equal name "!=" then mk loc Types.Bool (Tast.Prim (Tast.Not, [ cmp ])) @@ -9440,7 +10292,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = fail (List.hd args).Ast.loc "%s takes a type, as in (%s i32)" name name; let a = List.hd args in let ty = - match type_of_expr a, a.Ast.e with + match type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a, a.Ast.e with | Some t, _ -> resolve ctx.env t | _, Ast.Var n -> resolve_name ctx.env ~seen:[] a.Ast.loc n | _ -> fail a.Ast.loc "internal: %s's type argument is not a type" name @@ -11235,7 +12087,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = checked with [t] concrete. One generic argument defers the whole call: the printers for its neighbours would be re-selected at instantiation anyway, so building them here would be work thrown away twice. *) - if List.exists (fun a -> generic_ty a.Tast.ty) checked then + if List.exists (fun a -> open_ty a.Tast.ty) checked then mk loc Types.Unit Tast.Unit else let bslice = Types.Slice (Types.Mut, (Types.Int Types.U8)) in @@ -11264,7 +12116,20 @@ and named_call ?(qualified = false) ctx ~want loc name args = match a.Tast.ty with | Types.String | Types.Slice (_, (Types.Int Types.U8)) -> [ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ a ]))) ] - | _ -> Render.render rc 0 a + (* The walk names the value once per piece it reads — an option's tag + and then its payload, each field of a struct — so anything but a + plain variable is bound to a slot first, or [(println (pop! s))] + pops once per piece. *) + | _ -> + (match a.Tast.e with + | Tast.Local _ | Tast.Global _ -> Render.render rc 0 a + | _ -> + let s = fresh_slot ctx a.Tast.ty in + [ mk loc Types.Unit + (Tast.Let + ([ (s, a) ], + Render.render rc 0 (mk a.Tast.loc a.Tast.ty (Tast.Local s)))) + ]) in (* Built fresh per use rather than shared: nothing else in this file puts one node in two places of a tree, and a pass that hangs state off a @@ -11307,7 +12172,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = arity ctx loc name 2 args; let label = check ctx ~want:Types.String (List.hd args) in let v = check ctx (List.nth args 1) in - if generic_ty v.Tast.ty then mk loc Types.Unit Tast.Unit + if open_ty v.Tast.ty then mk loc Types.Unit Tast.Unit else begin let unit_rt sym args = mk loc Types.Unit (Tast.Prim (Tast.Rt sym, args)) in let bslice = Types.Slice (Types.Mut, (Types.Int Types.U8)) in @@ -11621,6 +12486,10 @@ and ordinary_call ctx ~want loc name args = name dname dname c.Tast.vname dname c.Tast.vname else if Hashtbl.mem ctx.env.structs name then positional_struct ctx ~want loc name args + else if Hashtbl.mem ctx.env.gstructs name then + positional_struct ctx ~want loc + (generic_ctor ctx ~want loc name + (`Positional args)) args else if List.mem_assoc name operator_aliases then (* Asked before the package test, because [/=] and [=/=] have a slash in them and are not package calls. The did-you-mean cannot reach @@ -11750,10 +12619,10 @@ and ordinary_call ctx ~want loc name args = same thing here so the answer does not depend on which side of the fork the form fell down. *) Loc.failk "check/unknown-function" loc - "unknown function %s. A capitalised name is a type, and (%s \ - ...) is a generic type, which is not there yet — a generic \ - function is, written with $t in its parameter vector" - name name + "unknown function %s. A capitalised name is a type, and no \ + struct or generic struct %s is declared — a generic struct is \ + one whose fields introduce $t, as in (defstruct %s [x $t])" + name name name else Loc.failk "check/unknown-function" loc "unknown function %s" name (* Does the program's own definition of this name take this call over? @@ -11907,9 +12776,14 @@ and generic_call ctx ~want loc name vars pats pret args = | Types.Var u -> String.equal u v | Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e | Types.Option e -> mentions v e + | Types.LArray (u, e) -> String.equal u v || mentions v e | Types.Map (k, w) -> mentions v k || mentions v w | Types.Fn (ps, r) | Types.CFn (ps, r) -> List.exists (mentions v) ps || mentions v r + | Types.Named k -> + (match Hashtbl.find_opt struct_apps k with + | Some (_, args) -> List.exists (mentions v) args + | None -> false) | _ -> false in let bound_exactly v = @@ -11937,7 +12811,7 @@ and generic_call ctx ~want loc name vars pats pret args = the [sort-by] path below is untouched by construction. *) let bound_scalar = match pat with - | Types.Var v when (not (generic_ty p)) && Types.is_numeric p -> + | Types.Var v when (not (open_ty p)) && Types.is_numeric p -> Some v | _ -> None in @@ -11958,11 +12832,11 @@ and generic_call ctx ~want loc name vars pats pret args = let bound_view = match pat, p with | Types.Var v, (Types.Slice _ | Types.Ptr _) - when not (generic_ty p || bound_exactly v) -> Some v + when not (open_ty p || bound_exactly v) -> Some v | _ -> None in let a = - if generic_ty p || bound_view <> None then check ctx a + if open_ty p || bound_view <> None then check ctx a else if bound_scalar <> None && not untyped_literal then (* On its own terms first. A form that has no type without a want — [(zeroed)] is the one that matters — refuses here and is @@ -12167,7 +13041,8 @@ and generic_call ctx ~want loc name vars pats pret args = !subst; let cparams = List.map (subst_ty !subst) pats in let cret = subst_ty !subst pret in - if List.exists generic_ty cparams || generic_ty cret then begin + List.iter (realise ctx.env loc) (cret :: cparams); + if List.exists open_ty cparams || open_ty cret then begin (* One generic function calling another at its *own* variable, seen from the abstract pass over the caller's body — [sort-by] calling [swap] at [t]. There is no copy to make yet: [t] is not a type. The node is @@ -12193,10 +13068,10 @@ and generic_call ctx ~want loc name vars pats pret args = | Some (Types.Var v) when not (declares ctx.env.tvpreds v p.Ast.pname) -> Loc.failk "check/predicate-not-carried" loc "%s is written {:where (%s $%s)}, and this call passes the \ - type variable %s, which nothing here declares %s. Add \ + type variable $%s, which nothing here declares %s. Add \ {:where (%s $%s)} to this function's own clause" name p.Ast.pname p.Ast.pvar v p.Ast.pname p.Ast.pname v - | Some t when not (generic_ty t) && not (pred_holds p.Ast.pname t) -> + | Some t when not (open_ty t) && not (pred_holds p.Ast.pname t) -> Loc.failk "check/predicate-unsatisfied" loc "%s is written {:where (%s $%s)}, and this call passes %s, \ which is not %s" @@ -12271,14 +13146,22 @@ and instantiate env loc gname vars subst cparams cret = concrete as one written out by hand. The [where] clause goes out of scope with them — there is nothing abstract left for it to permit, and every operator is answered by the concrete type it now has. *) + let saved_lens = env.lenvars and saved_ph = env.len_placeholder in env.subst <- List.map (fun v -> (v, List.assoc v subst)) vars; env.tyvars <- []; + env.lenvars <- []; + env.len_placeholder <- false; env.tvpreds <- []; env.chain <- env.chain @ [ (gname, cparams, loc) ]; let restore () = env.subst <- saved_subst; env.tyvars <- saved_vars; - env.tvpreds <- saved_preds; env.chain <- saved_chain + env.tvpreds <- saved_preds; env.chain <- saved_chain; + env.lenvars <- saved_lens; env.len_placeholder <- saved_ph in + if Hashtbl.mem env.refused_generics gname then begin + restore (); + sym + end else let tfn = (* Without recovery: a copy that does not check is refused whole, at the call that asked for it, as it always was. *) @@ -12286,6 +13169,60 @@ and instantiate env loc gname vars subst cparams cret = | tfn -> restore (); tfn | exception e -> restore (); + (* The refusal is inside the generic's source, which says nothing about + which call asked for this copy; the note names it. Nested copies + each add their own, so the notes walk the chain back to the call + the programmer wrote. *) + let at () = + String.concat ", " + (List.map + (fun v -> Printf.sprintf "$%s = %s" v + (Types.to_string (List.assoc v subst))) + vars) + in + let in_prelude (l : Loc.t) = String.equal l.Loc.file Prelude.file in + let e = + match e with + (* A prelude generic's body is source nobody at this call wrote, and + an editor cannot jump to it. The refusal moves to the call that + asked for the copy, and the prelude's line comes along as a + note. *) + | Loc.Error d when in_prelude d.Loc.dloc && not (in_prelude loc) -> + (* Only the reason comes along. The rest of the body's message is + a fix to the body, which the caller cannot make. *) + let reason = + let cut sep m = + match find_sub m sep with + | Some i -> String.sub m 0 i + | None -> m + in + cut ". " (cut " — " d.Loc.dmsg) + in + Loc.Error + (Loc.sort_notes + { d with + Loc.dloc = loc; + dmsg = + Printf.sprintf + "%s cannot be made at %s: its body in the prelude does \ + not compile at that type. Pass a value of a type it \ + takes, or write the operation here" + gname (at ()); + notes = + d.Loc.notes + @ [ Loc.note d.Loc.dloc ("in the prelude, " ^ reason) ]; + expansion = None }) + | Loc.Error d when d.Loc.dloc <> loc -> + Loc.Error + (Loc.sort_notes + { d with + Loc.notes = + d.Loc.notes + @ [ Loc.note loc + (Printf.sprintf "%s is instantiated at %s here" + gname (at ())) ] }) + | e -> e + in (* A copy whose body did not check is not a copy. Both entries go back out, so a second call at the same types is the same refusal again rather than a cache hit on a function that does not exist. *) @@ -13104,10 +14041,28 @@ let collect env (decls : Ast.decl list) = | None -> ()); Hashtbl.add claimed n d.Ast.dloc) decls; + (* A defstruct whose fields introduce a variable is a template. *) + let generic_fields (fs : Ast.field list) = + let vs, _, _ = + sigil_vars ~kinds_of:(fun _ -> None) + (List.map (fun (f : Ast.field) -> f.Ast.fty) fs) + in + vs <> [] + in + let gpending = Hashtbl.create 4 in (* Names first, so a struct may mention one declared below it. *) List.iter (fun (d : Ast.decl) -> match d.Ast.d with + | Ast.Defstruct (n, fs, parent) when generic_fields fs -> + (match parent with + | Some t -> + fail t.Ast.tloc + "%s is generic, and a condition struct is not — a handler \ + matches one type, and %s is a type only at its arguments" n n + | None -> ()); + Hashtbl.replace env.locs n d.Ast.dloc; + Hashtbl.replace gpending n (fs, d.Ast.dloc) | Ast.Defstruct (n, _, _) -> Hashtbl.replace env.locs n d.Ast.dloc; Hashtbl.replace env.structs n { Tast.sname = n; fields = [] } @@ -13148,6 +14103,26 @@ let collect env (decls : Ast.decl list) = | Ast.Defalias (n, t) -> Hashtbl.replace env.aliases n t | _ -> ()) decls; + (* Each template's parameters, which needs every other template's: a + template's length argument to another is a length of its own. A cycle + between templates reads the arguments on it as types; any length among + them is then refused where it is used. *) + let rec params_of visiting n = + match Hashtbl.find_opt env.gstructs n with + | Some g -> Some (List.map snd g.gparams) + | None -> + match Hashtbl.find_opt gpending n with + | None -> None + | Some _ when List.mem n visiting -> None + | Some (fs, gloc) -> + let _, _, vs = + sigil_vars ~kinds_of:(params_of (n :: visiting)) + (List.map (fun (f : Ast.field) -> f.Ast.fty) fs) + in + Hashtbl.replace env.gstructs n { gparams = vs; gfields = fs; gloc }; + Some (List.map snd vs) + in + Hashtbl.iter (fun n _ -> ignore (params_of [] n)) gpending; (* Compile-time integer constants next, to a fixpoint, because an array length may name a constant declared below it — top-level names in a package are order-independent (plan.org, Modules). *) @@ -13284,6 +14259,22 @@ let collect env (decls : Ast.decl list) = Hashtbl.replace env.externs fn.Ast.name csym; Hashtbl.replace env.extern_locs fn.Ast.name loc | Ast.Defalias _ -> () + | Ast.Defstruct (n, fs, _) when Hashtbl.mem env.gstructs n -> + let names = List.map (fun (f : Ast.field) -> f.Ast.fname) fs in + if List.length (List.sort_uniq compare names) <> List.length names then + fail loc "%s declares the same field twice" n; + (* The template is checked once, here, at its variables: an unknown + type in a field is refused at the defstruct rather than at the + first use of it. *) + let g = Hashtbl.find env.gstructs n in + (match + struct_copy ~at_definition:true env loc n + (List.map (fun (p, _) -> Types.Var p) g.gparams) + with + | _ -> () + | exception Loc.Error d -> + Hashtbl.replace env.broken n (); + defer_or_raise env d) | Ast.Defstruct (n, fs, parent) -> let names = List.map (fun (f : Ast.field) -> f.Ast.fname) fs in if List.length (List.sort_uniq compare names) <> List.length names then @@ -13404,7 +14395,7 @@ let collect env (decls : Ast.decl list) = signature: it goes in [gsigs] and the function goes nowhere near [fns], because nothing can be called at [t]. Every call site turns it into an ordinary entry. *) - let vars = signature_tyvars fn in + let vars, lens = signature_tyvars env fn in (* The [where] clause is checked against the signature here, once, rather than at every use of it: a predicate nobody has heard of, or one about a variable the signature never bound, is a mistake @@ -13422,18 +14413,47 @@ let collect env (decls : Ast.decl list) = (if vars = [] then " — it binds none" else " — it binds " - ^ String.concat ", " (List.map (fun v -> "$" ^ v) vars))) + ^ String.concat ", " (List.map (fun v -> "$" ^ v) vars)); + (* A where clause takes type predicates, and a length is not a + type. Whether it should take value predicates over one is + an open question in TODO.org, not an accident to fall out + of this. *) + if List.mem p.Ast.pvar lens then + defer_or_raise env + (Loc.diag ~kind:"check/length-predicate" p.Ast.ploc + (Printf.sprintf + "$%s is a length, and a where clause takes type \ + predicates only — %s is about a type" + p.Ast.pvar p.Ast.pname))) fn.Ast.fwhere; + (* A predicate over a length was refused above; what is left is the + clause every copy is judged against. *) + let fn = + { fn with + Ast.fwhere = + List.filter + (fun (p : Ast.pred) -> not (List.mem p.Ast.pvar lens)) + fn.Ast.fwhere } + in env.tyvars <- vars; + env.lenvars <- lens; env.tvpreds <- fn.Ast.fwhere; - let params = - List.map (fun (p : Ast.field) -> resolve env p.Ast.fty) fn.Ast.params + let params, ret = + Fun.protect + ~finally:(fun () -> + env.tyvars <- []; env.lenvars <- []; env.tvpreds <- []) + (fun () -> + let params = + List.map (fun (p : Ast.field) -> resolve env p.Ast.fty) + fn.Ast.params + in + let ret = + match fn.Ast.ret with + | None -> Types.Unit + | Some t -> resolve env t + in + params, ret) in - let ret = - match fn.Ast.ret with None -> Types.Unit | Some t -> resolve env t - in - env.tyvars <- []; - env.tvpreds <- []; if fn.Ast.fprivate <> Ast.Exported then Hashtbl.replace env.privates fn.Ast.name (fn.Ast.nloc, fn.Ast.fprivate); @@ -13444,7 +14464,8 @@ let collect env (decls : Ast.decl list) = end else begin Hashtbl.replace env.generics fn.Ast.name fn; - Hashtbl.replace env.gsigs fn.Ast.name (vars, params, ret) + Hashtbl.replace env.gsigs fn.Ast.name (vars, params, ret); + Hashtbl.replace env.glens fn.Ast.name lens end | Ast.Defvar (n, t, _, k) -> let ty = match t with @@ -13515,37 +14536,11 @@ let collect env (decls : Ast.decl list) = it is inline. Caught here rather than when a backend tries to lay the type out or a zero value is built for it — which would not fail, it would hang. *) let check_finite env = - let rec walk seen name = - if List.mem name seen then - fail (Option.value (Hashtbl.find_opt env.locs name) ~default:Loc.unknown) - "%s contains itself by value, so it has no size — go through (Ptr %s)" - name name; - let seen = name :: seen in - match Hashtbl.find_opt env.structs name with - | Some s -> List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) s.Tast.fields - | None -> - match Hashtbl.find_opt env.datas name with - | Some u -> - List.iter - (fun (c : Tast.variant) -> - List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) c.Tast.vfields) - u.Tast.cases - | None -> - (* A union whose member is itself is the same infinite type a struct's - is — the size is the largest member and the largest member is the - whole thing. Nothing about overlaying storage makes the recursion - finite, so it is on the same walk rather than left to hang the - layout calculator. *) - match Hashtbl.find_opt env.unions name with - | None -> () - | Some u -> - List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) u.Tast.fields - and ty seen = function - | Types.Named n -> walk seen n - | Types.Array (_, e) | Types.Option e -> ty seen e - | _ -> () - in - Hashtbl.iter (fun n _ -> walk [] n) env.structs; + let walk _ n = finite_from env n in + (* A generic struct's copy was asked this when it was made. *) + Hashtbl.iter + (fun n _ -> if not (Hashtbl.mem env.copies n) then walk [] n) + env.structs; Hashtbl.iter (fun n _ -> walk [] n) env.datas; Hashtbl.iter (fun n _ -> walk [] n) env.unions @@ -13813,8 +14808,30 @@ let rec check_fn env (fn : Ast.fn) : Tast.fn = and check_generic env (fn : Ast.fn) = let vars, params, ret = Hashtbl.find env.gsigs fn.Ast.name in let saved_lifted = env.lifted and saved_vars = env.tyvars - and saved_preds = env.tvpreds in + and saved_preds = env.tvpreds and saved_lens = env.lenvars + and saved_ph = env.len_placeholder in + (* The body sees a length variable's array at [abstract_len], an ordinary + array every array operation already answers for; the signature keeps + its [Types.LArray] for call sites to bind against. *) + let rec at_placeholder (t : Types.t) = + match t with + | Types.LArray (_, e) -> Types.Array (abstract_len, at_placeholder e) + | Types.Slice (m, e) -> Types.Slice (m, at_placeholder e) + | Types.Array (n, e) -> Types.Array (n, at_placeholder e) + | Types.Ptr (m, e) -> Types.Ptr (m, at_placeholder e) + | Types.Vec e -> Types.Vec (at_placeholder e) + | Types.Option e -> Types.Option (at_placeholder e) + | Types.Map (k, v) -> Types.Map (at_placeholder k, at_placeholder v) + | Types.Fn (ps, r) -> Types.Fn (List.map at_placeholder ps, at_placeholder r) + | Types.CFn (ps, r) -> + Types.CFn (List.map at_placeholder ps, at_placeholder r) + | t -> t + in + let params = List.map at_placeholder params and ret = at_placeholder ret in env.tyvars <- vars; + env.lenvars <- + Option.value (Hashtbl.find_opt env.glens fn.Ast.name) ~default:[]; + env.len_placeholder <- true; (* What the abstract pass may assume. Every operator the body reaches asks [env.tvpreds] whether the variable was declared to support it, and every instantiation asks the concrete type the same question again. *) @@ -13824,7 +14841,9 @@ and check_generic env (fn : Ast.fn) = Hashtbl.remove env.fns fn.Ast.name; env.lifted <- saved_lifted; env.tyvars <- saved_vars; - env.tvpreds <- saved_preds + env.tvpreds <- saved_preds; + env.lenvars <- saved_lens; + env.len_placeholder <- saved_ph in (match check_fn env fn with | _ -> finish () @@ -14971,6 +15990,7 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) : time it runs every signature is sound, so a body that fails to check cannot make the next body fail — which is what makes a declaration a resync point that needs no resynchronising. *) + if keep_going then env.deferred <- Some []; grow_warnings := []; let decls = collect env decls in if !print_warnings then @@ -14982,6 +16002,9 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) : check_finite env; check_union_members env; let s = Loc.sink ~on:keep_going in + (match env.deferred with + | Some ds -> s.Loc.found <- ds; env.deferred <- None + | None -> ()); ignore (Loc.caught s (fun () -> check_main env decls)); (* Every generic body, checked once with its variables left abstract, and the result thrown away. This is the pass plan.org's rule needs and Odin @@ -14996,11 +16019,14 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) : (fun (d : Ast.decl) -> match d.Ast.d with | Ast.Defn fn when Hashtbl.mem env.gsigs fn.Ast.name -> - ignore - (Loc.caught s (fun () -> + (match + Loc.caught s (fun () -> tolerant fn.Ast.name (fun () -> with_recovery env ~on:keep_going (fun () -> - Some (check_generic env fn))))) + Some (check_generic env fn)))) + with + | None -> Hashtbl.replace env.refused_generics fn.Ast.name () + | Some _ -> ()) | _ -> ()) decls; let globals = @@ -15073,7 +16099,12 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) : |> List.sort (fun (a : Tast.extern) b -> String.compare a.Tast.esym b.Tast.esym) in let p = - { Tast.structs = values (fun (s : Tast.structure) -> s.Tast.sname) env.structs; + { Tast.structs = + (* A struct copy at variables was only ever for an abstract pass. *) + List.filter + (fun (s : Tast.structure) -> + Hashtbl.find_opt env.copies s.Tast.sname <> Some true) + (values (fun (s : Tast.structure) -> s.Tast.sname) env.structs); datas = values (fun (u : Tast.data) -> u.Tast.dname) env.datas; unions = values (fun (u : Tast.structure) -> u.Tast.sname) env.unions; globals; externs; fns; cshim } @@ -15165,6 +16196,24 @@ let lifted_since env mark = let fresh = List.length env.lifted - mark in List.rev (List.filteri (fun i _ -> i < fresh) env.lifted) +(* The struct copies this env made that [have] does not hold: what an + expression checked against a running session named for the first time — + [(Pair 1 2)] typed at a REPL makes [(Pair i32)] — which the module built + for it has to lay out, and the session has to keep. *) +let fresh_copies env (have : Tast.structure list) = + Hashtbl.fold + (fun k at_vars acc -> + if at_vars + || List.exists (fun (s : Tast.structure) -> String.equal s.Tast.sname k) + have + then acc + else + match Hashtbl.find_opt env.structs k with + | Some s -> s :: acc + | None -> acc) + env.copies [] + |> List.sort (fun (a : Tast.structure) b -> String.compare a.Tast.sname b.Tast.sname) + let env_structs env (fns : Tast.fn list) = List.filter_map (fun (f : Tast.fn) -> Hashtbl.find_opt env.structs ("env/" ^ f.Tast.name)) diff --git a/lib/cimport.ml b/lib/cimport.ml index 9fea9ebe..266879f5 100644 --- a/lib/cimport.ml +++ b/lib/cimport.ml @@ -407,6 +407,7 @@ let rec ty_source (t : Ast.texpr) = | Ast.Tname n -> n | Ast.Tapp (n, args) -> Printf.sprintf "(%s %s)" n (String.concat " " (List.map ty_source args)) + | Ast.Tlen n -> Int64.to_string n | Ast.Tslice (c, e) -> Printf.sprintf "[%s%s]" (if c then "const " else "") (ty_source e) | Ast.Tarray (Ast.Lint n, e) -> Printf.sprintf "[%Ld %s]" n (ty_source e) diff --git a/lib/dev.ml b/lib/dev.ml index bac569e7..8c0b8967 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -704,14 +704,12 @@ let host_loc t name = that was written finds nothing in the program, and these are how it gets from that name to what the program does hold. *) -(* Its signature as written, [$] and all — [Types.to_string] prints a variable - bare, and [[t]] is not how anyone wrote it. *) +(* Its signature as written, [$] and all. *) let generic_signature t name = match Hashtbl.find_opt t.session.Session.env.Check.gsigs name with | None -> None - | Some (vars, params, ret) -> - let dollar = List.map (fun v -> (v, Types.Var ("$" ^ v))) vars in - let show ty = Types.to_string (Check.subst_ty dollar ty) in + | Some (_, params, ret) -> + let show ty = Types.to_string ty in Some (Printf.sprintf "%s [%s] %s" name (String.concat " " (List.map show params)) (show ret)) @@ -1914,8 +1912,27 @@ let defs t = ~loc:(Loc.to_string loc) ()) classes in + (* A generic struct is listed by its template, as [(Pair $t)]; its copies + are struct names only the compiler wrote. *) + let structs = + Hashtbl.fold + (fun name _ acc -> + if Hashtbl.mem env.Check.copies name then acc + else entry ~name ~kind:"struct" ~sign:name ~loc:"" () :: acc) + env.Check.structs [] + @ Hashtbl.fold + (fun name (g : Check.gstruct) acc -> + entry ~name ~kind:"struct" + ~sign: + (Printf.sprintf "(%s %s)" name + (String.concat " " + (List.map (fun (p, _) -> "$" ^ p) g.Check.gparams))) + ~loc:"" () + :: acc) + env.Check.gstructs [] + in List.sort compare - (of_table "struct" env.Check.structs + (structs @ datas @ classes @ of_table "union" env.Check.unions @ of_table "enum" env.Check.enums @@ -1996,13 +2013,19 @@ let defs t = text about the type and never touches the program. *) let layout t ~ty = let structs = t.session.Session.program.Tast.structs in + (* A generic struct's copy answers to the spelling a printed value's head + gives it, [Pair i32], and to its type's, [(Pair i32)], as well as to its + key. *) + let names (s : Tast.structure) = + [ s.Tast.sname; Types.struct_head s.Tast.sname; + Types.to_string (Types.Named s.Tast.sname) ] + in match - List.find_opt (fun (s : Tast.structure) -> String.equal s.Tast.sname ty) - structs + List.find_opt (fun (s : Tast.structure) -> List.mem ty (names s)) structs with | Some s -> ok - [ ":type " ^ Wire.quote s.Tast.sname; + [ ":type " ^ Wire.quote (Types.to_string (Types.Named s.Tast.sname)); ":fields " ^ Wire.list (List.map @@ -4545,7 +4568,8 @@ let rec handle t req = | Some l, None -> Some (l, 1) | _ -> None in - Source.with_code ~syntax ~at (fun () -> handle_op t req) + let indent = Wire.int_field req "indent" in + Source.with_code ?indent ~syntax ~at (fun () -> handle_op t req) and handle_op t req = match Wire.string_field req "op" with diff --git a/lib/emit.ml b/lib/emit.ml index 640078e6..01b82b72 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -370,7 +370,7 @@ let rec ll (t : Types.t) = integer spelling costs no casts and keeps the emitter honest about not knowing whether the bits are a pointer. *) | Types.Dyn -> "i64" - | Types.Var _ -> + | Types.Var _ | Types.Len _ | Types.LArray _ -> (* The checker rejects it by name — nothing reaches here. *) internal "no layout for %s" (Types.to_string t) @@ -647,7 +647,8 @@ let rec lay m (t : Types.t) : int * int = | Some u -> union_lay m u | None -> internal "no layout for struct %s" n) | Types.Dyn -> 8, 8 - | Types.Var _ -> internal "no layout for %s" (Types.to_string t) + | Types.Var _ | Types.Len _ | Types.LArray _ -> + internal "no layout for %s" (Types.to_string t) (* Size, alignment, and the offset of every member. *) and lay_fields m tys = @@ -1210,7 +1211,7 @@ let rec dty m d (t : Types.t) : int = reading: it prints, and the person reading it can hand it to the runtime's own printer. *) | Types.Dyn -> basic "dyn" 64 "DW_ATE_unsigned" - | Types.Var _ -> + | Types.Var _ | Types.Len _ | Types.LArray _ -> internal "no debug type for %s" (Types.to_string t) in Hashtbl.replace d.dtys key n; diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index aefc217b..2f8ad73f 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -249,7 +249,7 @@ 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"). *) -let layout ?(snippet = false) ?(base = 1) (toks : token list) : token array = +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 @@ -258,6 +258,11 @@ let layout ?(snippet = false) ?(base = 1) (toks : token list) : token array = let out = ref [] in let add tok loc = out := { tok; loc; sp = true } :: !out in let stack = ref [ base ] in + (* [indent] is the column of the statement a snippet was cut out of, when + the snippet starts after that statement's first word (an elif's + condition, an arm's value). Its first joined line continues as it does + in the file: deeper than the statement, not than the cut. *) + let first_line = ref true in let depth = ref 0 in let binop t = match t.tok with NAME s -> is_binop s | _ -> false in for i = 0 to n - 1 do @@ -277,11 +282,16 @@ let layout ?(snippet = false) ?(base = 1) (toks : token list) : token array = && arr.(i + 1).sp in let continues = (binop p && p.sp) || (binop t && spaced_after) in + let top = + match indent with + | Some c when !first_line && List.length !stack = 1 -> min c (List.hd !stack) + | _ -> List.hd !stack + in (* A continuation line sits deeper than the statement it continues. One at or left of that statement's column is not read as joining it: that would pull a line into a block it was written outside of, silently. *) - if continues && t.loc.Loc.col <= List.hd !stack then + if continues && t.loc.Loc.col <= top then failk "continuation" t.loc "%s" (if binop t then @@ -290,15 +300,16 @@ let layout ?(snippet = false) ?(base = 1) (toks : token list) : token array = line above, but it is not indented past the start of that \ line (column %d). Indent it further to continue the line, \ or give %s a value on its left" - (show t.tok) (List.hd !stack) (show t.tok) + (show t.tok) top (show t.tok) else Printf.sprintf "the line above ends with the operator %s, so this line \ continues it, but it is not indented past the start of \ that line (column %d). Indent it further, or finish the \ line above" - (show p.tok) (List.hd !stack)); + (show p.tok) top); if not continues then begin + first_line := false; let at = point p.loc in add NEWLINE at; let col = t.loc.Loc.col in @@ -1578,7 +1589,7 @@ and lines (s : st) (one : unit -> Form.t list) : Form.t list = (** 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 ~file src = +let read_all ?(line = 1) ?col ?indent ~file src = let snippet = col <> None in let col = Option.value col ~default:1 in let saved = !source in @@ -1588,7 +1599,7 @@ let read_all ?(line = 1) ?col ~file src = (file, Array.of_list (String.split_on_char '\n' (String.make (line - 1) '\n' ^ String.make (col - 1) ' ' ^ src))); Fun.protect ~finally:(fun () -> source := saved) (fun () -> - let toks = layout ~snippet ~base:col (lex ~line ~col ~file src) in + 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 (match (peek s.p).tok with diff --git a/lib/js.ml b/lib/js.ml index 74f86563..a8f01620 100644 --- a/lib/js.ml +++ b/lib/js.ml @@ -249,6 +249,8 @@ let rec refuse_ty loc (t : Types.t) = host's own, and that work has not been done" | Types.Var n -> at loc "a type variable (%s) reached the backend, which cannot happen" n + | Types.Len _ | Types.LArray _ -> + at loc "a length variable reached the backend, which cannot happen" (* Aggregates in the sense that matters here: the types whose assignment copies in Flan and would alias in JS. A slice is deliberately not one — diff --git a/lib/load.ml b/lib/load.ml index 301babc1..930fdda8 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -213,8 +213,11 @@ let rec rename_texpr owned alias (t : Ast.texpr) : Ast.texpr = Ast.Tarray (rename_len owned alias l, rename_texpr owned alias e) | Ast.Tmap (k, v) -> Ast.Tmap (rename_texpr owned alias k, rename_texpr owned alias v) + (* The head too, when it is a generic struct the package declares. *) | Ast.Tapp (n, args) -> + let n = if List.mem n owned then qualify alias n else n in Ast.Tapp (n, List.map (rename_texpr owned alias) args) + | Ast.Tlen _ as k -> k | Ast.Tfn (env, ps, r) -> Ast.Tfn (env, List.map (rename_texpr owned alias) ps, rename_texpr owned alias r) @@ -271,7 +274,7 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr = let bound = match a.Ast.pat with | Ast.Pctor (_, ns) -> ns @ bound - | Ast.Pkw _ | Ast.Pwild -> bound + | Ast.Pkw _ | Ast.Plit _ | Ast.Pwild -> bound in { a with Ast.body = List.map (rename_expr owned alias bound) a.Ast.body }) arms) @@ -800,8 +803,11 @@ let rec texpr_uses acc (t : Ast.texpr) = (match l with Ast.Lname n -> acc := (n, t.Ast.tloc) :: !acc | Ast.Lint _ -> ()); texpr_uses acc e | Ast.Tmap (k, v) -> texpr_uses acc k; texpr_uses acc v - | Ast.Tapp (_, args) -> List.iter (texpr_uses acc) args + | Ast.Tapp (n, args) -> + acc := (n, t.Ast.tloc) :: !acc; + List.iter (texpr_uses acc) args | Ast.Tfn (_, ps, r) -> List.iter (texpr_uses acc) ps; texpr_uses acc r + | Ast.Tlen _ -> () let rec expr_uses acc (e : Ast.expr) = let go = expr_uses acc in diff --git a/lib/parse.ml b/lib/parse.ml index f148fe2e..b325e03a 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -99,6 +99,16 @@ let no_pattern (f : Form.t) = (* ── Type expressions ──────────────────────────────────────────────── *) +(* A type constructor's spelling: its last segment starts with a capital. *) +let capitalised_name name = + let base = + match String.rindex_opt name '/' with + | Some i -> String.sub name (i + 1) (String.length name - i - 1) + | None -> name + in + base <> "" && Char.uppercase_ascii base.[0] = base.[0] + && Char.lowercase_ascii base.[0] <> base.[0] + let rec texpr (f : Form.t) : Ast.texpr = let mk t = { Ast.t; tloc = f.loc } in match f.v with @@ -161,8 +171,44 @@ let rec texpr (f : Form.t) : Ast.texpr = | [ { v = Vec params; _ }; ret ] -> mk (Ast.Tfn (env, List.map texpr params, texpr ret)) | _ -> fail f "a function type is (%s [T ...] R)" which) - | List ({ v = Sym name; _ } :: args) when args <> [] -> - mk (Ast.Tapp (name, List.map texpr args)) + | List ({ v = Sym name; _ } :: args) + when args <> [] || capitalised_name name -> + (* An integer argument is a generic struct's length, and a type + constructor is capitalised. A lowercase head is a body form in the + return slot — (+ x 1) — and its integer is the type parser's reason to + give up, which is the refusal that slot is built on. *) + let capitalised = capitalised_name name in + (* Integer arithmetic over literals is a length too — [(Small (+ 4 4) + i32)] — folded here, since nothing later reads it as a value. *) + let rec fold (a : Form.t) = + match a.v with + | Int n -> Some n + | List ({ v = Sym (("+" | "-" | "*") as op); _ } :: (_ :: _ as xs)) -> + let vs = List.map fold xs in + if List.for_all Option.is_some vs then + let vs = List.map Option.get vs in + match op, vs with + | "-", [ x ] -> Some (Int64.neg x) + | "+", v :: rest -> Some (List.fold_left Int64.add v rest) + | "-", v :: rest -> Some (List.fold_left Int64.sub v rest) + | "*", v :: rest -> Some (List.fold_left Int64.mul v rest) + | _ -> None + else None + | _ -> None + in + let arg (a : Form.t) = + match a.v, fold a with + | _, Some n when capitalised -> { Ast.t = Ast.Tlen n; tloc = a.loc } + | List _, None when capitalised -> + (try texpr a with + | Loc.Error _ -> + fail a + "%s is not a type or a length. An argument here is a type, or a \ + length: an integer, a constant's name or a length variable" + (Form.to_string a)) + | _ -> texpr a + in + mk (Ast.Tapp (name, List.map arg args)) | _ -> fail f "expected a type, found %s" (Form.to_string f) and len (f : Form.t) : Ast.len = @@ -1356,6 +1402,7 @@ and pattern (f : Form.t) : Ast.pattern = (* An enum member. Which enum is the scrutinee's type, so [Check] resolves it, as it resolves a keyword anywhere an enum is expected. *) | Kw member -> Ast.Pkw member + | Int _ | UInt _ | Float _ | Byte _ | Str _ -> Ast.Plit (expr f) | List ({ v = Sym ctor; _ } :: binds) -> List.iter no_pattern binds; Ast.Pctor (ctor, List.map dname binds) diff --git a/lib/render.ml b/lib/render.ml index c5cf9f6d..5cf3f33f 100644 --- a/lib/render.ml +++ b/lib/render.ml @@ -103,6 +103,12 @@ let print_refusal _loc t = Printf.sprintf "no printer for %s — print the values you want out of it" (Types.to_string t) +(* The head a struct value prints under: its name, or for a generic struct's + copy the template and its arguments, [Pair i32], so the value reads + [(Pair i32 {.a 1 .b 2})] the way its type is written. Every renderer of a + struct value goes through this, so they all print the same text. *) +let head n = Types.struct_head n + let rec render ?(refuse = print_refusal) c depth (e : Tast.expr) : Tast.expr list = let render c depth e = render ~refuse c depth e in let loc = e.Tast.loc in @@ -333,7 +339,7 @@ let rec render ?(refuse = print_refusal) c depth (e : Tast.expr) : Tast.expr lis @ render c (depth + 1) v) shown) in - [ do_ ((lit ("(" ^ n ^ " {") :: parts) + [ do_ ((lit ("(" ^ head n ^ " {") :: parts) @ (if List.length fields > max_span then [ lit " ..." ] else []) @ [ lit "})" ]) ]) (* A fixed array's length is in its type, so it unrolls — capped, because diff --git a/lib/session.ml b/lib/session.ml index d29fcc9e..3a1e371e 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -597,7 +597,7 @@ let compatible ~loc (old_ : Tast.program) (new_ : Tast.program) = if not same then fail loc "%s changes layout. Restart to change it." - s.Tast.sname + (Types.to_string (Types.Named s.Tast.sname)) | None -> ()) new_.Tast.structs @@ -1596,7 +1596,7 @@ let render_locals ?(origin = "") t ~frame ~(fn : Tast.fn) ~bound let loc = fn.Tast.floc in let extra = ref [] and nslots = ref 0 in let c = - { Render.structs = t.program.Tast.structs; + { Render.structs = t.program.Tast.structs @ Check.fresh_copies t.env t.program.Tast.structs; datas = t.program.Tast.datas; unions = t.program.Tast.unions; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; @@ -1708,7 +1708,7 @@ let render_condition t ~(st : Tast.structure) : change * (string * string) list let loc = Loc.unknown in let extra = ref [] and nslots = ref 0 in let c = - { Render.structs = t.program.Tast.structs; + { Render.structs = t.program.Tast.structs @ Check.fresh_copies t.env t.program.Tast.structs; datas = t.program.Tast.datas; unions = t.program.Tast.unions; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; @@ -1969,7 +1969,7 @@ let render_slot ?(origin = "") t ~frame ~(fn : Tast.fn) ~slot ~path | Some name -> let extra = ref [] and nslots = ref 0 in let c = - { Render.structs = t.program.Tast.structs; + { Render.structs = t.program.Tast.structs @ Check.fresh_copies t.env t.program.Tast.structs; datas = t.program.Tast.datas; unions = t.program.Tast.unions; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; @@ -2286,7 +2286,7 @@ let write_slot ?(origin = "") t ~frame ~(fn : Tast.fn) ~slot ~path in let extra = ref [] and nslots = ref (Array.length base) in let c = - { Render.structs = t.program.Tast.structs; + { Render.structs = t.program.Tast.structs @ Check.fresh_copies t.env t.program.Tast.structs; datas = t.program.Tast.datas; unions = t.program.Tast.unions; enums = @@ -2323,11 +2323,15 @@ let write_slot ?(origin = "") t ~frame ~(fn : Tast.fn) ~slot ~path Array.append bnames (Array.make (List.length !extra) None) } in + (* A struct copy the values named first, laid out in this + module and kept, as [eval_expr] keeps one. *) + let copies = Check.fresh_copies t.env t.program.Tast.structs in let program = { t.program with Tast.fns = t.program.Tast.fns @ fresh @ claim_lifted t lmark tname @ [ thunk ]; + structs = t.program.Tast.structs @ copies; externs = t.program.Tast.externs @ externs } in let ir = @@ -2340,7 +2344,9 @@ let write_slot ?(origin = "") t ~frame ~(fn : Tast.fn) ~slot ~path [eval_expr] says why, and the caller takes the same [held] around this that it takes around one. *) t.program <- - { t.program with Tast.fns = t.program.Tast.fns @ fresh }; + { t.program with + Tast.fns = t.program.Tast.fns @ fresh; + structs = t.program.Tast.structs @ copies }; Ok ({ ir; x86 = t.x86; names = []; fns = []; installs = true; stale = [] }, where, Types.to_string shown.Tast.ty)))) @@ -2412,7 +2418,7 @@ let arm_restart ?(origin = "") t ~index ~(params : Types.t list) in let extra = ref [] and nslots = ref (Array.length base) in let c = - { Render.structs = t.program.Tast.structs; + { Render.structs = t.program.Tast.structs @ Check.fresh_copies t.env t.program.Tast.structs; datas = t.program.Tast.datas; unions = t.program.Tast.unions; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; @@ -2453,17 +2459,22 @@ let arm_restart ?(origin = "") t ~index ~(params : Types.t list) slots = Array.append base (Array.of_list (List.rev !extra)); snames = Array.append bnames (Array.make (List.length !extra) None) } in + let copies = Check.fresh_copies t.env t.program.Tast.structs in let program = { t.program with Tast.fns = t.program.Tast.fns @ fresh @ claim_lifted t lmark tname @ [ thunk ]; + structs = t.program.Tast.structs @ copies; externs = t.program.Tast.externs @ externs } in let ir = redefinition t ~call:tname program ~fns:(List.map (fun (f : Tast.fn) -> f.Tast.name) fresh @ [ tname ]) in - t.program <- { t.program with Tast.fns = t.program.Tast.fns @ fresh }; + t.program <- + { t.program with + Tast.fns = t.program.Tast.fns @ fresh; + structs = t.program.Tast.structs @ copies }; Ok ({ ir; x86 = t.x86; names = []; fns = []; installs = true; stale = [] }, List.map Types.to_string params) @@ -2496,7 +2507,7 @@ let render_globals ?(origin = "") t ~(globals : Tast.global list) let loc = Loc.unknown in let extra = ref [] and nslots = ref 0 in let c = - { Render.structs = t.program.Tast.structs; + { Render.structs = t.program.Tast.structs @ Check.fresh_copies t.env t.program.Tast.structs; datas = t.program.Tast.datas; unions = t.program.Tast.unions; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; @@ -2682,7 +2693,7 @@ let eval_expr ?(origin = "") ?(pause = false) ?frame t src : change = appended past [base] and collected here to size the frame below. *) let extra = ref [] and nslots = ref (Array.length base) in let c = - { Render.structs = t.program.Tast.structs; + { Render.structs = t.program.Tast.structs @ Check.fresh_copies t.env t.program.Tast.structs; datas = t.program.Tast.datas; unions = t.program.Tast.unions; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; @@ -2728,10 +2739,12 @@ let eval_expr ?(origin = "") ?(pause = false) ?frame t src : change = let placed = List.filter (fun (f : Tast.fn) -> List.mem f.Tast.name own) placed in + let copies = Check.fresh_copies t.env t.program.Tast.structs in let program = { t.program with Tast.fns = t.program.Tast.fns @ fresh @ placed; - structs = t.program.Tast.structs @ Check.env_structs t.env lifted; + structs = + t.program.Tast.structs @ copies @ Check.env_structs t.env lifted; externs = t.program.Tast.externs @ externs } in let ir = @@ -2757,7 +2770,10 @@ let eval_expr ?(origin = "") ?(pause = false) ?frame t src : change = caller closes that half by taking a [held] before this and restoring it when either fails — a copy the session holds and no module defines is a null cell exactly as a stranded declaration is. *) - t.program <- { t.program with Tast.fns = t.program.Tast.fns @ fresh }; + t.program <- + { t.program with + Tast.fns = t.program.Tast.fns @ fresh; + structs = t.program.Tast.structs @ copies }; { ir; x86 = t.x86; names = []; fns = []; installs = true; stale = [] } (* ── What a macro call expands to ──────────────────────────────────── *) diff --git a/lib/shim.ml b/lib/shim.ml index c3070b04..10475b19 100644 --- a/lib/shim.ml +++ b/lib/shim.ml @@ -177,6 +177,106 @@ let prim_cty = function | "bool" -> Some "bool" | _ -> None +(* ── Generic structs ────────────────────────────────────────────────── + A defstruct whose fields introduce [$t] is a template, and C only ever sees + one of its copies: the fields with the arguments written in, laid out the + way [Check] lays the same copy out. The copy is registered here under its + written spelling, [(G u8)], which [ctype_name] turns into a C name. *) + +let sigil n = n <> "" && n.[0] = '$' +let bare n = if sigil n then String.sub n 1 (String.length n - 1) else n + +(* A template's parameters, in the order its fields first introduce them, and + whether each is a length — [Check]'s reading, repeated over the AST + because this runs before [Check] does. *) +let rec template_params ?(fuel = 16) env n = + match Hashtbl.find_opt env.structs n with + | None -> [] + | Some fs -> + let acc = ref [] in + let add m is_len = + if sigil m && not (List.mem_assoc (bare m) !acc) then + acc := (bare m, is_len) :: !acc + in + let rec walk (t : Ast.texpr) = + match t.Ast.t with + | Ast.Tname m -> add m false + | Ast.Tslice (_, e) -> walk e + | Ast.Tarray (Ast.Lname m, e) -> add m true; walk e + | Ast.Tarray (_, e) -> walk e + | Ast.Tmap (k, v) -> walk k; walk v + | Ast.Tapp (h, args) -> + let kinds = + if fuel = 0 || String.equal h n then [] + else List.map snd (template_params ~fuel:(fuel - 1) env h) + in + if List.length kinds = List.length args then + List.iter2 + (fun is_len (a : Ast.texpr) -> + match a.Ast.t with + | Ast.Tname m when is_len -> add m true + | _ -> walk a) + kinds args + else List.iter walk args + | Ast.Tfn (_, ps, r) -> List.iter walk ps; walk r + | Ast.Tlen _ -> () + in + List.iter (fun (f : Ast.field) -> walk f.Ast.fty) fs; + List.rev !acc + +let rec source (t : Ast.texpr) = + match t.Ast.t with + | Ast.Tname n -> n + | Ast.Tlen n -> Int64.to_string n + | Ast.Tapp (n, args) -> + Printf.sprintf "(%s %s)" n (String.concat " " (List.map source args)) + | Ast.Tslice (c, e) -> Printf.sprintf "[%s%s]" (if c then "const " else "") (source e) + | Ast.Tarray (Ast.Lint n, e) -> Printf.sprintf "[%Ld %s]" n (source e) + | Ast.Tarray (Ast.Lname n, e) -> Printf.sprintf "[%s %s]" n (source e) + | Ast.Tmap (k, v) -> Printf.sprintf "(Map %s %s)" (source k) (source v) + | Ast.Tfn (env, ps, r) -> + Printf.sprintf "(%s [%s] %s)" (if env then "Fn" else "CFn") + (String.concat " " (List.map source ps)) (source r) + +(* The copy of template [n] at [args], registered and named. *) +let copy env ~loc n (args : Ast.texpr list) = + let ps = template_params env n in + if List.length ps <> List.length args then + fail loc "%s takes %d argument%s, and this gives %d" n (List.length ps) + (if List.length ps = 1 then "" else "s") (List.length args); + let key = source { Ast.t = Ast.Tapp (n, args); tloc = loc } in + if not (Hashtbl.mem env.structs key) then begin + let sub = List.combine (List.map fst ps) args in + let rec go (t : Ast.texpr) = + let k = + match t.Ast.t with + | Ast.Tname m when List.mem_assoc (bare m) sub -> + (List.assoc (bare m) sub).Ast.t + | Ast.Tname _ | Ast.Tlen _ -> t.Ast.t + | Ast.Tslice (c, e) -> Ast.Tslice (c, go e) + | Ast.Tarray (Ast.Lname m, e) when List.mem_assoc (bare m) sub -> + let l = + match (List.assoc (bare m) sub).Ast.t with + | Ast.Tlen k -> Ast.Lint k + | Ast.Tname c -> Ast.Lname c + | _ -> fail loc "%s's $%s is a length" n (bare m) + in + Ast.Tarray (l, go e) + | Ast.Tarray (l, e) -> Ast.Tarray (l, go e) + | Ast.Tmap (k, v) -> Ast.Tmap (go k, go v) + | Ast.Tapp (h, a) -> Ast.Tapp (h, List.map go a) + | Ast.Tfn (b, ps, r) -> Ast.Tfn (b, List.map go ps, go r) + in + { t with Ast.t = k } + in + Hashtbl.replace env.structs key + (List.map (fun (f : Ast.field) -> { f with Ast.fty = go f.Ast.fty }) + (Hashtbl.find env.structs n)) + end; + key + +let is_template env n = template_params env n <> [] + (* [needed] collects the structs whose typedefs this signature pulls in, in the order they were first met. Order is the program's and never a hash fold's: the object cache keys on the generated text, so a reordering would be a @@ -184,6 +284,14 @@ let prim_cty = function let rec cty env ~needed ~loc ~what (t : Ast.texpr) : string = let t = unalias env t in match t.Ast.t with + | Ast.Tname n when Hashtbl.mem env.structs n && is_template env n -> + fail loc "%s is %s, a generic struct, which is a type only at its \ + arguments — write them, as in (%s %s)" what n n + (String.concat " " + (List.map (fun (_, l) -> if l then "8" else "i32") + (template_params env n))) + | Ast.Tapp (n, args) when Hashtbl.mem env.structs n && is_template env n -> + cty env ~needed ~loc ~what { t with Ast.t = Ast.Tname (copy env ~loc n args) } | Ast.Tname n -> (match prim_cty n with | Some c -> c @@ -256,6 +364,7 @@ let rec cty env ~needed ~loc ~what (t : Ast.texpr) : string = fail loc "%s is a function type, and a C callback is not implemented" what | Ast.Tapp (n, _) -> fail loc "%s is %s, which is not a type this shim generator knows" what n + | Ast.Tlen n -> fail loc "%s is %Ld, which is not a type" what n (* ── What one parameter does at the boundary ────────────────────────── *) @@ -268,6 +377,14 @@ let classify env ~needed ~loc ~what (t : Ast.texpr) = let t' = unalias env t in match t'.Ast.t with | Ast.Tname "string" -> (Pstr, "const char *") + (* A copy crosses behind a pointer only: by value, the Flan half this + generator writes would have to spell the copy's type, and it builds its + wrapper from struct names. *) + | Ast.Tapp (n, _) when Hashtbl.mem env.structs n && is_template env n -> + fail loc + "%s is %s, a generic struct's copy, which crosses to C behind a pointer \ + only — declare (Ptr %s) and let the C side read it" + what (source t') (source t') | Ast.Tname n when Hashtbl.mem env.structs n -> ignore (cty env ~needed ~loc ~what t'); (Pstruct n, ctype_name n) @@ -545,6 +662,8 @@ let typedefs env needed = (fun (f : Ast.field) -> match (unalias env f.Ast.fty).Ast.t with | Ast.Tname m when Hashtbl.mem env.structs m -> define m + | Ast.Tapp (m, args) when Hashtbl.mem env.structs m && is_template env m -> + define (copy env ~loc:f.Ast.floc m args) | _ -> ()) fs; Printf.bprintf b "struct %s_s { /* %s */\n" (ctype_name n) n; diff --git a/lib/source.ml b/lib/source.ml index 102da4fd..d6429e19 100644 --- a/lib/source.ml +++ b/lib/source.ml @@ -33,15 +33,21 @@ type syntax = Paren | Indented let code_syntax = ref Paren let code_at : (int * int) option ref = ref None +(* The column of the statement editor code was cut out of, when the code + starts after that statement's first word: see [Indent_reader.layout]. *) +let code_indent : int option ref = ref None + let syntax_of_field = function | Some ("indented" | "fln") -> Indented | _ -> Paren -let with_code ~syntax ~at f = - let s = !code_syntax and a = !code_at in +let with_code ?indent ~syntax ~at f = + let s = !code_syntax and a = !code_at and i = !code_indent in code_syntax := syntax; code_at := at; - Fun.protect ~finally:(fun () -> code_syntax := s; code_at := a) f + code_indent := indent; + Fun.protect + ~finally:(fun () -> code_syntax := s; code_at := a; code_indent := i) f (* The paren reader started at a line and column: [Reader.read_all] always starts at 1:1. *) @@ -65,7 +71,7 @@ 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 ~file code with + (match Indent_reader.read_all ~line ~col ?indent:!code_indent ~file code with | (first :: _ :: _ as forms) when expr -> let last = List.nth forms (List.length forms - 1) in let loc = diff --git a/lib/types.ml b/lib/types.ml index 3513621f..a6f25857 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -106,6 +106,15 @@ type t = | Fn of t list * t (* (Fn [T ...] R) *) | CFn of t list * t (* (CFn [T ...] R) *) | Var of string (* a type variable — milestone 5 *) + (* The two halves of a length parameter, and neither is the type of a value. + [Len] is a length standing where a generic struct's argument goes — the 8 + in (Small 8 i32) — and what a length variable is bound to. [LArray] is a + fixed array whose length is a variable, [[$n $t]], and exists only in a + generic signature, as the pattern a call site binds [n] from. A generic + body is checked with its lengths at [Check.abstract_len], so neither ever + reaches a backend. *) + | Len of int64 + | LArray of string * t (* [dyn]: one machine word whose contents the runtime knows and this module does not. It is a written type — [(defonce x dyn 5)] boxes the 5 — and it is also what an unannotated [defn] parameter means, which is why it is a @@ -206,8 +215,29 @@ let rec equal a b = && List.for_all2 equal ps ps' && equal r r' | Var x, Var y -> String.equal x y + | Len x, Len y -> Int64.equal x y + | LArray (n, x), LArray (m, y) -> String.equal n m && equal x y | _ -> false +(* How a generic struct's copy is spelled to a reader. The copy is an + ordinary struct under a symbol-safe key — [Small-8-i32] — and this is the + key's written form, [(Small 8 i32)], filled in as each copy is made. Global + rather than on a checker's env because every message that prints a type + comes through here with no env in hand. The key determines the spelling, + so an entry left from an earlier program in the same process is wrong only + for a struct that program's successor declares under a copy's key by hand, + and then only in how a message spells it. *) +let display : (string, string) Hashtbl.t = Hashtbl.create 16 + +(* A struct's name as a printed value's head: its own name, or for a generic + struct's copy the template and its arguments, [Pair i32] — so a value + prints as [(Pair i32 {.a 1 .b 2})], the way its type is written. *) +let struct_head n = + match Hashtbl.find_opt display n with + | Some d when String.length d >= 2 && d.[0] = '(' -> + String.sub d 1 (String.length d - 2) + | _ -> n + let rec to_string = function | Int k -> ikind_name k | Float k -> fkind_name k @@ -215,7 +245,8 @@ let rec to_string = function | String -> "string" | Unit -> "()" | Never -> "Never" - | Named n | Enum n -> n + | Named n -> (match Hashtbl.find_opt display n with Some d -> d | None -> n) + | Enum n -> n | Slice (Mut, t) -> "[" ^ to_string t ^ "]" | Slice (Const, t) -> "[const " ^ to_string t ^ "]" | Array (n, t) -> Printf.sprintf "[%Ld %s]" n (to_string t) @@ -231,7 +262,9 @@ let rec to_string = function | CFn (ps, r) -> Printf.sprintf "(CFn [%s] %s)" (String.concat " " (List.map to_string ps)) (to_string r) - | Var n -> n + | Var n -> "$" ^ n + | Len n -> Int64.to_string n + | LArray (n, t) -> Printf.sprintf "[$%s %s]" n (to_string t) | Dyn -> "dyn" let is_numeric = function Int _ | Float _ -> true | _ -> false diff --git a/lib/x86.ml b/lib/x86.ml index 10b3b60a..6b409272 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -533,6 +533,7 @@ let is_agg (t : Types.t) = the arithmetic. *) | Types.Dyn -> false | Types.Var v -> unsupported "type variable %s" v + | Types.Len _ | Types.LArray _ -> unsupported "length variable" let is_void (t : Types.t) = match t with Types.Unit | Types.Never -> true | _ -> false let is_float (t : Types.t) = match t with Types.Float _ -> true | _ -> false diff --git a/spec-syntax.md b/spec-syntax.md index c43bebb5..1da02171 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -202,9 +202,17 @@ Each item: the proposal, then the reason in one line. Rect(w, h) -> w * h :north -> 0 _ -> 0 + + match code + 404 -> "missing" + -1 -> "none" + "ok" -> "fine" + \a -> "a" + _ -> "other" ``` An arm's body can be an indented block, which reads as `(do …)`. **Built** (a - one-line block reads as that line). + one-line block reads as that line). A number, char or string pattern is the + literal as written, compared as `(= t lit)`. - **Conditions**, clauses at the header's column: ``` @@ -357,6 +365,10 @@ Each step lands on its own, with `dune test --root .` green. `flan--pause-bounds` at `flan.el:2637-2660`) must equal the start location the reader gave that form. `Ast.mark_pause` matches exactly (`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 + 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/generic-struct.flan b/test/programs/generic-struct.flan new file mode 100644 index 00000000..f295fbd8 --- /dev/null +++ b/test/programs/generic-struct.flan @@ -0,0 +1,109 @@ +;;;; Generic structs, end to end: type parameters and length parameters. +;;;; +;;;; A defstruct whose fields introduce $t is a template, and each set of +;;;; arguments it is given is a copy — an ordinary struct. A parameter is a +;;;; length when it stands in an array's length slot, and a type anywhere else; +;;;; the arguments are written in the order the fields first introduce them. +;;;; +;;;; Small is Odin's Small_Array: a fixed-capacity array with a count, and no +;;;; allocation anywhere. + +(defstruct Small [items [$n $t] count i32]) + +;; A generic function over a generic struct binds both of its parameters from +;; the argument, and reads the length back as a value. +(defn append! [s (Ptr (Small $n $t)) x $t] bool + (if (< (.count s) n) + (do (set (at (.items s) (.count s)) x) + (set (.count s) (+ (.count s) 1)) + true) + false)) + +;; One generic over the struct calling another at its own variables. +(defn append-all! [s (Ptr (Small $n $t)) xs [$t]] () + (dotimes [i (length xs)] + (append! s (at xs i)))) + +(defn pop! [s (Ptr (Small $n $t))] (Option $t) + (if (= (.count s) 0) + None + (do (set (.count s) (- (.count s) 1)) + (Some (at (.items s) (.count s)))))) + +(defn capacity [s (Ptr (Small $n $t))] i32 n) + +(defn total [s (Ptr (Small $n $t))] $t {:where (numeric? $t)} + (let [acc (the $t 0)] + (dotimes [i (.count s)] + (set acc (+ acc (at (.items s) i)))) + acc)) + +;; A type parameter alone, built positionally with the type read off the +;; fields, and returned under a variable. +(defstruct Pair [a $t b $t]) + +(defn swapped [p (Pair $t)] (Pair $t) (Pair (.b p) (.a p))) + +;; A copy that names itself through a pointer, and a literal field that +;; takes its width from the one beside it. +(defstruct Node [v $t next (Option (Ptr (Node $t)))]) + +(defn sum-list [n (Ptr (Node i64))] i64 + (loop [at n acc (the i64 0)] + (let [acc (+ acc (.v at))] + (match (.next at) + (Some p) (recur p acc) + None acc)))) + +;; A template naming another at its own parameters. +(defstruct Twice [x (Small $m $u) y (Small $m $u)]) + +;; A copy as a map key, and a named function over one handed where a +;; function value is wanted. +(defn pair-sum [p (Pair i32)] i32 (+ (.a p) (.b p))) +(defn apply-to [f (Fn [(Pair i32)] i32) p (Pair i32)] i32 (f p)) + +;; A length variable straight on an array parameter. +(defn len-of [a [$k $e]] i32 k) + +(defconst cap 3) + +(defn main [] i32 + (let [s (the (Small 4 i32) (zeroed)) + f (the (Small cap f64) (zeroed))] + (append! (addr s) 10) + (append! (addr s) 20) + (append! (addr s) 30) + (println (total (addr s)) (.count s) (capacity (addr s))) + (append! (addr f) 1.5) + (append! (addr f) 2.5) + (append! (addr f) 3.5) + (println (append! (addr f) 4.5) (total (addr f)) (capacity (addr f))) + (println (pop! (addr f)) (pop! (addr f)) (.count f)) + (let [p (Pair 1 2) + q (swapped p) + r (swapped (Pair {.a 1.5 .b 2.5}))] + (println (.a q) (.b q) (.a r) (.b r)) + (println q (Pair 1 2.5))) + (let [c (the (Node i64) {.v 3}) + b (Node 2 (Some (addr c))) + a (Node 1 (Some (addr b)))] + (println (sum-list (addr a)))) + (let [w (the (Twice 2 u8) (zeroed))] + (append! (addr (.y w)) 7) + (println (.count (.x w)) (.count (.y w)) (capacity (addr (.x w))))) + (println (len-of [1 2 3]) (len-of [1.5 2.5])) + (let [v (vec-new (Pair i32))] + (push v (Pair 5 6)) + (println (.b (at v 0))) + (free v)) + (let [t (the (Small 5 i64) (zeroed)) + xs (the [3 i64] [1 2 3])] + (append-all! (addr t) (slice xs)) + (println (total (addr t)) (.count t))) + (let [m (map-new (Pair i32) i32)] + (put m (Pair 1 2) 12) + (put m (Pair 3 4) 34) + (println (get m (Pair 3 4)) (get m (Pair 2 1)) (apply-to pair-sum (Pair 7 8))) + (free m)) + 0)) diff --git a/test/programs/match-literal.flan b/test/programs/match-literal.flan new file mode 100644 index 00000000..9253b7aa --- /dev/null +++ b/test/programs/match-literal.flan @@ -0,0 +1,71 @@ +;;;; match over numbers, chars, strings and dyn values: each arm is (= t lit) +;;;; over one temporary, and a _ arm is the rest. + +(defn small [n i16] string + (match n + 5 "five" + -3 "minus three" + _ "other")) + +(defn half [x f32] i32 + (match x + 0.5 1 + 2 2 + _ 0)) + +(defn letter [c u8] i32 + (match c + \a 1 + 98 2 + _ 0)) + +(defn command [s string] i32 + (match s + "go" 1 + "stop" 2 + "" 3 + _ 0)) + +(defn big [n u64] i32 + (match n + 18446744073709551615 1 + _ 0)) + +;; Over a dyn the test is dyn =, so 1 matches 1.0 and "go" matches only a +;; string. +(defn kind [d dyn] string + (match d + 1 "one" + 2.5 "two and a half" + "go" "go" + _ "other")) + +(defn calls [] i32 + (print "(called) ") + 7) + +;; recur from inside an arm: the arm is the loop's tail. +(defn count-down [from i32] i32 + (loop [n from steps 0] + (match n + 0 steps + _ (recur (- n 1) (+ steps 1))))) + +(defn main [] i32 + (println (small 5)) + (println (small -3)) + (println (small 4)) + (print (half 0.5)) (print (half 2.0)) (print (half 3.0)) (println "") + (print (letter 97)) (print (letter 98)) (print (letter 99)) (println "") + (print (command "go")) (print (command "stop")) (print (command "")) + (print (command "gone")) (println "") + (print (big 18446744073709551615)) (print (big 1)) (println "") + (println (kind 1)) + (println (kind 1.0)) + (println (kind 2.5)) + (println (kind "go")) + (println (kind "1")) + ;; The scrutinee is evaluated once, however many arms test it. + (println (match (calls) 1 "a" 2 "b" 7 "seven" _ "c")) + (print (count-down 4)) (println "") + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 1234ccf3..0691f63c 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -432,6 +432,19 @@ let () = match_enum_out; outputs ~dev:true "match over an enum, dev" "programs/match-enum.flan" match_enum_out; + (* A literal match is the same chain with (= t lit) as each test, so the + dyn rows (1 and 1.0 both "one") are dyn ='s answer. *) + let match_lit_out = + "five\nminus three\nother\n120\n120\n1230\n10\none\none\n\ + two and a half\ngo\nother\n(called) seven\n4\n" + in + outputs "match over literals" "programs/match-literal.flan" match_lit_out; + outputs ~opt:"-O0" "match over literals, -O0" "programs/match-literal.flan" + match_lit_out; + outputs ~x86:true "match over literals, --x86" "programs/match-literal.flan" + match_lit_out; + outputs ~dev:true "match over literals, dev" "programs/match-literal.flan" + match_lit_out; (* update, ++ and -- evaluate their place's subexpressions once: the counts are the number of calls an index or a key function got. *) let update_out = "3\n11 20 90\n1 1 3\n16\n2\n7 1\n32\n2 50\n" in @@ -3531,6 +3544,19 @@ let () = outputs "generics" "programs/generics.flan" generics_out; outputs ~opt:"-O0" "generics, -O0" "programs/generics.flan" generics_out; + (* Generic structs — see the program's header. The third line is two pops + printed in one call, which is also the pin for a printed call being + evaluated once: the walk reads an option's tag and then its payload, + and each read used to make the call again. *) + let generic_struct_out = + "60 3 4\nfalse 7.5 3\n(some 3.5) (some 2.5) 1\n2 1 2.5 1.5\n\ + (Pair i32 {.a 2 .b 1}) (Pair f64 {.a 1 .b 2.5})\n6\n\ + 0 1 2\n3 2\n6\n6 3\n(some 34) none 15\n" + in + outputs "generic structs" "programs/generic-struct.flan" generic_struct_out; + outputs ~x86:true "generic structs, --x86" "programs/generic-struct.flan" + generic_struct_out; + (* integer?, end to end — see the program's own header. The first eight lines are the collapsed abs at six widths and both signed minimums (which answer themselves; the negation wraps). The [0 0] after them is @@ -3673,7 +3699,7 @@ let () = chain of instantiations and not a depth it gave up at. *) refuses "an unconstrained operator in a generic body" "programs/generic-reject.flan" - "nothing declares t numeric?"; + "nothing declares $t numeric?"; refuses "an unconstrained operator names the way out" "programs/generic-reject.flan" "{:where (numeric? $t)}"; refuses "a runaway instantiation" "programs/generic-runaway.flan" @@ -4287,6 +4313,36 @@ level "1" end in let v2 = "(defstruct Vector2 [x f32 y f32])\n" in + + (* A generic struct's copy crosses behind a pointer, as a typedef of its + own with the arguments written in, and one held by value inside a + struct is defined before that struct. clang reads the text, so the + typedef is C and not only a spelling. *) + let gsrc = + "(defstruct G [x $t count i32])\n\ + (defstruct O [v i32 inner (G u8)])\n\ + (declare-c c-g [s (Ptr (G u8))] i32 \"c_g\")\n\ + (declare-c c-o [o (Ptr O)] i32 \"c_o\")\n" + in + shim_case "declare-c: a generic struct's copy crosses behind a pointer" gsrc + [ "/* (G u8) */\n uint8_t x;\n int32_t count;\n"; "inner;\n" ]; + (match shim_of gsrc with + | c -> + let file = Filename.temp_file "flan-shim-generic" ".c" in + let oc = open_out file in + output_string oc c; + close_out oc; + if Sys.command (Printf.sprintf "clang -fsyntax-only %s" (Filename.quote file)) <> 0 + then begin + incr failures; + print_endline "FAIL declare-c: a generic struct's copy is C clang accepts" + end; + Sys.remove file + | exception Loc.Error _ -> ()); + shim_refuses "declare-c: a generic struct's copy by value" + "(defstruct G [x $t])\n(declare-c c-v [s (G u8)] i32 \"c_v\")" + "crosses to C behind a pointer only"; + let img = "(defstruct Image [data (Ptr u8) width i32 height i32])\n" in @@ -4793,10 +4849,10 @@ level "1" the easier of the two to leave open. *) refuses "a nested function type does not widen" "programs/fn-generic-nested.flan" - "hof expects (Fn [(Fn [t] t)] i32) here"; + "hof expects (Fn [(Fn [$t] $t)] i32) here"; refuses "and neither does one in return position" "programs/fn-generic-nested-return.flan" - "call-twice expects (Fn [] (Fn [] t)) here"; + "call-twice expects (Fn [] (Fn [] $t)) here"; outputs ~dev:true "an fn capturing by value, dev" "programs/fn-capture.flan" fn_capture_out; diff --git a/test/test_dev.ml b/test/test_dev.ml index 2fbffd7f..6789224b 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -802,6 +802,28 @@ let () = | Some { Form.v = Form.Sym "t"; _ } -> () | _ -> fail "an expression against the park reported the program live"); + (* A generic struct's copy prints the way its type is written, with the + arguments after the template's name, and [layout] answers to that + spelling. *) + let r = + request c + "(:op \"eval\" :code \"(defstruct GPair [a $t b $t])\" :file \"/tmp/buf.flan\")" + in + if status r <> "ok" then + fail "a generic struct at the daemon: %s" + (Option.value ~default:(status r) (Wire.string_field r "message")); + let r = + request c + "(:op \"eval-expr\" :code \"(GPair 1 2)\" :file \"/tmp/buf.flan\")" + in + if Wire.string_field r "value" <> Some "(GPair i32 {.a 1 .b 2})" then + fail "a generic struct's copy printed as %s" + (Option.value ~default:(status r) (Wire.string_field r "value")); + let r = request c "(:op \"layout\" :type \"GPair i32\")" in + if Wire.string_field r "type" <> Some "(GPair i32)" then + fail "layout of a copy by its printed head: %s" + (Option.value ~default:(status r) (Wire.string_field r "message")); + (* And the half that needs the process rather than only the compiler. [extra] is a global this session introduced and the first run left at 105 — the third reload's [step] does not touch it — so this is the diff --git a/test/test_flan.ml b/test/test_flan.ml index c1cae02e..4135988e 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1417,7 +1417,7 @@ let () = accepts "all-distinct over a type variable" "(defn three [a $t b $t c $t] bool {:where (equal? $t)} (!= a b c))"; rejects_check "a chain still wants the right predicate" - ~needle:"nothing declares t ordered?" + ~needle:"nothing declares $t ordered?" "(defn between [a $t b $t c $t] bool {:where (equal? $t)} (< a b c))"; (* One operand and none. Both would have to be [true] whatever they were handed, which is a typo carrying a value. *) @@ -1470,10 +1470,10 @@ let () = can actually be written there; the parameter-vector suggestion survives where it works, which the return-type pin further down exercises. *) rejects_check "a real type variable at a field" "(defstruct Holder [x elem])" - ~needle:"a field is built at one type for every value"; + ~needle:"in a defstruct's fields that makes the struct generic over it"; rejects_check "and the field message offers what a field can hold" "(defstruct Holder [x elem])" - ~needle:"Write a concrete type here, or dyn to hold any value"; + ~needle:"Write $elem, a concrete type, or dyn to hold any value"; rejects_check "an unknown concrete type" "(defn f [x Widget] ())" ~needle:"unknown type Widget"; @@ -2693,7 +2693,7 @@ let () = accepts "a wildcard arm is exhaustive" "(defn g [] (Option i32) None) (defn f [] i32 (match (g) (Some v) v _ 0))"; rejects_check "match on a non-Option" - "(defn f [x i32] i32 (match x _ 0))" ~needle:"match works on an Option"; + "(defn f [x bool] i32 (match x _ 0))" ~needle:"match works on an Option"; (* ── Names, order-independence, entry point ────────────────────── *) accepts "mutually recursive, no forward declaration" @@ -2876,13 +2876,11 @@ let () = (* [(Pair i32)] in a defonce falls down the value fork now that the third element takes either reading, and the generics answer the type fork gave it has to be reachable from here too. *) - (* A capitalised head with arguments is a *type* given type arguments, and - that is the half of generics that is not built — Types.Named is a bare - string with no room for parameters. The sentence says which half, since - generic functions are here and pointing at them is the useful part. *) + (* A capitalised head with arguments is a *type* given type arguments; with + no such struct declared, the sentence says how one is. *) rejects_check "a capitalised call with arguments is a generic type" "(defonce x (Pair i32)) (defn f [] i32 0)" - ~needle:"is a generic type, which is not there yet"; + ~needle:"no struct or generic struct Pair is declared"; accepts "and the generic function it points at is" "(defn pair-fst [a $t b $u] $t (do b a))\n\ (defn main [] () (println (pair-fst 1 true)))"; @@ -4662,8 +4660,85 @@ let () = (k ^ "(defn f [k K] i32 (match k :lo 1 _ \"x\"))") ~needle:"expected i32, found string"; rejects_check "match over something that is none of them" - "(defn f [n i32] i32 (match n _ 2))" - ~needle:"match works on an Option, a data type or an enum, not on i32"; + "(defn f [n bool] i32 (match n _ 2))" + ~needle:"match works on an Option, a data type, an enum, a number, a \ + string or a dyn, not on bool"; + + (* ── match over literals ───────────────────────────────────────── *) + + (* Each arm is (= t lit) with the literal built at the scrutinee's type, so + a literal that type cannot hold is refused rather than widened into an + arm that never matches. *) + accepts "match over an i16, a literal arm built at i16" + "(defn f [n i16] i32 (match n 5 1 -3 2 _ 0))"; + accepts "match over a string" "(defn f [s string] i32 (match s \"go\" 1 _ 0))"; + accepts "match over a dyn, arms of several kinds" + "(defn f [d dyn] i32 (match d 1 1 2.5 2 \"go\" 3 \\a 4 _ 0))"; + accepts "match over a number, :else for the rest" + "(defn f [n i32] i32 (match n 5 1 :else 0))"; + rejects_check "a literal arm the scrutinee cannot hold" + "(defn f [n i8] i32 (match n 300 1 _ 0))" + ~needle:"this match is over i8, so each arm has to be an i8, and 300 does \ + not fit in one. Change the arm to a value an i8 holds, or remove it"; + rejects_check "a float arm over an integer" + "(defn f [n i32] i32 (match n 1.5 1 _ 0))" + ~needle:"and 1.5 is not a whole number"; + rejects_check "a string arm over a number" + "(defn f [n i32] i32 (match n \"a\" 1 _ 0))" + ~needle:"and \"a\" is a string"; + rejects_check "a number arm over a string" + "(defn f [s string] i32 (match s 5 1 _ 0))" + ~needle:"so each arm has to be a string, and 5 is a number"; + rejects_check "a literal match with no _ arm" + "(defn f [n i32] i32 (match n 5 1 6 2))" + ~needle:"this match is not exhaustive — its arms are literals, and no list \ + of them covers every i32. Add a _ arm for the rest, as in (match \ + n 5 1 _ 0)"; + rejects_check "a literal match over a dyn with no _ arm" + "(defn f [d dyn] i32 (match d 5 1))" + ~needle:"covers every dyn value"; + rejects_check "a literal named twice" + "(defn f [n i32] i32 (match n 5 1 5 2 _ 0))" + ~needle:"this match has two 5 arms"; + rejects_check "a char and a number that are one u8" + "(defn f [c u8] i32 (match c \\a 1 97 2 _ 0))" + ~needle:"this match has two \\a arms — 97 equals it as a u8"; + rejects_check "1 and 1.0 are one arm over a dyn, as dyn = says" + "(defn f [d dyn] i32 (match d 1 1 1.0 2 _ 0))" + ~needle:"this match has two 1 arms — 1.0 equals it as a dyn"; + rejects_check "two literals that round to one f32" + "(defn f [x f32] i32 (match x 0.1 1 0.10000000001 2 _ 0))" + ~needle:"this match has two 0.1 arms — 0.10000000001 equals it as an f32"; + rejects_check "two integers that round to one f32" + "(defn f [x f32] i32 (match x 16777216 1 16777217 2 _ 0))" + ~needle:"two 16777216 arms — 16777217 equals it as an f32"; + rejects_check "an integer and a float that are one f64" + "(defn f [x f64] i32 \ + (match x 4611686018427387904 1 4611686018427387904.0 2 _ 0))" + ~needle:"equals it as an f64"; + accepts "two f64 literals that differ" + "(defn f [x f64] i32 (match x 0.1 1 0.10000000001 2 _ 0))"; + rejects_check "a literal no dyn holds" + "(defn f [x dyn] i32 (match x 18446744073709551615 1 _ 0))" + ~needle:"this match is over a dyn, which holds a number as an i64 or an \ + f64, and 18446744073709551615 fits in neither. Change the arm to \ + a value an i64 holds, or remove it"; + rejects_check "a keyword arm among literal arms" + "(defn f [n i32] i32 (match n 5 1 :lo 2 _ 0))" + ~needle:":lo is an enum member, and this match is over i32, whose arms are \ + literals, as in (match n 5 1 _ 0)"; + rejects_check "a case arm among literal arms" + "(defn f [n i32] i32 (match n 5 1 (Some x) 2 _ 0))" + ~needle:"Some names a case, and this match is over i32"; + rejects_check "a literal arm among keyword arms" + (k ^ "(defn f [k K] i32 (match k :lo 1 5 2 _ 0))") + ~needle:"5 is a literal, and this match is over the enum K"; + rejects_check "a literal arm over an Option" + "(defn f [o (Option i32)] i32 (match o 5 1 _ 0))" + ~needle:"5 is a literal, and this match is over an Option"; + rejects_check "a literal match over a byte slice, which = does not compare" + "(defn f [b [u8]] i32 (match b \"a\" 1 _ 0))" + ~needle:"not on [u8]"; (* A destructuring pattern in an arm's binds is a name position like any other. *) rejects_check "a pattern inside a match arm's binds" @@ -6265,6 +6340,110 @@ let () = check "and they are in source order" (List.map (fun (d : Loc.diag) -> d.Loc.dloc.Loc.line) ds = [ 1; 2; 3 ])); + (* A generic whose abstract pass was refused is not checked again at each + copy: the refusal is one error, however many types call it, and the + caller's own later refusal is still found. *) + (match + Check.program_all + (Parse.program_all + (read "(defn g [x $t] u64 (nosuch x))\n\ + (defn main [] i32 (g 3) (g true) nope 0)\n")) + with + | _ -> check "a refused generic body is refused" false + | exception Loc.Errors ds -> + check "a refused generic body is one error, and its caller's is another" + (List.map (fun (d : Loc.diag) -> d.Loc.dloc.Loc.line) ds = [ 1; 2 ])); + + (* A refusal inside a copy names the call that asked for it, and each copy + between: the chain walks back to the line the programmer wrote. *) + (match + checked + "(defn show [v $t] () (println v)) \ + (defn outer [v $t] () (show v)) \ + (defn main [] i32 (outer main) 0)" + with + | _ -> check "a copy with no printer is refused" false + | exception Loc.Error d -> + let notes = List.map (fun (n : Loc.note) -> n.Loc.nmsg) d.Loc.notes in + check "a refusal in a copy names both instantiations" + (contains d.Loc.dmsg "no printer for" + && notes + = [ "show is instantiated at $t = (CFn [] i32) here"; + "outer is instantiated at $t = (CFn [] i32) here" ])); + + (* A refusal made while collecting declarations — a generic struct that + holds itself, one that grows without end, a where clause over a length — + is one error among the rest of the file's, not the end of the check. *) + let all_lines src = + match Check.program_all (Parse.program_all (read src)) with + | _ -> [] + | exception Loc.Errors ds -> + List.map (fun (d : Loc.diag) -> d.Loc.dloc.Loc.line) ds + in + check "a self-containing generic struct is one error of several" + (all_lines + "(defstruct Loop [next (Loop $t)])\n\ + (defn g [] i32 (let [p (the (Loop i32) (zeroed))] nope1))\n\ + (defn h [] i32 nope2)\n" + = [ 1; 2; 3 ]); + check "a generic struct that grows without end is one error of several" + (all_lines + "(defstruct Grow [next (Ptr (Grow [$t]))])\n\ + (defn g [] i32 (let [p (the (Grow i32) (zeroed))] nope1))\n\ + (defn h [] i32 nope2)\n" + = [ 1; 2; 3 ]); + check "a where clause over a length is one error of several" + (all_lines + "(defn f [a [$n i32]] i32 {:where (numeric? $n)} nope1)\n\ + (defn h [] i32 nope2)\n" + = [ 1; 1; 2 ]); + (* A literal that does not fit what a typed field decided names that field. *) + (match + checked + "(defstruct Pair [a $t b $t]) \ + (defn main [] i32 (let [p (Pair (the i32 1) 2.5)] 0))" + with + | _ -> check "a float literal where a typed field decided i32" false + | exception Loc.Error d -> + check "the refusal names the field that decided the variable" + (contains d.Loc.dmsg "Pair's .b is $t, which is i32 here" + && List.exists + (fun (n : Loc.note) -> + contains n.Loc.nmsg ".a is i32 here, which decides $t") + d.Loc.notes)); + + (* A copy that cannot be built at a closure's type: the zeroed value in the + body is refused there, and the call that asked is named. *) + (match + checked + "(defn blank [x $t] $t (let [z (the $t (zeroed))] z)) \ + (defn use-it [f (Fn [i32] i32)] i32 (blank f) 0)" + with + | _ -> check "a zeroed closure in a copy is refused" false + | exception Loc.Error d -> + check "a copy at a closure type names the call that asked" + (List.exists + (fun (n : Loc.note) -> + contains n.Loc.nmsg "blank is instantiated at $t = (Fn [i32] i32) here") + d.Loc.notes)); + + (* A prelude generic's body is nobody's source at the call: the refusal is + at the call, and the prelude's line is a note. *) + (match + checked + "(defn keep [g (Vec u8)] bool true) \ + (defn use-it [xs [(Vec u8)]] i32 (length (filter xs keep)))" + with + | _ -> check "a prelude copy that cannot be built is refused" false + | exception Loc.Error d -> + check "a prelude copy's refusal is at the user's call" + (d.Loc.dloc.Loc.file <> Prelude.file + && contains d.Loc.dmsg "filter cannot be made at $t = (Vec u8)" + && not (contains d.Loc.dmsg "clone") + && List.exists + (fun (n : Loc.note) -> n.Loc.nloc.Loc.file = Prelude.file) + d.Loc.notes)); + (* The parser resynchronises on a top-level form, so two bad declarations are two errors rather than one. *) (match Parse.program_all (read "(defn a)\n(defn b)\n") with @@ -6323,7 +6502,7 @@ let () = accepts "numeric? admits +" "(defn add [a $t b $t] $t {:where (numeric? $t)} (+ a b))"; rejects_check "equal? does not admit <" - ~needle:"nothing declares t ordered?" + ~needle:"nothing declares $t ordered?" "(defn less [a $t b $t] bool {:where (equal? $t)} (< a b))"; (* The entailments, which are the reason a signature is one predicate long rather than two. Every type the language orders is a number or an enum, @@ -6351,10 +6530,10 @@ let () = accepts "integer? admits the shifts" "(defn dbl [x $t] $t {:where (integer? $t)} (<< x 1))"; rejects_check "numeric? does not admit bit-and" - ~needle:"nothing declares t integer?" + ~needle:"nothing declares $t integer?" "(defn low? [x $t] bool {:where (numeric? $t)} (= (bit-and x 1) 1))"; rejects_check "nor the shifts" - ~needle:"nothing declares t integer?" + ~needle:"nothing declares $t integer?" "(defn dbl [x $t] $t {:where (numeric? $t)} (<< x 1))"; (* An integer?-bounded caller satisfies a numeric?-bounded callee: the entailment carries across generic calls exactly as ordered?-over-equal? @@ -6957,19 +7136,118 @@ let () = bound, because inside a signature that introduces one the mistake is nearly always the second spelling of the first. *) rejects_check "vec-new over a sigil that names no variable in scope" - ~needle:"this signature introduces t, so write t here" + ~needle:"this signature introduces $t, so write $t here" "(defn f [x $t] i32 (do x (let [v (vec-new $u)] (free v) 0)))"; rejects_check "and a cast over one tells the same story" - ~needle:"this signature introduces t, so write t here" + ~needle:"this signature introduces $t, so write $t here" "(defn f [x i32 d $t] $t {:where (numeric? $t)} (do d ($u x)))"; rejects_check "two variables in scope are both named" - ~needle:"introduces t and u, so write one of those" + ~needle:"introduces $t and $u, so write one of those" "(defn f [a $t b $u] i32 (do a b (let [v (vec-new $w)] (free v) 0)))"; (* Where no variable is in scope there is none to name, and the answer is the rule: a sigil binds, and only a defn signature is a binding site. *) - rejects_check "a sigil in a struct field, where nothing can bind one" - ~needle:"only a defn signature can" - "(defstruct S [v $t])"; + rejects_check "a sigil in a data case's field, where nothing can bind one" + ~needle:"only a defn signature or a defstruct's fields can" + "(defdata D [(C [v $t])])"; + + (* ── Generic structs: what is refused, and where ─────────────────── *) + rejects_check "a generic struct given the wrong number of arguments" + ~needle:"Pair takes 1 argument, (Pair $t), and this gives 2" + "(defstruct Pair [a $t b $t]) (defn f [p (Pair i32 i64)] i32 0)"; + rejects_check "a generic struct named with no arguments" + ~needle:"Pair is generic, and a type only once it is given its arguments" + "(defstruct Pair [a $t b $t]) (defn f [p Pair] i32 0)"; + rejects_check "a type where a length argument goes" + ~needle:"Small's $n is a length" + "(defstruct Small [items [$n $t] count i32]) \ + (defn f [p (Small i32 4)] i32 0)"; + rejects_check "a length where a type argument goes" + ~needle:"Small's $t is a type, and 4 is a length" + "(defstruct Small [items [$n $t] count i32]) \ + (defn f [p (Small 4 4)] i32 0)"; + rejects_check "a negative length argument" + ~needle:"-1 is negative" + "(defstruct Small [items [$n $t] count i32]) \ + (defn f [p (Small -1 i32)] i32 0)"; + rejects_check "one variable as both a length and a type" + ~needle:"$t stands for a length in one place here and a type in another" + "(defstruct Bad [x $t y [$t i32]])"; + rejects_check "a length variable where a type goes" + ~needle:"n is a length, not a type" + "(defn f [a [$n i32]] i32 (let [x (the n 0)] 0))"; + rejects_check "a where clause over a length variable" + ~needle:"$n is a length, and a where clause takes type predicates only" + "(defn f [a [$n i32]] i32 {:where (numeric? $n)} 0)"; + rejects_check "a generic struct that contains itself by value" + ~needle:"(Loop $t) contains itself by value" + "(defstruct Loop [next (Loop $t)])"; + rejects_check "a generic struct that asks for bigger copies of itself" + ~needle:"Grow names a copy of itself at a type built around its own" + "(defstruct Grow [next (Ptr (Grow [$t]))]) (defn f [p (Grow i32)] i32 0)"; + rejects_check "a copy whose key is already a struct's name" + ~needle:"Pair at these arguments is called Pair-i32, and Pair-i32 is \ + already defined" + "(defstruct Pair [a $t b $t]) (defstruct Pair-i32 [x i32]) \ + (defn f [p (Pair i32)] i32 0)"; + rejects_check "a generic struct literal whose fields decide nothing" + ~needle:"Pair's $t is not decided by the fields given here" + "(defstruct Pair [a $t b $t]) (defn f [] i32 (let [p (Pair {})] 0))"; + rejects_check "two fields that disagree about the variable" + ~needle:"(Pair $t)'s .b is i32 here, and this is f64" + "(defstruct Pair [a $t b $t]) \ + (defn f [] i32 (let [p (Pair (the i32 1) (the f64 2.5))] 0))"; + accepts "a literal field takes its width from a typed one beside it" + "(defstruct Pair [a $t b $t]) \ + (defn f [] f64 (let [p (Pair 1 (the f64 2.5))] (.a p)))"; + rejects_check "a generic struct as a condition" + ~needle:"Pair is generic, and a condition struct is not" + "(defstruct Pair :parent Error [a $t])"; + rejects_check "an operator a generic body's struct field does not support" + ~needle:"+ over the type variable $t" + "(defstruct Pair [a $t b $t]) (defn f [p (Pair $t)] $t (+ (.a p) (.b p)))"; + accepts "the same body with the predicate declared" + "(defstruct Pair [a $t b $t]) \ + (defn f [p (Pair $t)] $t {:where (numeric? $t)} (+ (.a p) (.b p))) \ + (defn main [] i32 (f (Pair 1 2)))"; + accepts "a copy wanted where it is built takes its type from there" + "(defstruct Pair [a $t b $t]) (defn f [] (Pair i64) (Pair 1 2))"; + (* A copy whose field is refused names each use that asked for it. *) + (match + checked + "(defstruct Box [f $t]) (defstruct Outer [b (Box $w)]) \ + (defn go [g (Fn [i32] i32)] i32 \ + (.x (the (Outer (Fn [i32] i32)) (zeroed))) 0)" + with + | _ -> check "a copy with a zeroed function field is refused" false + | exception Loc.Error d -> + let notes = List.map (fun (n : Loc.note) -> n.Loc.nmsg) d.Loc.notes in + check "a refused copy names each use that made it" + (List.mem "(Box (Fn [i32] i32)) is made here" notes + && List.mem "(Outer (Fn [i32] i32)) is made here" notes)); + rejects_check "a bare generic struct in ordinary code suggests real arguments" + ~needle:"write (Pair i32)" + "(defstruct Pair [a $t b $t]) (defn main [] i32 (let [p (the Pair (zeroed))] 0))"; + rejects_check "a generic struct applied to nothing" + ~needle:"Pair takes 1 argument, (Pair $t), and this gives 0" + "(defstruct Pair [a $t b $t]) \ + (defn main [] i32 (let [p (the (Pair) (zeroed))] 0))"; + rejects_check "a length argument that is not one" + ~needle:"(+ n 1) is not a type or a length" + "(defstruct Small [items [$n $t] count i32]) \ + (defn main [] i32 (let [n 3 p (the (Small (+ n 1) i32) (zeroed))] 0))"; + accepts "a length argument of literal arithmetic is folded" + "(defstruct Small [items [$n $t] count i32]) \ + (defn main [] i32 (let [p (the (Small (+ 1 2) i32) (zeroed))] \ + (length (.items p))))"; + accepts "two literal fields meet at the wider type" + "(defstruct Pair [a $t b $t]) \ + (defn f [] f64 (let [p (Pair 1 2.5)] (+ (.a p) (.b p))))"; + rejects_check "a callee's predicate names the caller's variable with its $" + ~needle:"passes the type variable $t, which nothing here declares ordered?" + "(defn f [s [$t]] () (sort s))"; + accepts "a defonce of a generic struct's copy" + "(defstruct Pair [a $t b $t]) (defonce g (Pair i32)) \ + (defn main [] i32 (.a g))"; (* ── The builtin table against the arms it describes ────────────── [Check.builtins] is what the editor's C-c C-v and M-. read for a name no diff --git a/test/test_session.ml b/test/test_session.ml index 49fc2817..241a5ef7 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -367,6 +367,76 @@ let () = | exception Loc.Error { Loc.dmsg = m; _ } -> fail "the session was poisoned by a bad expression: %s" m); + (* A generic struct's copy first named by an expression typed at the + session: the module built for it has to lay the copy out, and the + session keeps it, as it keeps a generic function's copy. *) + (let gt, _ = Session.create ~file:"programs/reload.flan" () in + (match Session.eval gt "(defstruct Pair [a $t b $t])" with + | _ -> () + | exception Loc.Error { Loc.dmsg = m; _ } -> + fail "a generic struct was refused at the session: %s" m); + match Session.eval_expr gt "(println (.b (Pair 7 8)))" with + | e -> + if not (has e.Session.ir "%\"Pair-i32\" = type") then + fail "the expression's module did not carry the struct copy"; + if not + (List.exists + (fun (s : Tast.structure) -> String.equal s.Tast.sname "Pair-i32") + gt.Session.program.Tast.structs) + then fail "the session did not keep the struct copy an expression made" + | exception Loc.Error { Loc.dmsg = m; _ } -> + fail "an expression building a generic struct was refused: %s" m); + + (* And the same for the other two modules the break loop builds out of + typed-in values: a store into a frame slot, and a restart's arguments. + A copy first named in one of them is laid out there and kept. *) + (let keeps t what = + List.exists + (fun (s : Tast.structure) -> String.equal s.Tast.sname what) + t.Session.program.Tast.structs + in + let lays_out (c : Session.change) what = + has c.Session.ir ("%\"" ^ what ^ "\" = type") + in + let st, _ = Session.create ~file:"programs/reload.flan" () in + (match Session.eval st "(defstruct Pair [a $t b $t])" with + | _ -> () + | exception Loc.Error { Loc.dmsg = m; _ } -> fail "Pair: %s" m); + (match Session.eval st "(defn holder [] i64 (let [x (the i64 0)] x))" with + | _ -> () + | exception Loc.Error { Loc.dmsg = m; _ } -> fail "holder: %s" m); + let fn = + List.find (fun (f : Tast.fn) -> f.Tast.name = "holder") + st.Session.program.Tast.fns + in + let slot = + let r = ref (-1) in + Array.iteri (fun i n -> if n = Some "x" then r := i) fn.Tast.snames; + !r + in + (match + Session.write_slot st ~frame:0 ~fn ~slot ~path:[] + ~edits:[ ([], "(.a (Pair (the i64 5) 6))") ] + with + | Ok (c, _, _) -> + if not (lays_out c "Pair-i64") then + fail "a store's module did not carry the struct copy its value made"; + if not (keeps st "Pair-i64") then + fail "the session did not keep the struct copy a store made" + | Error why -> fail "a store building a generic struct was refused: %s" why + | exception Loc.Error { Loc.dmsg = m; _ } -> + fail "a store building a generic struct was refused: %s" m); + match + Session.arm_restart st ~index:0 ~params:[ Types.Int Types.U16 ] + ~codes:[ "(.b (Pair (the u16 5) 6))" ] + with + | Ok (c, _) -> + if not (lays_out c "Pair-u16") then + fail "a restart's module did not carry the struct copy its argument made" + | Error why -> fail "a restart building a generic struct was refused: %s" why + | exception Loc.Error { Loc.dmsg = m; _ } -> + fail "a restart building a generic struct was refused: %s" m); + (* The other half of "a refusal costs nothing", and the half that used to be missing: a form can check and *then* fail, in the build or at the agent, and the session that already accepted it has no way to hear about it diff --git a/test/test_syntax.ml b/test/test_syntax.ml index ab4fb6a3..6b83e947 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -362,6 +362,8 @@ let () = "(restart-case (f) (continue [] (do)))"; reads "match" "match s\n Circle(r) -> r\n _ ->\n a()\n b()" "(match s (Circle r) r _ (do (a) (b)))"; + reads "match over literals" "match n\n 5 -> a\n -2.5 -> b\n \"go\" -> c\n \\a -> d\n _ -> e" + "(match n 5 a -2.5 b \"go\" c \\a d _ e)"; reads "handler-bind moves the clauses" "handler-bind\n f()\non E(c)\n g(c)" "(handler-bind [(E [c] (g c))] (f))"; reads "quote block" @@ -493,6 +495,22 @@ let () = | [ _; _ ] -> () | _ -> fail "a snippet with leading spaces" | exception e -> fail "a snippet with leading spaces: %s" (diag_text e)); + (* A condition cut out from after [elif ] at column 3: its wrapped line at + column 8 is deeper than the elif, which is what the file says, though not + deeper than the cut. [:indent] says where the statement starts; every + location stays the buffer's own. *) + Source.with_code ~indent:3 ~syntax:Source.Indented ~at:(Some (10, 8)) (fun () -> + (match Source.read_code ~expr:true ~file:"" "x == 0 or\n x == 1" with + | [ f ] -> span_is "a wrapped condition, cut mid-line" f (10, 8, 11, 14) + | _ -> fail "a wrapped condition read as more than one form" + | exception e -> fail "a wrapped condition: %s" (diag_text e)); + match Source.read_code ~expr:true ~file:"" "x == 0 or\n x == 1" with + | _ -> fail "a continuation left of its statement was read" + | exception Loc.Error _ -> ()); + Source.with_code ~syntax:Source.Indented ~at:(Some (10, 8)) (fun () -> + match Source.read_code ~expr:true ~file:"" "x == 0 or\n x == 1" with + | _ -> fail "without :indent, a wrapped line is measured from the cut" + | exception Loc.Error _ -> ()); Source.with_code ~syntax:Source.Paren ~at:(Some (7, 3)) (fun () -> match Source.read_code ~file:"" "(f 1)" with | [ f ] -> span_is "a paren snippet" f (7, 3, 7, 8) diff --git a/web/index.html b/web/index.html index ea18a0de..a8a8a6e8 100644 --- a/web/index.html +++ b/web/index.html @@ -1096,8 +1096,11 @@ as first-even does above.

Option, match and some

(Option T) is how absence is spelled: a lookup miss, an empty -collection, the end of a stream. match works on an Option and -on a defdata, and on nothing else. some unwraps +collection, the end of a stream. match works on an Option, a +defdata and an enum, whose arms name cases; and on a number, a string or a +dyn, whose arms are literals — (match n 0 "zero" -1 "none" _ "some") +— each compared with =, with a _ arm required for the rest. +some unwraps Some and early-returns None from the enclosing function.

(defconst nums [4 i32] [4 8 15 16])