bugGNU roff - Bugs: bug #67571, [troff] `class` request works or...

 
 

bug #67571: [troff] `class` request works or not depending on where in the input it's called

Submitter:  Dave <barx>
Submitted:  Fri 03 Oct 2025 06:04:53 AM UTC
   
 
Category:  Core Severity:  3 - Normal
Item Group:  Incorrect behaviour Status:  Confirmed
Privacy:  Public Assigned to:  gbranden
Open/Closed:  Open Planned Release:  None
* Mandatory Fields

Post a Comment

Add a New Comment Rich Markup
   

Discussion

Jump to the original submission

Fri 05 Dec 2025 11:29:00 AM UTC, comment #31: 

comment #27:

> Dave,
>
> You were right and I was wrong.  Now that I found the right place to look, it's clear to me that Werner intended to OR together all character flags that "apply" to a `charinfo` of interest, and the only way that isn't a trivial lookup on a `charinfo`'s `flags` member variable is by searching classes to see if they contain the charinfo of interest (or, more precisely, its Unicode code point).


If I'd looked even more carefully at that commit, like at the part that updated "ChangeLog", I'd have noticed that this wasn't Werner's design at all...


diff --git a/ChangeLog b/ChangeLog
index d59fd991b..e47e1a0b2 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,37 @@
+2010-12-06  Colin Watson  <cjwatson@debian.org>
+           Daiki Ueno  <ueno@unixuser.org>
+
+       Implement support for character classes.
+
+       This patch uses standard C++ headers, contrary to the rest of groff.
+       Ideally, everything in groff should be updated to do the same.
+
+       * src/include/font.h (glyph_to_unicode): New function.
+
+       * src/libs/libgroff/font.cpp (glyph_to_unicode): Implement it.
+       (font::contains, font::get_code): Use it.
+
+       * src/roff/troff/charinfo.h: Include <vector> and <utility>.
+       (charinfo): New members `ranges' and `nested_classes'.
+       New member functions `get_unicode_code' and `get_flags'.
+       New member functions `add_to_class', `is_class', and `contains'.
+       (charinfo::overlaps_horizontally, charinfo::overlaps_vertically,
+       charinfo::can_break_before, charinfo::can_break_after,
+       charinfo::can_break_after, charinfo::ends_sentence,
+       charinfo::transparent,, charinfo:ignore_hcodes): Use `get_flags',
+       which handles character classes also.
+
+       * src/roff/troff/input.cpp (char_class_dictionary): New global
+       variable.
+       (define_class): New function.
+       (init_input_requests): Register `class'.
+
+       (charinfo::get_unicode_code, charinfo::get_flags,
+       charinfo::contains): Implement it.
+
+       * NEWS, doc/groff.texinfo (Character Classes), man/groff_diff.man,
+       man/groff.man: Document it.


G. Branden Robinson <gbranden>
Group administrator
Fri 05 Dec 2025 05:13:24 AM UTC, comment #30: 

At 2025-12-04T15:07:44-0500, Dave wrote:

> Follow-up Comment #29, bug #67571 (group groff):
>
> comment #22:
> > I do want the formatter to diagnose when the input attempts
> > to interpolate a character class in any other context
>
> I'm in favor of that: it's making groff complain when the user attempts
> something that's always been explicitly documented as unsupported.


I just happen to have a patch in my Git stash to do exactly this.  :)


diff --git a/src/roff/troff/env.cpp b/src/roff/troff/env.cpp
index 1f4e3c054..c625abab7 100644
--- a/src/roff/troff/env.cpp
+++ b/src/roff/troff/env.cpp
@@ -1683,6 +1683,8 @@ void margin_character()
       curenv->margin_character_node = 0 /* nullptr */;
     }
   }
+  else if (ci->is_class())
+    error("cannot use %1 as a margin character", tok.description());
   else {
     // Call tok.next() only after making the node so that
     // .mc \s+9\(br\s0 works.
@@ -3856,6 +3858,12 @@ static void add_hyphenation_exceptions()
        skip_line();
        return;
       }
+      if (ci->is_class()) {
+       error("cannot use %1 in a hyphenation exception word",
+            tok.description());
+       skip_line();
+       return;
+      }
       tok.next();
       if (ci->get_ascii_code() == '-') {
        if (i > 0 && (npos == 0 || pos[npos - 1] != i))
diff --git a/src/roff/troff/input.cpp b/src/roff/troff/input.cpp
index b4333a2b6..bf090f05a 100644
--- a/src/roff/troff/input.cpp
+++ b/src/roff/troff/input.cpp
@@ -1673,8 +1673,15 @@ node *do_overstrike() // \o
        delete osnode;
        return 0 /* nullptr */;
       }
+      else if (ci->is_class()) {
+       error("cannot use %1 in overstrike escape sequence",
+             tok.description());
+       delete osnode;
+       return 0 /* nullptr */;
+      }
       else {
        node *n = curenv->make_char_node(ci);
+       // XXX add assertion here
        if (n != 0 /* nullptr */)
          osnode->overstrike(n);
       }
@@ -1732,8 +1739,15 @@ static node *do_bracket() // \b
       delete bracketnode;
       return 0 /* nullptr */;
     }
+    else if (ci->is_class()) {
+      error("cannot use %1 in bracket-building escape sequence",
+           tok.description());
+      delete bracketnode;
+      return 0 /* nullptr */;
+    }
     else {
        node *n = curenv->make_char_node(ci);
+       // XXX add assertion here
        if (n != 0 /* nullptr */)
        bracketnode->bracket(n);
     }
@@ -2633,11 +2647,17 @@ void token::next()
              break;
            }
            node *gn = curenv->make_char_node(ci);
+           // XXX add assertion here
            if (0 /* nullptr */ == gn) {
              assert("make_char_node failed to create a character"
                     " node");
              break;
            }
+           if (ci->is_class()) {
+             error("cannot use %1 as argument to zero-width escape"
+                   " sequence", tok.description());
+             break;
+           }
            nd = new zero_width_node(gn);
            type = TOKEN_NODE;
          }
