Skip to content

stdlib: add a format_text function for text-like format strings - #13569

Merged
Octachron merged 7 commits into
ocaml:trunkfrom
Octachron:format_text_function
Feb 20, 2025
Merged

Octachron merged 7 commits into
ocaml:trunkfrom
Octachron:format_text_function

Conversation

@Octachron

Copy link
Copy Markdown
Member

When converting warning messages to Format_doc.t, it was quite annoying to add formatting hints on every space in text paragraphs, for instance:

        "This primitive declaration uses type %a,@ whose@ representation@ \
         may be either boxed or unboxed.@ Without@ an@ annotation@ to@ \
         indicate@ which@ representation@ is@ intended,@ the@ boxed@ \
         representation@ has@ been@ selected@ by@ default.@ This@ default@ \
         choice@ may@ change@ in@ future@ versions@ of@ the@ compiler,@ \
         breaking@ the@ primitive@ implementation.@ You@ should@ explicitly@ \
         annotate@ the@ declaration@ of@ %a@ with@ %a@ or@ %a,@ so@ that@ its@ \
         external@ interface@ remains@ stable@ in@ the future."

In order to remove this annoyance, this PR propose to add a format_text function which replaces spaces and newlines ([' '] and ['\n']) with formatting hints in format string literals. This means that with this new function, one can write

Format.format_text
        "This primitive declaration uses type %a, whose representation \
         may be either boxed or unboxed. Without an annotation to \
         indicate which representation is intended, the boxed \
         representation has been selected by default. This default \
         choice may change in future versions of the compiler, \
         breaking the primitive implementation. You should explicitly \
         annotate the declaration of %a with %a or %a, so that its \
         external interface remains stable in the future."

and obtain a better version of the formatted string literal above.

Compared to pp_print_string, format_text is less efficient (it may allocate a copy of the string) but it does not require to separate the text contents from the specifiers, formatting tags and other formatting hints making it easier
to use in many context.

@dbuenzli

Copy link
Copy Markdown
Contributor

I think it would be nice to clarify and/or add a few things here:

  1. What happens if there are multiple spaces ? Are they collapsed to a single space like in HTML/TeX etc. ?
  2. Is there a way to avoid breaks (.e.g. U+00A0 NO BREAK SPACE, UTF-8: \xC2\xA0, or escape) ?
  3. Is there a way to force line breaks ?
  4. Can we make a blank line force two line breaks ? That is support a notion of paragraphs.

@gasche

gasche commented Oct 23, 2024

Copy link
Copy Markdown
Member

There could be a shorter implementation, but I don't know if it is conceptually better -- you will be disappointed that there is less exciting GADT programming. Format has a machinery to parse a format string at runtime, and check that it is compatible with a given format type -- see CamlinternalFormat.format_of_string_{format,fmtty}. So if you can transform the string representation of the format, and call these functions to parse the result back as a format of the same type.

Now that I think of it, it may actually be tricky to recognize, just in the format string, which spaces are part of the output and should turn into @ , and which are part of the non-literal format syntax. So maybe that approach does not work well.

