From c9e9d93a919d7561878607380995b7f83eea5dc0 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 15:03:33 +0700 Subject: [PATCH] #_ discards the next form, so commenting one out is not paren counting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clojure's spelling and Clojure's semantics. Repeated — #_#_ a b c — discards that many following forms, and that falls out of the recursion rather than being counted: the discard reads *a form*, and the form it reads may itself begin with a discard, so the outer one throws away what the inner one already stepped past. It belongs to read_form rather than to the sequence readers, which is what makes it work in every position a form can appear — top level, inside a list or a vector or a map, before or after a quote. The two loops that look for a closer or for end of input skip it as well, because a discard is not an element and a file ending in one has read everything there is to read. A trailing #_ with nothing after it is an error, and it is the same error an unterminated form already gives. --- lib/reader.ml | 40 +++++++++++++++++++++++++++++++++++++--- test/test_flan.ml | 29 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/lib/reader.ml b/lib/reader.ml index 803a1b1..7141a99 100644 --- a/lib/reader.ml +++ b/lib/reader.ml @@ -13,6 +13,17 @@ Lisp's, because [,] is already whitespace here (see [is_delimiter]) and every binding vector in the corpus relies on that. + [#_] discards the form after it, as in Clojure: it is read and thrown away, + so commenting out a form does not mean counting its closing parens. Repeated + ([#_#_]) discards that many following forms, which falls out of the + recursion rather than being counted — the discard reads *a form*, and the + form it reads may itself begin with a discard. + + It is a property of [read_form] rather than of the sequence readers, so it + works in every position a form can appear: at the top level, inside a list + or a vector or a map, and after a quote. A trailing [#_] with nothing after + it is the one error, and it is the same error an unterminated form gives. + Not handled yet: metadata ([^:async]). It is rejected rather than read as a symbol, so it cannot rot into a silently-wrong name the way quote would have. *) @@ -156,7 +167,7 @@ let wrap open_c items = | _ -> assert false let rec read_form st = - skip_trivia st; + skip_ignorable st; let loc = here st in match peek st with | '\000' -> Loc.fail loc "unexpected end of input" @@ -177,6 +188,24 @@ let rec read_form st = | ('-' | '+') when is_digit (peek2 st) -> read_number st | _ -> read_symbol_or_keyword st +(* Whitespace, comments, and forms that are read only to be thrown away. + [#_] is handled here rather than in [read_form]'s match so that it is gone + before *anything* looks at what comes next: a discard is not a form, so a + sequence must not count it as an element and a top-level loop must not stop + on it. + + [#_#_ a b c] discards [a] and [b] with no counting. The outer discard reads + one form; that read is [read_form], which sees the inner [#_], discards [a] + and returns [b]; the outer discard then throws [b] away. What is left is + [c]. *) +and skip_ignorable st = + skip_trivia st; + if peek st = '#' && peek2 st = '_' then begin + advance st; advance st; + ignore (read_form st : Form.t); + skip_ignorable st + end + (* One sigil character, then the form it applies to, wrapped in a name. The name's location is the sigil's, so an error inside the wrapper points at the character the reader saw rather than at the form after it. *) @@ -190,7 +219,10 @@ and read_seq st open_c loc = advance st; let want = closer open_c in let rec go acc = - skip_trivia st; + (* [skip_ignorable], not [skip_trivia]: a discard just before the closer — + [(a #_b)] — has to be gone before the closer is looked for, or the + sequence would try to read a form and find [)]. *) + skip_ignorable st; if at_end st then Loc.fail loc "unclosed %C, expected %C" open_c want else @@ -206,7 +238,9 @@ and read_seq st open_c loc = let read_all ~file src = let st = of_string ~file src in let rec go acc = - skip_trivia st; + (* Same reason as in [read_seq]: a file ending in [#_(defn …)] has read + everything there is to read, and must not then be asked for a form. *) + skip_ignorable st; if at_end st then List.rev acc else go (read_form st :: acc) in go [] diff --git a/test/test_flan.ml b/test/test_flan.ml index 191f879..9bf98b2 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -162,6 +162,35 @@ let () = reads "backtick ends a name" "(f a`b)" "(f a (quasiquote b))"; reads "backtick in vec" "[`a ~b]" "[(quasiquote a) (unquote b)]"; + (* ── Discard ───────────────────────────────────────────────────── *) + (* [#_] reads the next form and throws it away, so commenting out a form does + not mean counting its closing parens. Clojure's spelling and Clojure's + semantics, including the repeated form. *) + reads "discard in a call" "(f #_a b)" "(f b)"; + reads "discard the last" "(f a #_b)" "(f a)"; + reads "discard the first" "(#_f g a)" "(g a)"; + reads "discard a list" "(f #_(g x) b)" "(f b)"; + reads "discard in a vector" "[a #_b c]" "[a c]"; + reads "discard in a map" "{:a 1 #_:b #_2 :c 3}" "{:a 1 :c 3}"; + (* Two discards drop two forms, and that is the recursion rather than a + count: the outer discard reads one form, and the form it reads is itself a + discard that returns the one after. *) + reads "two discards" "(f #_#_a b c)" "(f c)"; + reads "three discards" "(f #_#_#_a b c d)" "(f d)"; + (* Every position a form can appear in. *) + reads "discard at top level" "#_(defn a [] 1) (defn b [] 2)" "(defn b [] 2)"; + reads "discard a whole file" "#_(defn a [] 1)" ""; + reads "discard before quote" "(f #_a 'b)" "(f (quote b))"; + reads "discard of a quote" "(f #_'a b)" "(f b)"; + (* Nested, which the recursive read gives for free. *) + reads "discard inside a discarded form" "(f #_(g #_h i) j)" "(f j)"; + (* A name may still contain '#' — it is only a discard at the start of a + form, after trivia. *) + reads "hash inside a name" "(f a#_b)" "(f a#_b)"; + (* Nothing to discard is an error, not a silent nothing. *) + rejects ~needle:"end of input" "discard at end of input" "(f a #_"; + rejects ~needle:"unbalanced" "discard of a closing paren" "(f a #_)"; + (* The whole class: no reader-significant character may end up inside a name. *) let rec bad_names f = let open Form in