@@ -5933,6 +5953,12 @@ static bool get_line_arg(units *n, unsigned char si, charinfo **cip)
     if (!(start_token == tok
          && input_stack::get_level() == start_level)) {
       *cip = tok.get_charinfo(true /* required */);
+      // XXX add assertion here
+      if ((*cip != 0 /* nullptr */) && (*cip)->is_class()) {
+       error("cannot use %1 in line-drawing escape sequence",
+            tok.description());
+       return false;
+      }
       tok.next();
     }
     if (!(start_token == tok
@@ -6276,6 +6302,7 @@ void read_title_parts(node **part, hunits *part_width)
   token start(tok);
   int start_level = input_stack::get_level();
   tok.next();
+  // A title has three parts: "'left'center'right'".
   for (int i = 0; i < 3; i++) {
     while (!tok.is_newline() && !tok.is_eof()) {
       if ((tok == start)
@@ -6473,14 +6500,19 @@ static void map_special_character_for_device_output(macro *mac,
 static void encode_special_character_for_device_output(macro *mac)
 {
   const char *sc;
-  if (font::use_charnames_in_special) {
-    charinfo *ci = tok.get_charinfo(true /* required */);
-    if (ci != 0 /* nullptr */)
-       sc = ci->get_symbol()->contents();
-    else
-      assert(0 == "attempted to encode token without charinfo for"
-                 " device extension command output");
+  charinfo *ci = tok.get_charinfo(true /* required */);
+  if (0 /* nullptr */ == ci) {
+    assert(0 == "attempted to encode token without charinfo for"
+               " device extension command output");
+    return;
+  }
+  if (ci->is_class()) {
+    error("cannot encode %1 for device extension command output",
+         tok.description());
+    return;
   }
+  if (font::use_charnames_in_special)
+    sc = ci->get_symbol()->contents();
   else
     sc = tok.get_charinfo(true /* required */)->get_symbol()
       ->contents();
@@ -8865,6 +8897,18 @@ void token::process()
     curenv->space();
     break;
   case TOKEN_SPECIAL_CHAR:
+    {
+      charinfo *ci = get_charinfo();
+      if (0 /* nullptr */ == ci) {
+       assert(0 == "attempted to interpolate special character token"
+             " without charinfo");
+       break;
+      }
+      if (ci->is_class())
+       error("cannot interpolate %1", description());
+      else
+       curenv->add_char(ci);
+    }
     // Optimize `curenv->add_char(get_charinfo())` for token type.
     curenv->add_char(lookup_charinfo(nm));
     break;


G. Branden Robinson <gbranden>
Group administrator
Thu 04 Dec 2025 08:07:41 PM UTC, comment #29: 

comment #22:

> I do want the formatter to diagnose when the input attempts
> to interpolate a character class in any other context


I'm in favor of that: it's making groff complain when the user attempts something that's always been explicitly documented as unsupported.

Dave <barx>
Group Member
Wed 03 Dec 2025 02:38:44 PM UTC, comment #28: 

Expect this in my next push:


$ ./build/test-groff -mja -T utf8
.pchar \C'[CJKnormal]'\C'[CJKprepunct]'\C'[CJKpostpunct]'
character class '[CJKnormal]'
  defined at: file name: "/home/branden/src/GIT/groff/build/../tmac/ja.tmac", line number: 44
  contains ranges: U+3041-U+3096 U+30A0-U+30FF U+4E00-U+9FFF
  contains nested classes: (none)
  flags: 512 (is interword space)
character class '[CJKprepunct]'
  defined at: file name: "/home/branden/src/GIT/groff/build/../tmac/ja.tmac", line number: 32
  contains ranges: U+002C U+003A U+003B U+003E U+007D U+3001 U+3002 U+FF0C U+FF0E U+30FB U+FF1A U+FF1B U+FF1F U+FF01 U+FF09 U+3015 U+FF3D U+FF5D U+300D U+300F U+3011 U+3041 U+3043 U+3045 U+3047 U+3049 U+3063 U+3083 U+3085 U+3087 U+30FC U+30A1 U+30A3 U+30A5 U+30A7 U+30A9 U+30C3 U+30E3 U+30E5 U+30E7
  contains nested classes: (none)
  flags: 128 (prohibits break before)
character class '[CJKpostpunct]'
  defined at: file name: "/home/branden/src/GIT/groff/build/../tmac/ja.tmac", line number: 40
  contains ranges: U+FF08 U+3014 U+FF3B U+FF5B U+300C U+300E U+3010
  contains nested classes: (none)
  flags: 266 (allows break before, overlaps horizontally, prohibits break after)


G. Branden Robinson <gbranden>
Group administrator
Wed 03 Dec 2025 02:12:05 PM UTC, comment #27: 

Dave,

You were right and I was wrong.  Now that I found the right place to look, it's clear to me that Werner intended to OR together all character flags that "apply" to a `charinfo` of interest, and the only way that isn't a trivial lookup on a `charinfo`'s `flags` member variable is by searching classes to see if they contain the charinfo of interest (or, more precisely, its Unicode code point).

'Cause that's exactly what `charinfo::get_flags()` does as Werner originally implemented it.


$ git show 1cb8dd7bde -- src/roff/troff/input.cpp | sed -n '/\/\/ Get the union/,+19p'
+// Get the union of all flags affecting this charinfo.
+unsigned char charinfo::get_flags()
+{
+  unsigned char all_flags = flags;
+  dictionary_iterator iter(char_class_dictionary);
+  charinfo *cp;
+  symbol s;
+  while (iter.get(&s, (void **)&cp)) {
+    assert(!s.is_null());
+    if (cp->contains(get_unicode_code())) {
+#if defined(DEBUGGING)
+      if (debug_state)
+       fprintf(stderr, "charinfo::get_flags %p %s %d\n",
+                       (void *)cp, cp->nm.contents(), cp->flags);
+#endif
+      all_flags |= cp->flags;
+    }
+  }
+  return all_flags;
+}


G. Branden Robinson <gbranden>
Group administrator
Wed 03 Dec 2025 01:52:13 PM UTC, comment #26: 

comment #24:

> Observe that there is no special handling of character classes at all here.  That doesn't mean it doesn't exist, but if it does, it must sit within the `charinfo` class's member function `set_flags()`.


That rules out quickly.  That member function is trivially simple.  (I did this blame from Git HEAD for a reason that should be obvious.)


$ git blame src/roff/troff/charinfo.h | sed -n '/void charinfo::set_flags/,/) }/p'
38e6049d0d src/roff/troff/charinfo.h (Werner LEMBERG      2010-12-18 09:13:18 +0000 241) inline void charinfo::set_flags(unsigned int c)
^351da0dcd troff/charinfo.h          (James Clark         1991-06-02 04:20:34 -0500 242) {
^351da0dcd troff/charinfo.h          (James Clark         1991-06-02 04:20:34 -0500 243)   flags = c;
^351da0dcd troff/charinfo.h          (James Clark         1991-06-02 04:20:34 -0500 244) }


G. Branden Robinson <gbranden>
Group administrator
Wed 03 Dec 2025 01:47:09 PM UTC, comment #25: 

s/little /un/

G. Branden Robinson <gbranden>
Group administrator
Wed 03 Dec 2025 01:46:42 PM UTC, comment #24: 

More source diving.  As of groff 1.22.3, the function implementing the innards of the `cflags` request was little changed from the James Clark era.


$ git blame 1.22.3 -- src/roff/troff/input.cpp | sed -n '/void char_flags/,/) }/p'
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6954) void char_flags()
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6955) {
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6956)   int flags;
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6957)   if (get_integer(&flags))
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6958)     while (has_arg()) {
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6959)       charinfo *ci = tok.get_char(1);
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6960)       if (ci) {
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6961)     charinfo *tem = ci->get_translation();
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6962)     if (tem)
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6963)       ci = tem;
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6964)     ci->set_flags(flags);
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6965)       }
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6966)       tok.next();
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6967)     }
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6968)   skip_line();
^351da0dcd troff/input.c            (James Clark    1991-06-02 04:20:34 -0500 6969) }


Observe that there is no special handling of character classes at all here.  That doesn't mean it doesn't exist, but if it does, it must sit within the `charinfo` class's member function `set_flags()`.

G. Branden Robinson <gbranden>
Group administrator
Tue 02 Dec 2025 09:59:54 AM UTC, comment #23: 

original submission:

> I get the above results whether I use groff 1.22.4 or 1.23.0.  (The other historical groff I have at hand, 1.19.2, had not yet implemented the .class request.)


I can reproduce it with groff 1.22.3.

The `define_class()` function seems to have come in in December 2010 and soldified almost instantly.  I see only 3 commits implicated, all within a week.


$ git blame 1.22.3 -- src/roff/troff/input.cpp | sed -n '/void define_class/,/) }/p'
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7028) void define_class()
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7029) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7030)   tok.skip();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7031)   symbol nm = get_name(1);
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7032)   if (nm.is_null()) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7033)     skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7034)     return;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7035)   }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7036)   charinfo *ci = get_charinfo(nm);
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7037)   charinfo *child1 = 0, *child2 = 0;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7038)   while (!tok.newline() && !tok.eof()) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7039)     tok.skip();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7040)     if (child1 != 0 && tok.ch() == '-') {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7041)       tok.next();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7042)       child2 = tok.get_char(1);
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7043)       if (!child2) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7044)     warning(WARN_MISSING,
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7045)             "missing end of character range in class `%1'",
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7046)             nm.contents());
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7047)     skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7048)     return;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7049)       }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7050)       if (child1->is_class() || child2->is_class()) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7051)     warning(WARN_SYNTAX,
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7052)             "nested character class is not allowed in range definition");
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7053)     skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7054)     return;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7055)       }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7056)       int u1 = child1->get_unicode_code();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7057)       int u2 = child2->get_unicode_code();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7058)       if (u1 < 0) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7059)     warning(WARN_SYNTAX,
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7060)             "invalid start value in character range");
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7061)     skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7062)     return;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7063)       }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7064)       if (u2 < 0) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7065)     warning(WARN_SYNTAX,
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7066)             "invalid end value in character range");
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7067)     skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7068)     return;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7069)       }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7070)       ci->add_to_class(u1, u2);
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7071)       child1 = child2 = 0;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7072)     }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7073)     else if (child1 != 0) {
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7074)       if (child1->is_class()) {
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7075)     if (ci == child1) {
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7076)       warning(WARN_SYNTAX, "invalid cyclic class nesting");
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7077)       skip_line();
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7078)       return;
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7079)     }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7080)     ci->add_to_class(child1);
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7081)       }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7082)       else {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7083)     int u1 = child1->get_unicode_code();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7084)     if (u1 < 0) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7085)       warning(WARN_SYNTAX,
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7086)               "invalid character value in class `%1'",
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7087)               nm.contents());
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7088)       skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7089)       return;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7090)     }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7091)     ci->add_to_class(u1);
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7092)       }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7093)       child1 = 0;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7094)     }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7095)     child1 = tok.get_char(1);
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7096)     tok.next();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7097)     if (!child1) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7098)       if (!tok.newline())
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7099)     skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7100)       break;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7101)     }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7102)   }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7103)   if (child1 != 0) {
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7104)     if (child1->is_class()) {
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7105)       if (ci == child1) {
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7106)     warning(WARN_SYNTAX, "invalid cyclic class nesting");
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7107)     skip_line();
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7108)     return;
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7109)       }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7110)       ci->add_to_class(child1);
9a08b71d07 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-19 20:47:38 +0000 7111)     }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7112)     else {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7113)       int u1 = child1->get_unicode_code();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7114)       if (u1 < 0) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7115)     warning(WARN_SYNTAX,
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7116)             "invalid character value in class `%1'",
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7117)             nm.contents());
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7118)     skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7119)     return;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7120)       }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7121)       ci->add_to_class(u1);
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7122)     }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7123)     child1 = 0;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7124)   }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7125)   if (!ci->is_class()) {
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7126)     warning(WARN_SYNTAX,
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7127)         "empty class definition for `%1'",
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7128)         nm.contents());
a1954c5a05 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 17:36:55 +0000 7129)     skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7130)     return;
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7131)   }
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7132)   (void)char_class_dictionary.lookup(nm, ci);
a1954c5a05 src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 17:36:55 +0000 7133)   skip_line();
1cb8dd7bde src/roff/troff/input.cpp (Werner LEMBERG 2010-12-13 15:30:20 +0000 7134) }


I expect that to resolve our disagreement over character flag and character class interactions, we're going to both need to look carefully at the foregoing.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 10:41:15 PM UTC, comment #22: 

At 2025-11-17T17:33:08-0500, Dave wrote:

> Follow-up Comment #21, bug #67571 (group groff):
> comment #17:
> > But the `class` request itself must be able to as well, or
> > there's no point having any code referring to nested classes.
>
> That's a good point.  I suspect this is a bit of imprecision on
> Werner's part.  (I admire the quality of his documentation, but it
> does sometimes lack attention to detail.)  He explicitly says "classes
> can be nested" later in the section, so I think the intent of the
> earlier sentence is that only .cflags, besides .class itself, can use
> a class.


That's not something I'm looking to disrupt, but I do want the formatter
to diagnose when the input attempts to interpolate a character class in
any other context (meaning, one where doing so "doesn't make sense").

> > Or maybe nested classes don't actually work, and we've been
> > carrying dead weight for 15 years.
>
> I've never tried it so I couldn't say.


Me neither.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 10:33:05 PM UTC, comment #21: 

comment #17:

> But the `class` request itself must be able to as well, or
> there's no point having any code referring to nested classes.


That's a good point.  I suspect this is a bit of imprecision on Werner's part.  (I admire the quality of his documentation, but it does sometimes lack attention to detail.)  He explicitly says "classes can be nested" later in the section, so I think the intent of the earlier sentence is that only .cflags, besides .class itself, can use a class.

> Or maybe nested classes don't actually work, and we've been
> carrying dead weight for 15 years.


I've never tried it so I couldn't say.

Dave <barx>
Group Member
Mon 17 Nov 2025 09:57:50 PM UTC, comment #20: 

Looks like I get to pivot to a *third* position.

Here's a summary of my working copy at a point of bisection.  I'll explain why I'm bisecting in a moment.


$ git log --oneline --reverse origin..HEAD
632b9d7b6 ChangeLog: Fix component annotations.
75f3d1db3 [doc,man]: Clarify "Other differences" discussion.
f290bb60d src/roff/troff/input.cpp: Fix grammar in comment.
b111f6f08 src/libs/libgroff/glyphuni.cpp: Correct comment.
3c770e337 [troff]: Add `.class` read-only Boolean register.
9f5e58272 [troff]: Track character class defn locations.
6ed81442c (refs/bisect/good-6ed81442c60835e544de183cee202435046bd17b) [troff]: Describe character classes better (1/2).
71b13f801 [troff]: Describe character classes better (2/2).
2ddfba4cb (refs/bisect/good-2ddfba4cb71b4fb749f871c101aa1c20d8edd21c) [groff]: Regression-test Savannah #67571.
05eae8986 (HEAD, refs/bisect/bad) REWRITE src/roff/troff/input.cpp: Annotate mystery code.


The changes to the formatter between HEAD on Savannah up to 2ddfb are mostly not material, but here they are.


$ git diff origin 2ddfb -- src/roff/troff/ | cat
diff --git a/src/roff/troff/input.cpp b/src/roff/troff/input.cpp
index 339695d2a..b1b236e92 100644
--- a/src/roff/troff/input.cpp
+++ b/src/roff/troff/input.cpp
@@ -2907,21 +2907,26 @@ const char *token::description()
   case TOKEN_SPACE:
     return "a space";
   case TOKEN_SPECIAL_CHAR:
-    // We normally using apostrophes for quotation in diagnostic
-    // messages, but many special character names contain them.  Fall
-    // back to double quotes if this one does.  A user-defined special
-    // character name could contain both characters; we expect such
-    // users to lie comfortably in the bed they made for themselves.
+    // We normally use apostrophes for quotation in diagnostic messages,
+    // but many special character names contain them.  Fall back to
+    // double quotes if this one does.  A user-defined special character
+    // name could contain both characters; we expect such users to lie
+    // comfortably in the bed they made for themselves.
     {
       const char *sc = nm.contents();
       char qc = '\'';
       if (strchr(sc, '\'') != 0 /* nullptr */)
        qc = '"';
       // TODO: This truncates the names of impractically long special
-      // character names.  Do something about that.  (The truncation is
-      // visually indicated by the absence of a closing quotation mark.)
-      (void) snprintf(buf, maxstr, "special character %c%s%c", qc, sc,
-                     qc);
+      // character or character class names.  Do something about that.
+      // (The truncation is visually indicated by the absence of a
+      // closing quotation mark.)
+      if (using_character_classes && tok.get_char()->is_class())
+       (void) snprintf(buf, maxstr, "character class %c%s%c", qc, sc,
+                       qc);
+      else
+       (void) snprintf(buf, maxstr, "special character %c%s%c", qc, sc,
+                       qc);
       return buf;
     }
   case TOKEN_SPREAD:
@@ -3922,6 +3927,14 @@ void macro::print_size()
   errprint("%1", len);
 }

+// Use this only for zero-length macros associated with charinfo objects
+// that are character classes.
+void macro::dump()
+{
+  if (filename != 0 /* nullptr */)
+    errprint("file name: \"%1\", line number: %2\n", filename, lineno);
+}
+
 void macro::json_dump()
 {
   bool need_comma = false;
@@ -4987,8 +5000,8 @@ static void print_character_request()
     ci = tok.get_char(false /* required */,
                      true /* suppress creation */);
     if (!tok.is_character()) {
-      error("character report request expects characters as arguments;"
-           " got %1", tok.description());
+      error("character report request expects characters or character"
+           " classes as arguments; got %1", tok.description());
       break;
     }
     if (0 /* nullptr */ == ci) {
@@ -8522,6 +8535,10 @@ void define_class()
     return;
   }
   charinfo *ci = get_charinfo(nm);
+  // Assign the charinfo an empty macro as a hack to record the
+  // file:line location of its definition.
+  macro *m = new macro;
+  (void) ci->set_macro(m);
   charinfo *child1 = 0 /* nullptr */, *child2 = 0 /* nullptr */;
   while (!tok.is_newline() && !tok.is_eof()) {
     tok.skip();
@@ -8925,6 +8942,16 @@ const char *break_flag_reg::get_string()
   return i_to_a(input_stack::get_break_flag());
 }

+class character_classes_in_use_reg : public reg {
+public:
+  const char *get_string();
+};
+
+const char *character_classes_in_use_reg::get_string()
+{
+  return i_to_a(using_character_classes);
+}
+
 class enclosing_want_att_compat_reg : public reg {
 public:
   const char *get_string();
@@ -9957,6 +9984,7 @@ void init_input_requests()
   register_dictionary.define(".$", new nargs_reg);
   register_dictionary.define(".br", new break_flag_reg);
   register_dictionary.define(".C", new readonly_boolean_register(&want_att_compat));
+  register_dictionary.define(".class", new character_classes_in_use_reg);
   register_dictionary.define(".cp", new enclosing_want_att_compat_reg);
   register_dictionary.define(".O", new variable_reg(&suppression_level));
   register_dictionary.define(".c", new lineno_reg);
@@ -10700,92 +10728,139 @@ bool charinfo::contains(charinfo *, bool)

 void charinfo::dump()
 {
-  if (translation != 0 /* nullptr */)
-    errprint("  is translated\n");
-  else
-    errprint("  is not translated\n");
-  if (mac != 0 /* nullptr */) {
-    errprint("  has a macro: ");
-    mac->json_dump();
+  if (is_class()) {
+    std::vector<std::pair<int, int> >::const_iterator ranges_iter;
+    ranges_iter = ranges.begin();
+    assert(mac != 0 /* nullptr */);
+    errprint("  defined at: ");
+    mac->dump();
+    fflush(stderr);
+    errprint("  contains ranges: ");
+    const size_t buflen = 8; // "U+" + four/five hex digits + '\0'
+    int range_begin = 0;
+    int range_end = 0;
+    char beg_hexbuf[buflen];
+    char end_hexbuf[buflen];
+    (void) memset(beg_hexbuf, '\0', buflen);
+    (void) memset(end_hexbuf, '\0', buflen);
+    bool has_ranges = false;
+    while (ranges_iter != ranges.end()) {
+      has_ranges = true;
+      range_begin = ranges_iter->first;
+      range_end = ranges_iter->second;
+      (void) snprintf(beg_hexbuf, buflen, "U+%.4X", range_begin);
+      (void) snprintf(end_hexbuf, buflen, "U+%.4X", range_end);
+      // TODO: comma-separate?  JSON list?
+      if (range_begin == range_end)
+       errprint("%1 ", beg_hexbuf, end_hexbuf);
+      else
+       errprint("%1-%2 ", beg_hexbuf, end_hexbuf);
+      ++ranges_iter;
+    }
+    if (!has_ranges)
+      errprint("(none)");
+    errprint("\n");
+    errprint("  contains nested classes: ");
+    std::vector<charinfo *>::const_iterator nested_iter;
+    nested_iter = nested_classes.begin();
+    bool has_nested_classes = false;
+    while (nested_iter != nested_classes.end()) {
+      has_nested_classes = true;
+      // TODO: Here's where JSON would really pay off.
+      (*nested_iter)->dump();
+    }
+    if (!has_nested_classes)
+      errprint("(none)");
     errprint("\n");
   }
-  else
-    errprint("  does not have a macro\n");
-  errprint("  special translation: %1\n",
-          static_cast<int>(special_translation));
-  errprint("  hyphenation code: %1\n",
-          static_cast<int>(hyphenation_code));
-  errprint("  flags: %1 (", flags);
-  if (0U == flags)
-    errprint("none)\n");
   else {
-    char none[] = { '\0' };
-    char comma[] = { ',', ' ', '\0' };
-    char *separator = none;
-    if (flags & ENDS_SENTENCE) {
-      errprint("%1ends sentence", separator);
-      separator = comma;
-    }
-    if (flags & ALLOWS_BREAK_BEFORE) {
-      errprint("%1allows break before", separator);
-      separator = comma;
-    }
-    if (flags & ALLOWS_BREAK_AFTER) {
-      errprint("%1allows break after", separator);
-      separator = comma;
-    }
-    if (flags & OVERLAPS_HORIZONTALLY) {
-      errprint("%1overlaps horizontally", separator);
-      separator = comma;
-    }
-    if (flags & OVERLAPS_VERTICALLY) {
-      errprint("%1overlaps vertically", separator);
-      separator = comma;
-    }
-    if (flags & IS_TRANSPARENT_TO_END_OF_SENTENCE) {
-      errprint("%1is transparent to end of sentence", separator);
-      separator = comma;
-    }
-    if (flags & IGNORES_SURROUNDING_HYPHENATION_CODES) {
-      errprint("%1ignores surrounding hyphenation codes", separator);
-      separator = comma;
-    }
-    if (flags & PROHIBITS_BREAK_BEFORE) {
-      errprint("%1prohibits break before", separator);
-      separator = comma;
-    }
-    if (flags & PROHIBITS_BREAK_AFTER) {
-      errprint("%1prohibits break after", separator);
-      separator = comma;
-    }
-    if (flags & IS_INTERWORD_SPACE) {
-      errprint("%1is interword space", separator);
-      separator = comma;
-    }
-    errprint(")\n");
-  }
-  errprint("  asciify code: %1\n", static_cast<int>(asciify_code));
-  errprint("  ASCII code: %1\n", static_cast<int>(ascii_code));
-  // Also see node.cpp::glyph_node::asciify().
-  int mapping = get_unicode_mapping();
-  if (mapping >= 0) {
-    const size_t buflen = 6; // enough for five hex digits + '\0'
-    char hexbuf[buflen];
-    (void) memset(hexbuf, '\0', buflen);
-    (void) snprintf(hexbuf, buflen, "%.4X", mapping);
-    errprint("  Unicode mapping: U+%1\n", hexbuf);
+    if (translation != 0 /* nullptr */)
+      errprint("  is translated\n");
+    else
+      errprint("  is not translated\n");
+    if (mac != 0 /* nullptr */) {
+      errprint("  has a macro: ");
+      mac->json_dump();
+      errprint("\n");
+    }
+    else
+      errprint("  does not have a macro\n");
+    errprint("  special translation: %1\n",
+            static_cast<int>(special_translation));
+    errprint("  hyphenation code: %1\n",
+            static_cast<int>(hyphenation_code));
+    errprint("  flags: %1 (", flags);
+    if (0U == flags)
+      errprint("none)\n");
+    else {
+      char none[] = { '\0' };
+      char comma[] = { ',', ' ', '\0' };
+      char *separator = none;
+      if (flags & ENDS_SENTENCE) {
+       errprint("%1ends sentence", separator);
+       separator = comma;
+      }
+      if (flags & ALLOWS_BREAK_BEFORE) {
+       errprint("%1allows break before", separator);
+       separator = comma;
+      }
+      if (flags & ALLOWS_BREAK_AFTER) {
+       errprint("%1allows break after", separator);
+       separator = comma;
+      }
+      if (flags & OVERLAPS_HORIZONTALLY) {
+       errprint("%1overlaps horizontally", separator);
+       separator = comma;
+      }
+      if (flags & OVERLAPS_VERTICALLY) {
+       errprint("%1overlaps vertically", separator);
+       separator = comma;
+      }
+      if (flags & IS_TRANSPARENT_TO_END_OF_SENTENCE) {
+       errprint("%1is transparent to end of sentence", separator);
+       separator = comma;
+      }
+      if (flags & IGNORES_SURROUNDING_HYPHENATION_CODES) {
+       errprint("%1ignores surrounding hyphenation codes", separator);
+       separator = comma;
+      }
+      if (flags & PROHIBITS_BREAK_BEFORE) {
+       errprint("%1prohibits break before", separator);
+       separator = comma;
+      }
+      if (flags & PROHIBITS_BREAK_AFTER) {
+       errprint("%1prohibits break after", separator);
+       separator = comma;
+      }
+      if (flags & IS_INTERWORD_SPACE) {
+       errprint("%1is interword space", separator);
+       separator = comma;
+      }
+      errprint(")\n");
+    }
+    errprint("  asciify code: %1\n", static_cast<int>(asciify_code));
+    errprint("  ASCII code: %1\n", static_cast<int>(ascii_code));
+    // Also see node.cpp::glyph_node::asciify().
+    int mapping = get_unicode_mapping();
+    if (mapping >= 0) {
+      const size_t buflen = 6; // enough for five hex digits + '\0'
+      char hexbuf[buflen];
+      (void) memset(hexbuf, '\0', buflen);
+      (void) snprintf(hexbuf, buflen, "%.4X", mapping);
+      errprint("  Unicode mapping: U+%1\n", hexbuf);
+    }
+    else
+      errprint("  Unicode mapping: none (%1)\n", mapping);
+    errprint("  is%1 found\n", is_not_found ? " not" : "");
+    errprint("  is%1 transparently translatable\n",
+            is_transparently_translatable ? "" : " not");
+    errprint("  is%1 translatable as input\n",
+            translatable_as_input ? "" : " not");
+    const char *modestr = character_mode_description(mode);
+    if (strcmp(modestr, "") == 0)
+      modestr =" normal";
+    errprint("  mode:%1\n", modestr);
   }
-  else
-    errprint("  Unicode mapping: none (%1)\n", mapping);
-  errprint("  is%1 found\n", is_not_found ? " not" : "");
-  errprint("  is%1 transparently translatable\n",
-          is_transparently_translatable ? "" : " not");
-  errprint("  is%1 translatable as input\n",
-          translatable_as_input ? "" : " not");
-  const char *modestr = character_mode_description(mode);
-  if (strcmp(modestr, "") == 0)
-    modestr =" normal";
-  errprint("  mode:%1\n", modestr);
   fflush(stderr);
 }

diff --git a/src/roff/troff/request.h b/src/roff/troff/request.h
index ce9967cf2..dd7f92428 100644
--- a/src/roff/troff/request.h
+++ b/src/roff/troff/request.h
@@ -70,6 +70,7 @@ public:
   bool is_diversion();
   bool is_string();
   void clear_string_flag();
+  void dump();
   void json_dump();
   friend class string_iterator;
   friend void chop_macro();


This looks like *way* more churn than it really is because of the changes to dumping, so that `pchar` on a character class looks different from `pchar` on a special character.

(I now think I should probably revert my addition of a `.class` register.)

But here's what I'm writing about.  That last change, the one marked "REWRITE", is a doozy.  It had been just a comment, which I pasted into one of the recent highly active Savannah tickets.  But I changed it to do this:


$ git show
commit 05eae8986ff3800b2616c62e9b617664914c76b9 (HEAD, refs/bisect/bad)
Author: G. Branden Robinson <g.branden.robinson@gmail.com>
Date:   Fri Nov 14 16:56:55 2025 -0600

    REWRITE src/roff/troff/input.cpp: Annotate mystery code.

diff --git a/src/roff/troff/charinfo.h b/src/roff/troff/charinfo.h
index 797a7f5f2..0ad68f70c 100644
--- a/src/roff/troff/charinfo.h
+++ b/src/roff/troff/charinfo.h
@@ -285,7 +285,6 @@ inline symbol *charinfo::get_symbol()

 inline void charinfo::add_to_class(int c)
 {
-  using_character_classes = true;
   // TODO ranges cumbersome for single characters?
   ranges.push_back(std::pair<int, int>(c, c));
 }
@@ -293,13 +292,11 @@ inline void charinfo::add_to_class(int c)
 inline void charinfo::add_to_class(int lo,
                                   int hi)
 {
-  using_character_classes = true;
   ranges.push_back(std::pair<int, int>(lo, hi));
 }

 inline void charinfo::add_to_class(charinfo *ci)
 {
-  using_character_classes = true;
   nested_classes.push_back(ci);
 }

diff --git a/src/roff/troff/input.cpp b/src/roff/troff/input.cpp
index b1b236e92..f79325fb9 100644
--- a/src/roff/troff/input.cpp
+++ b/src/roff/troff/input.cpp
@@ -8635,6 +8635,7 @@ void define_class()
     skip_line();
     return;
   }
+  using_character_classes = true;
   (void) char_class_dictionary.lookup(nm, ci);
   skip_line();
 }
@@ -10596,7 +10597,6 @@ void get_flags()
     assert(!s.is_null());
     ci->get_flags();
   }
-  using_character_classes = false;
 }

 // Get the union of all flags affecting this charinfo.


This change shot performance right to hell: normally, using all the cores on my dev box, I can run the entire test suite in 8 to 9 seconds.

With just this patch above, the tests take more like 38 seconds.

Deri would kill me.

I still think getting rid of this global bit of parser state is the right idea, because it's bound up with the issue of this report: "`class` request works or not depending on where in the input it's called", which I could re-express as "character class interpolations should work the same regardless of where in the input they occur".

I already have a patch in development to make the formatter complain anytime a character class gets used where a simple special character is expected.

Observe:


$ git stash show -p 0 | cat
diff --git a/src/roff/troff/env.cpp b/src/roff/troff/env.cpp
index b1e1fb321..e4ce56c4a 100644
--- a/src/roff/troff/env.cpp
+++ b/src/roff/troff/env.cpp
@@ -1676,6 +1676,11 @@ void margin_character()
   while (tok.is_space())
     tok.next();
   charinfo *ci = tok.read_troff_character();
+  if (using_character_classes && ci->is_class()) {
+    error("cannot use %1 as a margin character", tok.description());
+    skip_line();
+    return;
+  }
   if (ci != 0 /* nullptr */) {
     // Call tok.next() only after making the node so that
     // .mc \s+9\(br\s0 works.
@@ -3849,6 +3854,12 @@ static void add_hyphenation_exceptions()
     while (i < WORD_MAX && !tok.is_space() && !tok.is_newline()
           && !tok.is_eof()) {
       charinfo *ci = tok.read_troff_character(true /* required */);
+      if (using_character_classes && ci->is_class()) {
+       error("cannot use %1 in a hyphenation exception word",
+             tok.description());
+       skip_line();
+       return;
+      }
       if (0 /* nullptr */ == ci) {
        error("%1 has no associated character information(!)",
              tok.description());
diff --git a/src/roff/troff/input.cpp b/src/roff/troff/input.cpp
index 15d0a8495..166cf1752 100644
--- a/src/roff/troff/input.cpp
+++ b/src/roff/troff/input.cpp
@@ -1667,6 +1667,12 @@ node *do_overstrike() // \o
     }
     else {
       charinfo *ci = tok.read_troff_character(true /* required */);
+      if (using_character_classes && ci->is_class()) {
+       error("cannot use %1 in overstrike escape sequence",
+             tok.description());
+       delete osnode;
+       return 0 /* nullptr */;
+      }
       if (ci != 0 /* nullptr */) {
        node *n = curenv->make_char_node(ci);
        if (n != 0 /* nullptr */)
@@ -1720,6 +1726,12 @@ static node *do_bracket() // \b
        && (want_att_compat || input_stack::get_level() == start_level))
       break;
     charinfo *ci = tok.read_troff_character(true /* required */);
+    if (using_character_classes && ci->is_class()) {
+      error("cannot use %1 in bracket-building escape sequence",
+           tok.description());
+      delete bracketnode;
+      return 0 /* nullptr */;
+    }
     if (ci != 0 /* nullptr */) {
       node *n = curenv->make_char_node(ci);
       if (n != 0 /* nullptr */)
@@ -2616,6 +2628,11 @@ void token::next()
            nd = new zero_width_node(nd);
          else {
            charinfo *ci = read_troff_character(true /* required */);
+           if (using_character_classes && ci->is_class()) {
+             error("cannot use %1 as argument to zero-width escape"
+                   " sequence", tok.description());
+             return;
+           }
            if (0 /* nullptr */ == ci)
              break;
            node *gn = curenv->make_char_node(ci);
@@ -2922,13 +2939,15 @@ const char *token::description()
       // character or character class names.  Do something about that.
       // (The truncation is visually indicated by the absence of a
       // closing quotation mark.)
-      if (using_character_classes
-         && tok.read_troff_character()->is_class())
-       (void) snprintf(buf, maxstr, "character class %c%s%c", qc, sc,
-                       qc);
-      else
-       (void) snprintf(buf, maxstr, "special character %c%s%c", qc, sc,
-                       qc);
+      static const char special_character[] = "special character";
+      static const char character_class[] = "character class";
+      const char *type = special_character;
+      if (using_character_classes) {
+       charinfo *ci = get_charinfo(nm);
+        if ((ci != 0 /* nullptr */) && ci->is_class())
+         type = character_class;
+      }
+      (void) snprintf(buf, maxstr, "%s %c%s%c", type, qc, sc, qc);
       return buf;
     }
   case TOKEN_SPREAD:
@@ -3557,12 +3576,14 @@ void process_input_stack()
                tok.description());
       else {
        reading_beginning_of_input_line = false;
+       debug("GBR1: calling process() on %1", tok.description());
        tok.process();
       }
       break;
     default:
       {
        reading_beginning_of_input_line = false;
+       debug("GBR2: calling process() on %1", tok.description());
        tok.process();
        break;
       }
@@ -4911,6 +4932,7 @@ void define_character(char_mode mode, const char *font_name)
     skip_line();
     return;
   }
+  // TODO: If `ci` is already a character class, clobber it.
   if (font_name != 0 /* nullptr */) {
     string s(font_name);
     s += ' ';
@@ -5042,6 +5064,7 @@ static void remove_character()
        charinfo *ci = tok.read_troff_character(true /* required */,
                                                true /* suppress
                                                        creation */);
+       // TODO: If `ci` is a character class, clobber it.
        if (0 /* nullptr */ == ci) {
          if (!tok.is_indexed_character())
            warning(WARN_CHAR, "%1 is not defined", tok.description());
@@ -5919,6 +5942,11 @@ static bool get_line_arg(units *n, unsigned char si, charinfo **cip)
     if (!(start_token == tok
          && input_stack::get_level() == start_level)) {
       *cip = tok.read_troff_character(true /* required */);
+      if (using_character_classes && (*cip)->is_class()) {
+       error("cannot use %1 in line-drawing escape sequence",
+             tok.description());
+       return false;
+      }
       tok.next();
     }
     if (!(start_token == tok
@@ -6229,6 +6257,7 @@ static void do_width() // \w
     if (tok == start_token
        && (want_att_compat || input_stack::get_level() == start_level))
       break;
+    debug("GBR3: calling process() on %1", tok.description());
     tok.process();
   }
   env.wrap_up_tab();
@@ -6274,8 +6303,10 @@ void read_title_parts(node **part, hunits *part_width)
       if ((page_character != 0 /* nullptr */)
          && (tok.read_troff_character() == page_character))
        interpolate_register(percent_symbol, 0);
-      else
+      else {
+       debug("GBR4: calling process() on %1", tok.description());
        tok.process();
+      }
       tok.next();
     }
     curenv->wrap_up_tab();
@@ -6909,6 +6940,7 @@ static bool are_comparands_equal()
          && (want_att_compat
              || input_stack::get_level() == delim_level))
         break;
+      debug("GBR5: calling process() on %1", tok.description());
       tok.process();
     }
     curenv = &env2;
@@ -8639,6 +8671,7 @@ void define_class()
     skip_line();
     return;
   }
+  debug("GBR: now using character classes");
   using_character_classes = true;
   (void) char_class_dictionary.lookup(nm, ci);
   skip_line();
@@ -8839,7 +8872,19 @@ void token::process()
     curenv->space();
     break;
   case TOKEN_SPECIAL_CHAR:
-    curenv->add_char(get_charinfo(nm));
+    {
+      charinfo *ci = get_charinfo(nm);
+      if (!using_character_classes) {
+       debug("GBR: token:process(): not using character classes");
+       curenv->add_char(get_charinfo(nm));
+      }
+      else if ((ci != 0 /* nullptr */) && !ci->is_class()) {
+       debug("GBR: token:process(): using character classes, but special character is not a character class");
+       curenv->add_char(get_charinfo(nm));
+      }
+      else
+       error("cannot interpolate %1", description());
+    }
     break;
   case TOKEN_SPREAD:
     curenv->spread();
@@ -10081,6 +10126,7 @@ node *charinfo_to_node_list(charinfo *ci, const environment *envp)
       break;
     }
     else
+      debug("GBR6: calling process() on %1", tok.description());
       tok.process();
   }
   node *n = curenv->extract_output_line();


This work was only about 50% done.

I think I need to finish up that work and see if the addition of these "ci->is_class()" guards restores acceptable performance.  I suspect what's happening is that character classes are getting traversed into and searched when they need not be.  The order in which parallelized tests eventually finish supports this hypothesis.


PASS: src/utils/grog/tests/smoke-test.sh
PASS: src/roff/groff/tests/do-not-loop-infinitely-when-breaking-cjk.sh
PASS: src/roff/groff/tests/check-delimiter-validity.sh
PASS: contrib/hdtbl/examples/test-hdtbl.sh
PASS: tmac/tests/localization-works.sh


These guys reliably trail the pack as of this commit.  "check-delimiter-validity.sh" is expected to be slow, because it launches the formatter dozens or hundreds of times in an even now not quite exhaustive exploration of the available escape sequence delimiter space.  grog's smoke test has always been slow because it's a lot of Perl wrapped around running groff several times.

But the other 3 all hit the CJK macro files.  Two for, I suspect, obvious reasons, and "test-hdtbl.sh" because it prints (empty) code charts for the abstract CJK faces contributed by TANAKA Takuji.  So all 3 of them definitely exercise code paths involving the character class dictionary.

So, bottom line, "using_character_classes" is working both as a performance optimizer *and* as a parser-state-tweaking bit.

I don't know if the former was intended, but I now think I can scotch the variable and simplify these pending


if (using_character_classes && ci->is_class()) {


tests to just


if (ci->is_class()) {


...and if that's still crappily slow, I can make `charinfo::is_class` either ask the `char_class_dictionary` object if it's populated, or "memoize" (I've always hated that word, though not the technique) the fact thereof.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 08:58:02 PM UTC, comment #19: 

It's worth noting that I now think the quoted code in comment #18 is not correct.

`using_character_classes` is not, I now think, intended as an optimization of any kind, but rather to put the parser into a special state such that special character identifiers are looked up first in `char_class_dictionary`.

See bug #67711.

I furthermore acknowledge that my comments on several character class-related are coming thick and fast and I'm venturing and withdrawing hypotheses at a rapid rate.

I won't apologize for that, but offer it as an object lesson: this is why we (should) write comprehensive documentation and unit tests for features when implementing them.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 08:51:12 PM UTC, comment #18: 

comment #15:

> Aha!  This appears to be the same as issue as one I was already on the scent of.


No, I'm wrong.  Instead it appears to be truly the same issue as #67570 after all.

I remain dubious of this global `using_character_classes` Boolean as noted in the regression test I *was* writing for this ticket, though.


# Regression-test Savannah #67571.  The formatter implements an internal
# optimization, using a global Boolean variable to track whether
# character classes are "in use", presumably because it was thought
# cheaper to do a comparison on that scalar value than to see if the
# dictionary of character class names is empty.  (That decision makes
# more sense when you realize that the mechanisms for removing special
# characters and character classes are incomplete; see Savannah #67703.)
# Unfortunately manual maintenance means that Boolean variable's state
# can get deranged, and it did, when looking up character flags in some
# circumstances.


G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 08:48:08 PM UTC, comment #17: 

At 2025-11-17T15:05:33-0500, Dave wrote:

> Follow-up Comment #16, bug #67571 (group groff):
>
> comment #15:
> > commit 7e9e9260244ed1a551ea9663124841ac7861b97d
>
> (For the record, this commit has not been pushed.)


Agreed and acknowledged.

> > an attempt to interpolate a character class as a "title part"
> > results in undesired code flow.
>
> As this is documented as unsupported usage, undefined behavior seems a
> reasonable result.


Yes, but I don't like it, especially when it's not diagnosed.  I'm work
up a commit to do precisely that.

In fact I suppose that's what this global Boolean
`using_character_classes` is actually for--to kick the parser into a
special state so that it interprets special character tokens differently
just for this one request.

I strongly dislike that from a design perspective.

> "Currently, only the 'cflags' request can handle references to
> character classes," says the manual.


But the `class` request itself must be able to as well, or there's no
point having any code referring to nested classes.

Or maybe nested classes don't actually work, and we've been carrying
dead weight for 15 years.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 08:05:29 PM UTC, comment #16: 

comment #15:

> commit 7e9e9260244ed1a551ea9663124841ac7861b97d


(For the record, this commit has not been pushed.)

> an attempt to interpolate a character class as a "title part"
> results in undesired code flow.


As this is documented as unsupported usage, undefined behavior seems a reasonable result.  "Currently, only the 'cflags' request can handle references to character classes," says the manual.

Dave <barx>
Group Member
Mon 17 Nov 2025 07:13:29 PM UTC, comment #15: 

Aha!  This appears to be the same as issue as one I was already on the scent of.


commit 7e9e9260244ed1a551ea9663124841ac7861b97d
Author: G. Branden Robinson <g.branden.robinson@gmail.com>
Date:   Fri Nov 14 16:56:55 2025 -0600

    src/roff/troff/input.cpp: Annotate mystery code.

diff --git a/src/roff/troff/input.cpp b/src/roff/troff/input.cpp
index b1b236e92..01ac6c838 100644
--- a/src/roff/troff/input.cpp
+++ b/src/roff/troff/input.cpp
@@ -10596,6 +10596,8 @@ void get_flags()
     assert(!s.is_null());
     ci->get_flags();
   }
+  // XXX: What purpose does this serve?  Commenting it out breaks no
+  // tests.  So either it's useless or we need another test.  --GBR
   using_character_classes = false;
 }


Evidently it's worse than useless--it causes this bug.

It also causes a hard-to-perceive bug in `tl` request processing, where an attempt to interpolate a character class as a "title part" results in undesired code flow.  Harmlessly as far as I can tell, but I noticed it because I've been adding, yes, even more input-validating diagnostics.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 06:28:31 PM UTC, comment #14: 

Even more minimal:


$ sed '2aF' EXPERIMENTS/67571c.groff
.br
.class [EOS] :
F
.cflags 1 \C'[EOS]'
.pchar :
I:
fold.
.pl \n[nl]u
$ sed '2aF' EXPERIMENTS/67571c.groff|nroff
character ':'
  is not translated
  does not have a macro
  special translation: 0
  hyphenation code: 0
  flags: 0 (none)
  asciify code: 0
  ASCII code: 58
  Unicode mapping: U+003A
  is found
  is transparently translatable
  is not translatable as input
  mode: normal
F I: fold.


But check *this* out.


$ sed '2a\\\\&' EXPERIMENTS/67571c.groff
.br
.class [EOS] :
\&
.cflags 1 \C'[EOS]'
.pchar :
I:
fold.
.pl \n[nl]u
$ sed '2a\\\\&' EXPERIMENTS/67571c.groff | nroff
character ':'
  is not translated
  does not have a macro
  special translation: 0
  hyphenation code: 0
  flags: 0 (none)
  asciify code: 0
  ASCII code: 58
  Unicode mapping: U+003A
  is found
  is transparently translatable
  is not translatable as input
  mode: normal
 I:  fold.


This discrepancy is stinky like bad meat or good cheese.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 06:20:58 PM UTC, comment #13: 

comment #12:

> comment #11:
> > I'm assuming that my "EXPERIMENTS/67571c.groff" behaves for you
> > as it does for me, and as you expect.
>
> Yes and yes.
>
> I can make your 67571c.groff fail by adding a line of output.


> $ nroff 67571c.groff
> I:  fold.
> $ sed '2aFail.' 67571c.groff | nroff
> Fail.  I: fold.


Can reproduce.  Well.  That's damnable as all hell.  I have *no* idea why that's happening.  Looks like I have some deep digging to do.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 10:40:26 AM UTC, comment #12: 

comment #11:

> I'm assuming that my "EXPERIMENTS/67571c.groff" behaves for you
> as it does for me, and as you expect.


Yes and yes.

I can make your 67571c.groff fail by adding a line of output.

$ nroff 67571c.groff
I:  fold.
$ sed '2aFail.' 67571c.groff | nroff
Fail.  I: fold.


Dave <barx>
Group Member
Mon 17 Nov 2025 12:39:21 AM UTC, comment #11: 

Thanks.

So it looks like what we need to is to zero in on why my "EXPERIMENTS/67571c.groff" and your "c1.roff" behave differently, because they're pretty similar.

I'm assuming that my "EXPERIMENTS/67571c.groff" behaves for you as it does for me, and as you expect.

I'm beating on bug #67711 this evening.  Can you take my "EXPERIMENTS/67571c.groff" and perturb it in as small increments as possible until it becomes your "c1.roff", stopping when the behavior suddenly becomes incorrect?

If you can show me the "last good" and "first bad" steps, that'll help me zero in on where the parser is losing its mind.

G. Branden Robinson <gbranden>
Group administrator
Mon 17 Nov 2025 12:07:16 AM UTC, comment #10: 

I used registers to avoid having two input files, but it looks like it's clearer with two input files.

$ cat c1.roff
.class [EOS] :
cflags:
not yet run.
.br
.cflags 1 \C'[EOS]'
cflags:
now run.
$ cat c2.roff
cflags:
not yet run.
.br
.class [EOS] :
.cflags 1 \C'[EOS]'
cflags:
now run.
$ diff c1.roff c2.roff
1d0
< .class [EOS] :
4a4
> .class [EOS] :
$ groff -Tascii c1.roff | cat -s
cflags: not yet run.
cflags: now run.

$ groff -Tascii c2.roff | cat -s
cflags: not yet run.
cflags:  now run.

What about the difference between c1.roff and c2.roff leads you to believe their output should be different?

The above results hold for groff 1.22.4 and 1.23.  If you're running a groff with bug #67570 unfixed, that'll further skew your results.

Dave <barx>
Group Member
Sun 16 Nov 2025 09:03:50 PM UTC, comment #9: 


comment #8:

> comment #7:
> > the first "I: fold" renders with character flags of 0
> > attached to the ':' character.
>
> That is precisely the purpose of the first "I: fold".  It is the "before" exhibit.  It should (and does) render the same way in all cases.
>
> The second "I: fold" is the "after" exhibit.  It follows the unconditional .cflags call.
>
> For this .cflags to have any observable effect, however, the [EOS] class must already be defined.  If it is, this second "I: fold" will render differently from the first; if it isn't, the second will render the same as the first.
>
> When neither register is defined, the class is never established.  Thus the two output lines are identical, as expected.
>
> When either one of the registers is defined, the class should be established: the two .class lines are identical other than the register name, and both occur before the .cflags call.  So if either "early" or "late" is defined, one would expect the second output line to differ from the first.  But only in the "late" case does it.


Our impedance mismatch in discussing this ticket is puzzling me more than GNU troff itself, which is really saying something.  :P

Taking registers out of the picture, I get behavior that is exactly as I expect and as documented.


##$ cat EXPERIMENTS/67571a.groff
.br
I:
fold.
.pl \n[nl]u
##$ nroff EXPERIMENTS/67571a.groff
I: fold.
##$ cat EXPERIMENTS/67571b.groff
.br
.class [EOS] :
I:
fold.
.pl \n[nl]u
##$ nroff EXPERIMENTS/67571b.groff
I: fold.
##$ cat EXPERIMENTS/67571c.groff
.br
.class [EOS] :
.cflags 1 \C'[EOS]'
I:
fold.
.pl \n[nl]u
##$ nroff EXPERIMENTS/67571c.groff
I:  fold.


Even with a couple of sleeps under my belt, I'm about as baffled, either by what you claim the problem is or how your reproducer produced deceptive results, as I was in comment #7.

Help?

G. Branden Robinson <gbranden>
Group administrator
Sun 16 Nov 2025 10:34:34 AM UTC, comment #8: 

comment #7:

> the first "I: fold" renders with character flags of 0
> attached to the ':' character.


That is precisely the purpose of the first "I: fold".  It is the "before" exhibit.  It should (and does) render the same way in all cases.

The second "I: fold" is the "after" exhibit.  It follows the unconditional .cflags call.

For this .cflags to have any observable effect, however, the [EOS] class must already be defined.  If it is, this second "I: fold" will render differently from the first; if it isn't, the second will render the same as the first.

When neither register is defined, the class is never established.  Thus the two output lines are identical, as expected.

When either one of the registers is defined, the class should be established: the two .class lines are identical other than the register name, and both occur before the .cflags call.  So if either "early" or "late" is defined, one would expect the second output line to differ from the first.  But only in the "late" case does it.

Dave <barx>
Group Member
Sat 15 Nov 2025 02:28:55 AM UTC, comment #7: 

Hmm, I've been bashing on groff all day and my brain is probably starting to flag.

Please tell me what's wrong with my modified reproducer, in the sense that it doesn't mimic your original case.



$ cat ATTIC/67571.groff
1234567890123456
.br
.\"pchar \C'[EOS]'
.\"rchar \C'[EOS]'
.pchar :
.if \n[early] .class [EOS] :
I:
fold.
.pchar \C'[EOS]'
.pchar :
.br
.if \n[late] .class [EOS] :
.cflags 1 \C'[EOS]'
I:
fold.
.pchar \C'[EOS]'
.pchar :
.pl \n[nl]u


> The run with "early" should show the same result as "late": as long as the class is defined before .cflags is invoked, it shouldn't matter how long before.  But it does.


I disagree.  Because `cflags 1` has not yet been invoked on the character class in the `-rearly=1` case, the first "I: fold" renders with character flags of 0 attached to the ':' character.

I may be missing something.  I'm just about empty for useful work today.

G. Branden Robinson <gbranden>
Group administrator
Sat 15 Nov 2025 01:39:19 AM UTC, comment #6: 

There isn't one.

The first line of output is meant to show the default rendering, without any flag fiddling.  The second line is meant to show the fiddled rendering.

The "grep ^wh" matches two lines of every run.  In the first run, with no register set, the class is never defined, so the two lines are, expectedly, identical.  The run with "late" set also behaves as expected, showing more space on the second line than on the first.  The run with "early" should show the same result as "late": as long as the class is defined before .cflags is invoked, it shouldn't matter how long before.  But it does.

Dave <barx>
Group Member
Sat 15 Nov 2025 01:19:17 AM UTC, comment #5: 


original submission:

> $ cat colon.roff
> .if \n[early] .class [EOS] :
> I:
> fold.
> .br
> .if \n[late] .class [EOS] :
> .cflags 1 \C'[EOS]'
> I:
> fold.

Where's the `cflags` request after the early `class` request?

G. Branden Robinson <gbranden>
Group administrator
Sat 15 Nov 2025 01:14:33 AM UTC, comment #4: 

It's strange that '[EOS]' keeps getting referred to as a "special character".

My earlier change hasn't regressed already.


$ ./build/test-groff -bww
.pchar \C'[CJKnormal]'
troff: backtrace: file '<standard input>':1
troff:<standard input>:1: warning: special character '[CJKnormal]' is not defined
.mso ja.tmac
ja.tmac: warning: font TR may lack coverage of Japanese script
.pchar \C'[CJKnormal]'
character class '[CJKnormal]'
  defined at: file name: "ja.tmac", line number: 44
  contains ranges: U+3041-U+3096 U+3096 U+30A0-U+30FF U+30FF U+4E00-U+9FFF U+9FFF
  contains nested classes: (none)


G. Branden Robinson <gbranden>
Group administrator
Sat 15 Nov 2025 01:09:42 AM UTC, comment #3: 


$ cat ./ATTIC/67571.groff
1234567890123456
.br
.pchar \C'[EOS]'
.pchar :
.if \n[early] .class [EOS] :
I:
fold.
.pchar \C'[EOS]'
.pchar :
.br
.if \n[late] .class [EOS] :
.cflags 1 \C'[EOS]'
I:
fold.
.pchar \C'[EOS]'
.pchar :
.pl \n[nl]u


Definitely seems like that "early" cflags assignment isn't taking.


$ ./build/test-groff -rearly=1 -T ascii ./ATTIC/67571.groff
troff:./ATTIC/67571.groff:3: warning: special character '[EOS]' is not defined
character ':'
  is not translated
  does not have a macro
  special translation: 0
  hyphenation code: 0
  flags: 0 (none)
  asciify code: 0
  ASCII code: 58
  Unicode mapping: U+003A
  is found
  is transparently translatable
  is not translatable as input
  mode: normal
special character '[EOS]'
  defined at: file name: "./ATTIC/67571.groff", line number: 5
  contains ranges: U+003A
  contains nested classes: (none)
character ':'
  is not translated
  does not have a macro
  special translation: 0
  hyphenation code: 0
  flags: 0 (none)
  asciify code: 0
  ASCII code: 58
  Unicode mapping: U+003A
  is found
  is transparently translatable
  is not translatable as input
  mode: normal
special character '[EOS]'
  defined at: file name: "./ATTIC/67571.groff", line number: 5
  contains ranges: U+003A
  contains nested classes: (none)
character ':'
  is not translated
  does not have a macro
  special translation: 0
  hyphenation code: 0
  flags: 0 (none)
  asciify code: 0
  ASCII code: 58
  Unicode mapping: U+003A
  is found
  is transparently translatable
  is not translatable as input
  mode: normal
1234567890123456
I: fold.
I: fold.


G. Branden Robinson <gbranden>
Group administrator
Sat 15 Nov 2025 01:01:59 AM UTC, comment #2: 


> Can reproduce even after the fix for bug #67571.


Damn it.  Even after the fix for bug #67570.

G. Branden Robinson <gbranden>
Group administrator
Sat 15 Nov 2025 01:01:17 AM UTC, comment #1: 

Can reproduce even after the fix for bug #67571.


$ cat ./ATTIC/67571.groff
.if \n[early] .class [EOS] :
I:
fold.
.br
.if \n[late] .class [EOS] :
.cflags 1 \C'[EOS]'
I:
fold.
.pl \n[nl]u
$ ./build/test-groff -T ascii ./ATTIC/67571.groff
I: fold.
I: fold.
$ ./build/test-groff -rearly=1 -T ascii ./ATTIC/67571.groff
I: fold.
I: fold.
$ ./build/test-groff -rlate=1 -T ascii ./ATTIC/67571.groff
I: fold.
I:  fold.


G. Branden Robinson <gbranden>
Group administrator
Fri 03 Oct 2025 06:04:53 AM UTC, original submission:  

This one smells a lot like bug #67570.  But I see #67570 only on a new build, whereas I can demonstrate this bug in older groffs.

The input file below has the same .class line in two different places; one or the other (or neither) can be called by setting a register (or not) on the command line.  Both .class lines appear before the .cflags line that uses the class; only one of them seems to work.

$ cat colon.roff
.if \n[early] .class [EOS] :
I:
fold.
.br
.if \n[late] .class [EOS] :
.cflags 1 \C'[EOS]'
I:
fold.
$ groff -Z colon.roff | grep ^wh
wh2500
wh2500
$ groff -r early=1 -Z colon.roff | grep ^wh
wh2500
wh2500
$ groff -r late=1 -Z colon.roff | grep ^wh
wh2500
wh5000

I get the above results whether I use groff 1.22.4 or 1.23.0.  (The other historical groff I have at hand, 1.19.2, had not yet implemented the .class request.)

In bleeding-edge groff, #67570 seems to rear its ugly head with late=1:

$ groff-latest -r late=1 -Z colon.roff | grep ^wh
wh2500
wh2500

But early=1 output being different from late=1 is a longstanding bug.  It may have the same root cause as #67570, but since I don't have enough info to say that for sure, I'm filing it separately for now.

Dave <barx>
Group Member

 

Attached Files

This item currently has no attached files.

(Note: upload size limit is set to 4.0MiB, after insertion of the required escape characters.)

Attach Files:
   
   
Comment:
   

 

Dependencies

This item does not depend on any other items.

No items depend on this one.

 

Mail Notification Carbon-Copy List

Carbon-Copy List
  • -email is unavailable- added by gbranden (authors and committer)
  • -email is unavailable- added by gbranden (authors and committer)
  • -email is unavailable- added by gbranden (authors and committer)
  • -email is unavailable- added by gbranden (Updated the item)
  • -email is unavailable- added by barx (Submitted the item)
  •  

    Votes

    There are 0 votes so far. Votes easily highlight which items people would like to see resolved in priority, independently of the priority of the item set by tracker managers.

    Only logged-in users can vote.

     

    Please enter the title of George Orwell's famous dystopian book (it's a date):

    History

    Follow 9 latest changes.

    Date Changed by Updated Field Previous Value => Replaced by
    2025-12-05 gbranden Carbon-Copy- Added -email is unavailable-
        Carbon-Copy- Added -email is unavailable-
        Carbon-Copy- Added -email is unavailable-
    2025-11-17 gbranden StatusNeed Info Confirmed
        Assigned tobarx gbranden
    2025-11-15 gbranden StatusConfirmed Need Info
        Assigned toNone barx
    2025-11-15 gbranden StatusNone Confirmed
    2025-10-20 gbranden Summary.class works or not depending on where in the input it's called [troff] `class` request works or not depending on where in the input it's called

    Back to the top

    Powered by Savane 3.16-a7ba.
    Corresponding source code