type neutral_concat =
{ f:
'a 'b 'c 'd 'e 'f. [`String of string | `Char of char ] ->
('a,'b,'c,'d,'e,'f) fmt -> ('a,'b,'c,'d,'e,'f) fmt

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also express this as the following, if you wanted -- reusing the type of End_of_format as the generic type of parameter-less formats.

'f 'b 'c 'e. [ `String of string | `Char of char ] -> ('f, 'b, 'c, 'e, 'e, 'f) fmt

Comment thread stdlib/format.ml Outdated
Comment thread stdlib/format.ml
Comment thread stdlib/format.ml Outdated
@Octachron

Octachron commented Oct 23, 2024 •

Copy link
Copy Markdown
Member Author

@dbuenzli :

  1. k contiguous spaces are translated to a @;<0 k> break hints.
  2. only "\u{20}" is converted to break hints, other white spaces and in particular non-breaking spaces are not affected
    (which are probably the other important space in text outside of CJK texts)
  3. @\n works with its usual composability issue.
  4. not at this point: this sounds potentially nice and useful.

@gasche , I have the impression that in term of complexity using format_of_string only avoids the straightforward helper in CamlnternalFormatBasics at the cost of a parser call and probably mishandling @[<@[h 2@]>@] at the string level.

@dbuenzli

Copy link
Copy Markdown
Contributor

Thanks for your answers @Octachron would likely good to add them in some form to the doc string.

2. non-breaking spaces are not affected

Note: the problem is that using "\u{00A0}" (or its UTF-8 literal byte sequence) will derail the pretty printing process since it spans two bytes. So I feel something is missing to specify non breakable space (if you want to make it confusing it could be "@ " :-).

@Octachron

Copy link
Copy Markdown
Member Author

For this point, I think it would be better to partially solve the issue at the output device level, see #13570 .

@gasche

gasche commented Oct 24, 2024

Copy link
Copy Markdown
Member

I'm satisfied with the tweaked implementation. @dbuenzli, are you satisfied with the proposed specification?

@dbuenzli dbuenzli left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some clarifications are needed.

Comment thread stdlib/format.mli Outdated
with hint breaks or forced newlines:
- Only ascii spaces ([" " = "\u{20}"]) and linefeeds ["\n" = "\u{A}"] are
replaced. In particular, this means that non-breaking spaces (for instance
["\u{A0}"]) are left unchanged.

@dbuenzli dbuenzli Oct 24, 2024 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to talk in Format speak:

  • Spaces " " (U+0020) are replaced by "@ ".
  • Newlines "\n" (U+000A) are replaced by "@<0 1>".
  • The rest is kept unchanged and in particular, non-breaking spaces like U+00A0.

Comment thread stdlib/format.mli Outdated
replaced. In particular, this means that non-breaking spaces (for instance
["\u{A0}"]) are left unchanged.
- [0<=k<=1] newlines followed by [n] spaces are converted to a [@;<0 (k+n)>]
break hint (when [k+n>=1]).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that rule redundant with the space and newline replacing rules ? If not I'm not sure I understand its implications.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coalescing multiple spaces in a single break hint ensure that the line is split at most once in the beginning of the group of spaces. This ensures that the spaces in

format_text "a                                                                                                                                                          b"

is translated to a single line break rather than multiple line breaks between blank lines.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes I think I was confused by the sentence. It read as if this happened only after newlines, but k can be 0. I think the overall formulation is more difficult to understand that it could be. Wouldn't something like this work:

  1. First blank lines (lines made only of spaces) are replaced by two forced newlines "@\n@\n". That is they remain blank in the output.
  2. Remaining sequences made of k spaces (U+0020) and newlines (U+000A) are replaced by @;<0 k>. That is they are either k spaces in the output or a single break.
  3. Breaks can be avoided using-no break space characters (U+00A0).
  4. Lines can be forced by using "@\n"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank lines by themselves should map to a single @\n, it is the preceding line that must be a \@n too:

  1. Blank lines (lines made only of spaces) are replaced by \@n.
  2. Sequences of spaces and a newline preceding a blank line are replaced by \@n: Blank lines remain blank lines in the output.
  3. Remaining sequences made of k spaces and newlines are replaced by @;<0 k>.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added the new description (with a tweak that the space width is at least 1 on break hints) and adapted the implementation to match the behavior and the specification.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Does @dbuenzli's thumb up mean that he approves the specification you converged towards? If so, please feel free to merge.)

Comment thread stdlib/format.mli Outdated

@gasche gasche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find a trace of me having approved this PR. I approve on the basis of my own review and also @dbuenzli's.

@nojb nojb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread stdlib/format.mli Outdated
*)

val format_text: ('a,'b,'c,'d,'e,'f) format6 -> ('a,'b,'c,'d,'e,'f) format6
(** [text fmt] replaces spaces and newlines in the format string literal [fmt]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(** [text fmt] replaces spaces and newlines in the format string literal [fmt]
(** [format_text fmt] replaces spaces and newlines in the format string literal [fmt]

Comment thread stdlib/format.ml Outdated
Comment on lines +1350 to +1358
match newlines, spaces with
| (0|1), spaces ->
let break = Break("", max spaces 1, 0) in
String_literal(before, cons ~repeat:1 break len s pos fmt)
| bl, _ ->
String_literal(
before,
cons ~repeat:bl Force_newline len s pos fmt
)

@nojb nojb Feb 8, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
match newlines, spaces with
| (0|1), spaces ->
let break = Break("", max spaces 1, 0) in
String_literal(before, cons ~repeat:1 break len s pos fmt)
| bl, _ ->
String_literal(
before,
cons ~repeat:bl Force_newline len s pos fmt
)
let repeat, break =
match newlines, spaces with
| (0|1), spaces -> 1, Break("", max spaces 1, 0)
| bl, _ -> bl, Force_newline
in
String_literal(before, cons ~repeat break len s pos fmt)

this functions replaces spaces and newlines ([' '] and ['\n']) by Format
formatting hints in format string literals. This make it easier to mix
free-flowing text with [Printf] specifiers, boxes and tags.
and simplify the implementation along the way
@Octachron
Octachron force-pushed the format_text_function branch from d4b6cac to 21e840f Compare February 20, 2025 12:52
@Octachron
Octachron merged commit b019ca2 into ocaml:trunk Feb 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants