Skip to content

ERTS doctests#680

Open
garazdawi wants to merge 16 commits into
masterfrom
lukas/erts/doctest
Open

ERTS doctests#680
garazdawi wants to merge 16 commits into
masterfrom
lukas/erts/doctest

Conversation

@garazdawi

@garazdawi garazdawi commented Mar 14, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Add extensive doctest examples across file handling, atomics, counters, and persistent_term modules, introduce in-memory compilation and preprocessing support (including include_path_open) for strings and doctests, and extend test suites to exercise doctests and new in-memory behaviors.

New Features:

  • Add compile:string/1,2 and compile:noenv_string/2 for compiling Erlang source from strings with full preprocessor support, including include_path_open.
  • Introduce the cooked ram file mode that wraps ram files in a pid-based I/O server, making them usable with io and epp.
  • Allow epp to use a custom include_path_open function for resolving includes and doc file references, enabling fully in-memory preprocessing.
  • Extend ct_doctest with a compile_options option and support for compiling module code blocks via compile:string/2 with include_path_open.

Enhancements:

  • Add comprehensive doctest examples to file, atomics, counters, and persistent_term documentation to demonstrate common usage patterns and edge cases.
  • Refine epp’s module scanning to carry macros in state and support new include handling semantics.
  • Improve ct_doctest option handling by introducing a typed options record and centralizing verbose/skipped_blocks parsing and logging.

Tests:

  • Add doctest-based test cases to compiler, file, atomics, counters, persistent_term, and ct_doctest suites to automatically verify documentation examples.
  • Add new tests for compile:string/2, epp include_path_open handling (including ram-backed includes), cooked ram file behavior, and doctest preprocessor support.

@sourcery-ai

sourcery-ai Bot commented Mar 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds extensive doctest examples for file, atomics, counters, persistent_term and related modules; introduces compile:string/noenv_string and an in‑memory compilation path using ram files and epp’s new include_path_open option; extends ct_doctest to support compiler options and preprocessor-aware module blocks; adds a cooked ram file mode integrated with file_io_server; and wires all of this into new Common Test suites that execute the doctests.

Sequence diagram for file:open/2 with ram and cooked modes

sequenceDiagram
  actor Caller
  participant file
  participant file_io_server
  participant OpenFun
  participant ram_file

  Caller->>file: open(Item, [ram, cooked, read, binary])
  activate file

  file->>file: mode_list(Item, Modes)
  file-->>file: {HasRam=true, HasIoServer=true}

  file->>file_io_server: start_handle(self(), OpenFun, [ram, cooked, read, binary])
  activate file_io_server

  note over file_io_server: do_start_handle(spawn, Owner, OpenFun, ModeList)

  file_io_server->>file_io_server: parse_options(ModeList)
  file_io_server-->>file_io_server: {ReadMode, UnicodeMode, Opts}

  file_io_server->>OpenFun: OpenFun(ReadMode, Opts)
  activate OpenFun
  OpenFun->>ram_file: open(Item, [binary | Modes - {cooked}])
  ram_file-->>OpenFun: {ok, RamFd}
  deactivate OpenFun

  file_io_server-->>file_io_server: Handle = RamFd
  file_io_server-->>Caller: {ok, IoDevice}
  deactivate file_io_server

  deactivate file

  Caller->>file_io_server: read_line(IoDevice)
  file_io_server->>ram_file: read_line(RamFd)
  ram_file-->>file_io_server: {ok, Line}
  file_io_server-->>Caller: {ok, Line}
Loading

Sequence diagram for epp include handling with include_path_open

sequenceDiagram
  actor Compiler
  participant compile
  participant epp
  participant IncludePathOpenFun
  participant file
  participant ram_file

  Compiler->>compile: string(Source, [{include_path_open, Fun} | Opts])
  activate compile
  compile->>compile: internal({string,Source}, Opts)
  compile->>compile: do_parse_string(Fd, Opts)

  compile->>epp: open([{fd, Fd}, {include_path_open, Fun}, ...])
  activate epp
  epp-->>compile: {ok, EppHandle, Extra}

  compile->>epp: parse_file(EppHandle)

  epp->>epp: scan tokens, encounter -include("hdr.hrl")
  epp->>IncludePathOpenFun: Fun(Path, "hdr.hrl", [read])

  alt default Fun = file:path_open/3
    IncludePathOpenFun->>file: path_open(Path, "hdr.hrl", [read])
    file-->>IncludePathOpenFun: {ok, IoDev, FullName}
  else custom in-memory Fun
    IncludePathOpenFun->>ram_file: open(BinaryHeader, [ram, read, binary, cooked])
    ram_file-->>IncludePathOpenFun: {ok, RamFd}
    IncludePathOpenFun-->>IncludePathOpenFun: wrap as IoDev
    IncludePathOpenFun-->>epp: {ok, IoDev, "hdr.hrl"}
  end

  epp-->>compile: Forms
  deactivate epp

  compile->>compile: internal_comp(Passes, Forms, ...)
  compile-->>Compiler: {ok, Module, BeamBin}
  deactivate compile
Loading

File-Level Changes

Change Details Files
Add doctest examples to core library modules and wire them into Common Test suites so documentation code blocks are executed.
  • Augment file.erl, atomics.erl, counters.erl, and persistent_term.erl docs with numerous markdown doctest code blocks demonstrating typical usage and edge cases.
  • Extend file_SUITE, atomics_SUITE, counters_SUITE, persistent_term_SUITE to run ct_doctest:module/3 or /1 against the corresponding modules, preparing any required bindings (e.g., temporary directory, persistent_term cleanup).
  • Adjust an existing persistent_term example to be a complete compilable module (scheduler_ets) plus a runnable shell session snippet.
lib/kernel/src/file.erl
erts/preloaded/src/atomics.erl
erts/preloaded/src/counters.erl
erts/preloaded/src/persistent_term.erl
lib/kernel/test/file_SUITE.erl
erts/emulator/test/atomics_SUITE.erl
erts/emulator/test/counters_SUITE.erl
erts/emulator/test/persistent_term_SUITE.erl
Extend the compiler with string-based compilation using in-memory files and include_path_open, and add tests for this API.
  • Export and document compile:string/1,2 and noenv_string/2, routing them through the existing compilation pipeline with implicit binary output and env_default_opts.
  • Implement an internal {string, String} compilation path that opens the source as a cooked ram file, then feeds it to a new do_parse_string/2 helper using epp:open/1 with features, include paths, macros, and optional include_path_open callback.
  • Teach the compiler to derive a default source name from forms (source_from_forms/1) and to honor a provided {source, ...} option when reporting errors.
  • Add a large string_1/1 test in compile_SUITE.erl covering basic compilation from string/binary, preprocessor usage (macros, records, ifdef, ?MODULE, -d defines), source option effects on compile info and error file names, noenv_string/2, and include_path_open using in-memory headers.
  • Add a doctests/1 test case in compile_SUITE that runs ct_doctest on the compile module with custom compile_options that provide include_path_open for module blocks.
lib/compiler/src/compile.erl
lib/compiler/test/compile_SUITE.erl
Enhance epp and ct_doctest to support in-memory includes, custom compile options, and preprocessor-aware module doctest blocks.
  • Extend epp:open/1 options and internal state with an include_path_open fun defaulting to file:path_open/3; use it for -include, -include_lib, and -doc {file,...} resolution, and ensure it is carried across nested includes and filedoc loading.
  • Refactor module macro tracking in epp so scan_module/scan_module_1 work on the full #epp state, updating macs in place instead of returning plain maps.
  • In ct_doctest, introduce a typed options() list that includes a compile_options field, backed by a new #options record; refactor module/3 and file/3 to normalize the options list and thread the record through parsing, running, and verbose logging.
  • Change doctest module-block compilation to detect -module(...) blocks and call compile:string/2 with a synthesized filename and ct_doctest’s compile_options, instead of hand-scanning/parsing forms; remove the old erl_scan/erl_parse-based helper.
  • Update verbose logging helpers to accept the options record; adjust run_blocks/test_block/run_test/run_tests/run_successful/run_failing signatures accordingly.
  • Add module_preprocessor/1 test in ct_doctest_SUITE exercising module-block doctests that use ?MODULE, -define and -record, supported by a new ct_doctest_module_preproc_mod.erl test module, and tighten existing error expectations for module() tests.
lib/stdlib/src/epp.erl
lib/common_test/src/ct_doctest.erl
lib/common_test/test/ct_doctest_SUITE.erl
lib/common_test/test/ct_doctest_SUITE_data/ct_doctest_module_preproc_mod.erl
Introduce a cooked mode for ram files, integrating ram_file with file_io_server so ram-backed IO works with the io module and higher-level tooling.
  • Add a cooked option to file:open/2 docs, explaining that ram files can be wrapped in a pid-based I/O server compatible with io:get_line/2 and io:scan_erl_form/3.
  • Extend file:open/2 implementation so [ram,cooked] uses file_io_server:start_handle with an OpenFun that opens a ram_file descriptor, stripping ram/cooked from the server’s mode list; keep existing behavior when cooked is absent.
  • Expose file_io_server:start_handle/3 and start_link_handle/3 and refactor do_start to delegate to a more general do_start_handle that accepts a caller-supplied OpenFun and still does parse_options, maybe_add_read_ahead, monitoring, and server_loop setup.
  • Extend ram_file with a read_line/1 implementation layered over read/2, handling newline detection, CRLF normalization, and repositioning the file descriptor to leave unread bytes; export read_line/1 accordingly.
  • Add cooked_ram/1 test in file_SUITE that verifies cooked ram files return a pid, work with io:get_line, io:scan_erl_form, io:put_chars, position/read, and io:getopts, and that non-cooked ram files still return a raw fd.
lib/kernel/src/file.erl
lib/kernel/src/file_io_server.erl
lib/kernel/src/ram_file.erl
lib/kernel/test/file_SUITE.erl
Add epp tests and helpers for include_path_open with ram-backed include files.
  • Extend epp_SUITE with an include_path_open/1 test that preprocesses ram-backed modules including headers served from memory, covering basic macros, nested includes, include_lib resolution, and missing-include errors.
  • Add helper functions epp_open_ram/2, ram_include_path_open/1, and ram_include_path_open_try/3 that open binaries as cooked ram files, run epp:open/1 with include_path_open, and collect forms from epp:parse_file/1 for use in tests.
lib/stdlib/test/epp_SUITE.erl

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In compile.erl the -doc attribute for forms/2 was changed to start and end with four quotes (""""), which will likely break doc parsing/rendering; this should be corrected back to a standard triple-quoted doc string.
  • In compile_SUITE:doctests/1, the fallback branch in PathOpen uses file:open(~"", [ram, cooked | Modes]), but ~"" is not valid Erlang syntax and will fail to compile; replace it with a proper empty string/binary literal (e.g. <<>> or "").
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In compile.erl the -doc attribute for forms/2 was changed to start and end with four quotes (`""""`), which will likely break doc parsing/rendering; this should be corrected back to a standard triple-quoted doc string.
- In compile_SUITE:doctests/1, the fallback branch in PathOpen uses `file:open(~"", [ram, cooked | Modes])`, but `~""` is not valid Erlang syntax and will fail to compile; replace it with a proper empty string/binary literal (e.g. `<<>>` or `""`).

## Individual Comments

### Comment 1
<location path="lib/compiler/src/compile.erl" line_range="1330-1337" />
<code_context>
                        strip_columns(Forms)
                end,
     internal_comp(Ps, NewForms, Source, "", Compile);
+internal({string,String}, Opts0) ->
+    Bin = unicode:characters_to_binary(String),
+    {ok, Fd} = file:open(Bin, [ram, read, binary, cooked]),
+    try
+        do_parse_string(Fd, Opts0)
+    after
+        file:close(Fd)
+    end;
 internal({file,File}, Opts) ->
     {Ext,Ps} = passes(file, Opts),
</code_context>
<issue_to_address>
**issue:** compile:string/2 crashes on file:open/2 failure instead of returning a structured compiler error.

This pattern match on `{ok, Fd}` means any `file:open/2` failure (e.g. ram file allocation failure under memory pressure) will raise `badmatch` instead of returning the usual `{error, {errors, ..., ...}}` tuple. Please handle the `{error, Reason}` case explicitly and convert it into the same error format as `file/2` and `forms/2` so `compile:string/*` remains consistent and doesn’t unexpectedly crash on such failures.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +1330 to +1337
internal({string,String}, Opts0) ->
Bin = unicode:characters_to_binary(String),
{ok, Fd} = file:open(Bin, [ram, read, binary, cooked]),
try
do_parse_string(Fd, Opts0)
after
file:close(Fd)
end;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue: compile:string/2 crashes on file:open/2 failure instead of returning a structured compiler error.

This pattern match on {ok, Fd} means any file:open/2 failure (e.g. ram file allocation failure under memory pressure) will raise badmatch instead of returning the usual {error, {errors, ..., ...}} tuple. Please handle the {error, Reason} case explicitly and convert it into the same error format as file/2 and forms/2 so compile:string/* remains consistent and doesn’t unexpectedly crash on such failures.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +935 to +948
-doc #{ equiv => forms(Forms, ?DEFAULT_OPTIONS) }.
-spec forms(forms()) -> CompRet :: comp_ret().

forms(Forms) -> forms(Forms, ?DEFAULT_OPTIONS).

-doc """
-doc """"
Analogous to [`file/1`](`file/1`), but takes a list of forms (in either Erlang
abstract or Core Erlang format representation) as first argument.

Option `binary` is implicit, that is, no object code file is
produced. For options that normally produce a listing file, such as
'E', the internal format for that compiler pass (an Erlang term,
usually not a binary) is returned instead of a binary.
""".

Comment on lines +2441 to +2447
ram_include_path_open_try([Candidate | Rest], Files, Modes) ->
case maps:find(Candidate, Files) of
{ok, Content} ->
{ok, Fd} = file:open(Content, [ram, read, binary, cooked]),
{ok, Fd, Candidate};
error ->
ram_include_path_open_try(Rest, Files, Modes)
Comment thread lib/kernel/src/file.erl
true ->
file_io_server:start_handle(
self(),
fun(_, _) -> ram_file:open(Item, [binary | ModeList -- [cooked]]) end,
Add an `include_path_open` option to `epp:open/1` that allows callers
to provide a custom function for opening include files. The function
has the same signature as `file:path_open/3` and is used for
`-include`, `-include_lib`, and `-doc {file, ...}` directives.

This enables fully in-memory preprocessing using ram files, where
both the main source and all included files can be served from
memory without touching disk.
This commit makes ram files work as I/O servers so that code that
expects I/O server can use ram files, for example epp:open.

Add read_line/1 to ram_file for reading lines from in-memory binary
data, handling both LF and CRLF line endings.

Add start_handle/3 and start_link_handle/3 to file_io_server, which
accept an open function instead of a filename.

Add a `cooked` option that can be combined with `ram` in file:open
to return a pid-based I/O server instead of a raw file descriptor.
Add compile:string/1,2 and compile:noenv_string/2 that compile Erlang
source code from a string or binary. Unlike compile:forms, the source
goes through the Erlang preprocessor (epp), supporting -define, -include,
-ifdef, records, and all other preprocessor directives.

The `{include_path_open, Fun}` option is passed through to epp, enabling
fully in-memory compilation without file system access for include files.
@garazdawi garazdawi force-pushed the lukas/erts/doctest branch from a53f9ea to 13b55f8 Compare March 17, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants