From 19b2f92bf2bbc7254192683077c67e57286fc5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Fri, 13 Mar 2026 11:02:02 +0100 Subject: [PATCH 01/16] stdlib: Add include_path_open option to epp 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. --- lib/stdlib/src/epp.erl | 38 ++++++++--- lib/stdlib/test/epp_SUITE.erl | 117 +++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 11 deletions(-) diff --git a/lib/stdlib/src/epp.erl b/lib/stdlib/src/epp.erl index a230ce81b709..858ccb09d355 100644 --- a/lib/stdlib/src/epp.erl +++ b/lib/stdlib/src/epp.erl @@ -129,7 +129,8 @@ Module:format_error(ErrorDescriptor) features = [] :: [atom()], else_reserved = false :: boolean(), fname = [] :: function_name_type(), - deterministic = false :: boolean() + deterministic = false :: boolean(), + include_path_open = fun file:path_open/3 :: fun() }). %% open(Options) @@ -190,6 +191,13 @@ The option `location` is forwarded to the Erlang token scanner, see The `{compiler_internal,term()}` option is forwarded to the Erlang token scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`). + +The `{include_path_open, Fun}` option 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 when resolving `-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. """. -doc(#{since => <<"OTP 17.0">>}). -spec open(Options) -> @@ -202,6 +210,12 @@ scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`). {'name',FileName :: file:name()} | {'location',StartLocation :: erl_anno:location()} | {'fd',FileDescriptor :: file:io_device()} | + {'include_path_open', IncludePathOpenFun :: fun((Path :: [file:name()], + FileName :: file:name(), + Modes :: [mode]) -> + {ok, IoDevice :: file:io_device(), + FullName :: file:name()} | + {error, term()})} | 'extra' | {'compiler_internal', [term()]}], Epp :: epp_handle(), @@ -827,6 +841,8 @@ init_server(Pid, FileName, Options, St0) -> AtLocation = proplists:get_value(location, Options, 1), Deterministic = proplists:get_value(deterministic, Options, false), + PathOpen = proplists:get_value(include_path_open, Options, + fun file:path_open/3), St = St0#epp{delta=0, name=SourceName, name2=SourceName, path=Path, location=AtLocation, macs=Ms1, default_encoding=DefEncoding, @@ -839,7 +855,8 @@ init_server(Pid, FileName, Options, St0) -> end, features = Features, else_reserved = ResWordFun('else'), - deterministic = Deterministic}, + deterministic = Deterministic, + include_path_open = PathOpen}, From = wait_request(St), Anno = erl_anno:new(AtLocation), enter_file_reply(From, file_name(SourceName), Anno, @@ -993,7 +1010,7 @@ enter_file(_NewName, Inc, From, St) epp_reply(From, {error,{loc(Inc),epp,{depth,"include"}}}), wait_req_scan(St); enter_file(NewName, Inc, From, St) -> - case file:path_open(St#epp.path, NewName, [read]) of + case (St#epp.include_path_open)(St#epp.path, NewName, [read]) of {ok,NewF,Pname} -> Loc = start_loc(St#epp.location), wait_req_scan(enter_file2(NewF, Pname, From, St, Loc)); @@ -1014,7 +1031,8 @@ enter_file2(NewF, Pname, From, St0, AtLocation) -> erl_scan_opts = ScanOpts, else_reserved = ElseReserved, features = Ftrs, - deterministic = Deterministic} = St0, + deterministic = Deterministic, + include_path_open = PathOpen} = St0, Ms = Ms0#{'FILE':={none,[{string,Anno,source_name(St0,Pname)}]}}, %% update the head of the include path to be the directory of the new %% source file, so that an included file can always include other files @@ -1031,7 +1049,8 @@ enter_file2(NewF, Pname, From, St0, AtLocation) -> erl_scan_opts = ScanOpts, else_reserved = ElseReserved, default_encoding=DefEncoding, - deterministic=Deterministic}. + deterministic=Deterministic, + include_path_open=PathOpen}. enter_file_reply(From, Name, LocationAnno, AtLocation, Where, Deterministic) -> Anno0 = erl_anno:new(AtLocation), @@ -1214,7 +1233,7 @@ scan_filedoc_content({string, _A, DocFilename}, Dot, {atom,DocLoc,Doc}, From, #epp{name = CurrentFilename} = St) -> %% The head of the path is the dir where the current file is Cwd = hd(St#epp.path), - case file:path_open([Cwd], DocFilename, [read, binary]) of + case (St#epp.include_path_open)([Cwd], DocFilename, [read, binary]) of {ok, NewF, Pname} -> case file:read_file_info(NewF) of {ok, #file_info{ size = Sz }} -> @@ -1506,14 +1525,15 @@ scan_include_lib1([{'(',_Alp},{string,_Af,NewName0}=N,{')',_Arp},{dot,_Ad}], _Inc, From, St) -> NewName = expand_var(NewName0), Loc = start_loc(St#epp.location), - case file:path_open(St#epp.path, NewName, [read]) of + case (St#epp.include_path_open)(St#epp.path, NewName, [read]) of {ok,NewF,Pname} -> wait_req_scan(enter_file2(NewF, Pname, From, St, Loc)); {error,_E1} -> case expand_lib_dir(NewName) of {ok,Header} -> - case file:open(Header, [read]) of - {ok,NewF} -> + {ok, Cwd} = file:get_cwd(), + case (St#epp.include_path_open)([Cwd], Header, [read]) of + {ok,NewF,_} -> wait_req_scan(enter_file2(NewF, Header, From, St, Loc)); {error,_E2} -> diff --git a/lib/stdlib/test/epp_SUITE.erl b/lib/stdlib/test/epp_SUITE.erl index 911001015ef9..7f4cc0cae6c7 100644 --- a/lib/stdlib/test/epp_SUITE.erl +++ b/lib/stdlib/test/epp_SUITE.erl @@ -36,7 +36,8 @@ gh_8268/1, moduledoc_include/1, stringify/1, - fun_type_arg/1 + fun_type_arg/1, + include_path_open/1 ]). -export([epp_parse_erl_form/2]). @@ -84,7 +85,8 @@ all() -> gh_8268, moduledoc_include, stringify, - fun_type_arg]. + fun_type_arg, + include_path_open]. groups() -> [{upcase_mac, [], [upcase_mac_1, upcase_mac_2]}, @@ -2334,6 +2336,117 @@ run_test(Config, Test0, Opts0) -> Reply end. +%% Test the include_path_open option for epp using ram files. +include_path_open(_Config) -> + %% Test 1: Basic ram file preprocessing with macros + Code1 = <<"-module(ram_test).\n" + "-export([f/0]).\n" + "-define(HELLO, hello).\n" + "f() -> {?MODULE, ?HELLO}.\n">>, + Forms1 = epp_open_ram(Code1, []), + {attribute,_,module,ram_test} = lists:keyfind(module, 3, Forms1), + {ok, ram_test, Bin1} = compile:forms(Forms1), + {module, ram_test} = code:load_binary(ram_test, "ram_test.erl", Bin1), + {ram_test, hello} = ram_test:f(), + true = code:delete(ram_test), + code:purge(ram_test), + + %% Test 2: Ram file with -include resolved via include_path_open + HrlContent = <<"-record(point, {x, y}).\n" + "-define(ORIGIN, #point{x=0, y=0}).\n">>, + Code2 = <<"-module(ram_inc).\n" + "-include(\"my_header.hrl\").\n" + "-export([origin/0]).\n" + "origin() -> ?ORIGIN.\n">>, + Files2 = #{"my_header.hrl" => HrlContent}, + PathOpen2 = ram_include_path_open(Files2), + Forms2 = epp_open_ram(Code2, [{include_path_open, PathOpen2}]), + {attribute,_,module,ram_inc} = lists:keyfind(module, 3, Forms2), + {ok, ram_inc, Bin2} = compile:forms(Forms2), + {module, ram_inc} = code:load_binary(ram_inc, "ram_inc.erl", Bin2), + {point, 0, 0} = ram_inc:origin(), + true = code:delete(ram_inc), + code:purge(ram_inc), + + %% Test 3: Nested includes via ram files + Hrl3a = <<"-define(A, a_value).\n">>, + Hrl3b = <<"-include(\"a.hrl\").\n" + "-define(B, {?A, b_value}).\n">>, + Code3 = <<"-module(ram_nested).\n" + "-include(\"b.hrl\").\n" + "-export([t/0]).\n" + "t() -> ?B.\n">>, + Files3 = #{"a.hrl" => Hrl3a, "b.hrl" => Hrl3b}, + PathOpen3 = ram_include_path_open(Files3), + Forms3 = epp_open_ram(Code3, [{include_path_open, PathOpen3}]), + {ok, ram_nested, Bin3} = compile:forms(Forms3), + {module, ram_nested} = code:load_binary(ram_nested, "ram_nested.erl", Bin3), + {a_value, b_value} = ram_nested:t(), + true = code:delete(ram_nested), + code:purge(ram_nested), + + %% Test 4: include_lib resolved via include_path_open + HrlLib = <<"-define(LIB_VAL, from_lib).\n">>, + Code4 = <<"-module(ram_lib).\n" + "-include_lib(\"myapp/include/lib.hrl\").\n" + "-export([t/0]).\n" + "t() -> ?LIB_VAL.\n">>, + Files4 = #{"myapp/include/lib.hrl" => HrlLib}, + PathOpen4 = ram_include_path_open(Files4), + Forms4 = epp_open_ram(Code4, [{include_path_open, PathOpen4}]), + {ok, ram_lib, Bin4} = compile:forms(Forms4), + {module, ram_lib} = code:load_binary(ram_lib, "ram_lib.erl", Bin4), + from_lib = ram_lib:t(), + true = code:delete(ram_lib), + code:purge(ram_lib), + + %% Test 5: Missing include via include_path_open gives proper error + Code5 = <<"-module(ram_missing).\n" + "-include(\"missing.hrl\").\n" + "-export([t/0]).\n" + "t() -> ok.\n">>, + PathOpen5 = ram_include_path_open(#{}), + Forms5 = epp_open_ram(Code5, [{include_path_open, PathOpen5}]), + {error, {2, epp, {include, file, "missing.hrl"}}} = + lists:keyfind(error, 1, Forms5), + + ok. + +%% Helper: open a binary as a cooked ram file and preprocess with epp. +epp_open_ram(Bin, ExtraOpts) -> + {ok, Fd} = file:open(Bin, [ram, read, binary, cooked]), + try + {ok, Epp} = epp:open([{fd, Fd}, {name, "ram_file.erl"}, + {location, 1} | ExtraOpts]), + try + collect_epp_forms(Epp) + after + epp:close(Epp) + end + after + file:close(Fd) + end. + +%% Helper: create a include_path_open function backed by in-memory files. +ram_include_path_open(Files) -> + fun(Path, Name, Modes) -> + %% Try Name directly (for absolute paths and include_lib), + %% then try each path prefix. + Candidates = [Name | [filename:join(Dir, Name) || Dir <- Path]], + ram_include_path_open_try(Candidates, Files, Modes) + end. + +ram_include_path_open_try([], _Files, _Modes) -> + {error, enoent}; +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) + end. + fail() -> ct:fail(failed). From 50c9f61cbd92b4ac58c8317525af79c425919ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Fri, 13 Mar 2026 10:26:47 +0100 Subject: [PATCH 02/16] kernel: Extend ram files to work as IO servers 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. --- lib/kernel/src/file.erl | 16 +++++++++- lib/kernel/src/file_io_server.erl | 33 +++++++++++++------- lib/kernel/src/ram_file.erl | 37 +++++++++++++++++++++-- lib/kernel/test/file_SUITE.erl | 50 +++++++++++++++++++++++++++++-- 4 files changed, 120 insertions(+), 16 deletions(-) diff --git a/lib/kernel/src/file.erl b/lib/kernel/src/file.erl index b006a6a9156c..deaa1180ba9e 100644 --- a/lib/kernel/src/file.erl +++ b/lib/kernel/src/file.erl @@ -1411,6 +1411,12 @@ more of the following options: - **`ram`** - `File` must be `t:iodata/0`. Returns an `t:fd/0`, which lets module `file` operate on the data in-memory as if it is a file. +- **`cooked`** - Only meaningful together with `ram`. When specified, the + ram file is wrapped in a pid-based I/O server, making it compatible with + the `m:io` module (for example, `io:get_line/2`, `io:scan_erl_form/3`). + Without `cooked`, ram files return a raw file descriptor that cannot be + used with `m:io` functions. + - **`sync`** - On platforms supporting it, enables the POSIX `O_SYNC` synchronous I/O flag or its platform-dependent equivalent (for example, `FILE_FLAG_WRITE_THROUGH` on Windows) so that writes to the file block until @@ -1490,7 +1496,15 @@ open(Item, ModeList) when is_list(ModeList) -> {true, false} -> raw_file_io:open(file_name(Item), ModeList); {false, true} -> - ram_file:open(Item, ModeList); + case lists:member(cooked, ModeList) of + true -> + file_io_server:start_handle( + self(), + fun(_, _) -> ram_file:open(Item, [binary | ModeList -- [cooked]]) end, + ModeList -- [ram, cooked]); + false -> + ram_file:open(Item, ModeList) + end; {true, true} -> erlang:error(badarg, [Item, ModeList]) end; diff --git a/lib/kernel/src/file_io_server.erl b/lib/kernel/src/file_io_server.erl index 956fcd7d60bb..89555fd20965 100644 --- a/lib/kernel/src/file_io_server.erl +++ b/lib/kernel/src/file_io_server.erl @@ -28,6 +28,7 @@ -export([format_error/1]). -export([start/3, start_link/3]). +-export([start_handle/3, start_link_handle/3]). -export([count_and_find/3]). @@ -60,27 +61,36 @@ start_link(Owner, FileName, ModeList) when is_pid(Owner), (is_list(FileName) orelse is_binary(FileName)), is_list(ModeList) -> do_start(spawn_link, Owner, FileName, ModeList). +start_handle(Owner, OpenFun, ModeList) + when is_pid(Owner), is_function(OpenFun, 2), is_list(ModeList) -> + do_start_handle(spawn, Owner, OpenFun, ModeList). + +start_link_handle(Owner, OpenFun, ModeList) + when is_pid(Owner), is_function(OpenFun, 2), is_list(ModeList) -> + do_start_handle(spawn_link, Owner, OpenFun, ModeList). + %%%----------------------------------------------------------------- %%% Server starter, dispatcher and helpers do_start(Spawn, Owner, FileName, ModeList) -> + OpenFun = fun(ReadMode, Opts0) -> + Opts = maybe_add_read_ahead(ReadMode, Opts0), + raw_file_io:open(FileName, [raw | Opts]) + end, + do_start_handle(Spawn, Owner, OpenFun, ModeList). + +do_start_handle(Spawn, Owner, OpenFun, ModeList) -> Self = self(), Ref = make_ref(), Utag = erlang:dt_spread_tag(true), - Pid = + Pid = erlang:Spawn( fun() -> erlang:dt_restore_tag(Utag), - %% process_flag(trap_exit, true), case parse_options(ModeList) of - {ReadMode, UnicodeMode, Opts0} -> - Opts = maybe_add_read_ahead(ReadMode, Opts0), - case raw_file_io:open(FileName, [raw | Opts]) of - {error, Reason} = Error -> - Self ! {Ref, Error}, - exit(Reason); + {ReadMode, UnicodeMode, Opts} -> + case OpenFun(ReadMode, Opts) of {ok, Handle} -> - %% XXX must I handle R6 nodes here? M = erlang:monitor(process, Owner), Self ! {Ref, ok}, server_loop( @@ -89,7 +99,10 @@ do_start(Spawn, Owner, FileName, ModeList) -> mref = M, buf = <<>>, read_mode = ReadMode, - unic = UnicodeMode}) + unic = UnicodeMode}); + {error, Reason} = Error -> + Self ! {Ref, Error}, + exit(Reason) end; {error,Reason1} = Error1 -> Self ! {Ref, Error1}, diff --git a/lib/kernel/src/ram_file.erl b/lib/kernel/src/ram_file.erl index 43a8cc4028f9..20625e196dcf 100644 --- a/lib/kernel/src/ram_file.erl +++ b/lib/kernel/src/ram_file.erl @@ -26,8 +26,8 @@ %% Generic file contents operations -export([open/2, close/1]). --export([write/2, read/2, copy/3, - pread/2, pread/3, pwrite/2, pwrite/3, +-export([write/2, read/2, read_line/1, copy/3, + pread/2, pread/3, pwrite/2, pwrite/3, position/2, truncate/1, datasync/1, sync/1]). %% Specialized file operations @@ -143,7 +143,38 @@ read(#file_descriptor{module = ?MODULE, data = Port}, Sz) {error, einval} end. -write(#file_descriptor{module = ?MODULE, data = Port}, Bytes) -> +read_line(#file_descriptor{module = ?MODULE} = Fd) -> + read_line(Fd, []). + +read_line(Fd, Acc) -> + case read(Fd, 256) of + {ok, Data} -> + case binary:match(Data, <<"\n">>) of + {Pos, 1} -> + %% Check for \r\n: replace with just \n. + {Before, After} = + case Pos > 0 andalso binary:at(Data, Pos - 1) =:= $\r of + true -> + {binary:part(Data, 0, Pos - 1), binary:part(Data, Pos + 1, byte_size(Data) - Pos - 1)}; + false -> + {binary:part(Data, 0, Pos), binary:part(Data, Pos + 1, byte_size(Data) - Pos - 1)} + end, + %% Seek back the unread portion after the \n. + case byte_size(After) of + 0 -> ok; + N -> {ok, _} = position(Fd, {cur, -N}) + end, + {ok, iolist_to_binary(lists:reverse(Acc, [Before, <<"\n">>]))}; + nomatch -> + read_line(Fd, [Data | Acc]) + end; + eof when Acc =:= [] -> + eof; + eof -> + {ok, iolist_to_binary(lists:reverse(Acc))} + end. + +write(#file_descriptor{module = ?MODULE, data = Port}, Bytes) -> case call_port(Port, [?RAM_FILE_WRITE | Bytes]) of {ok, _Sz} -> ok; diff --git a/lib/kernel/test/file_SUITE.erl b/lib/kernel/test/file_SUITE.erl index 1cfa5bfbdb06..a746a616040c 100644 --- a/lib/kernel/test/file_SUITE.erl +++ b/lib/kernel/test/file_SUITE.erl @@ -56,7 +56,8 @@ -export([close/1, consult1/1, path_consult/1, delete/1]). -export([ eval1/1, path_eval/1, script1/1, path_script/1, open1/1, - old_modes/1, new_modes/1, path_open/1, open_errors/1]). + old_modes/1, new_modes/1, path_open/1, open_errors/1, + cooked_ram/1]). -export([ file_info_basic_file/1, file_info_basic_directory/1, file_info_bad/1, file_info_times/1, file_write_file_info/1, file_wfi_helpers/1]). @@ -154,7 +155,7 @@ groups() -> {open, [], [open1, old_modes, new_modes, path_open, close, access, read_write, pread_write, append, open_errors, - exclusive]}, + exclusive, cooked_ram]}, {pos, [], [pos1, pos2, pos3]}, {file_info, [], [file_info_basic_file, file_info_basic_directory, @@ -1318,6 +1319,51 @@ exclusive(Config) when is_list(Config) -> ok = ?FILE_MODULE:close(Fd), ok. +%% Test cooked option for ram files. +%% The cooked option wraps a ram file in a pid-based I/O server, +%% making it usable with the io module (unlike a plain ram file +%% which returns a raw file descriptor). +cooked_ram(_Config) -> + Data = <<"hello, world\nline two\n">>, + + %% Basic read via io module. + {ok, Fd1} = ?FILE_MODULE:open(Data, [ram, read, binary, cooked]), + true = is_pid(Fd1), + <<"hello, world\n">> = io:get_line(Fd1, ''), + <<"line two\n">> = io:get_line(Fd1, ''), + eof = io:get_line(Fd1, ''), + ok = ?FILE_MODULE:close(Fd1), + + %% Write and read back. + {ok, Fd2} = ?FILE_MODULE:open(<<>>, [ram, write, read, binary, cooked]), + true = is_pid(Fd2), + ok = io:put_chars(Fd2, "test data"), + {ok, 0} = ?FILE_MODULE:position(Fd2, bof), + {ok, <<"test data">>} = ?FILE_MODULE:read(Fd2, 100), + ok = ?FILE_MODULE:close(Fd2), + + %% io:scan_erl_form works (needed for epp). + ErlData = <<"-module(test).\n">>, + {ok, Fd3} = ?FILE_MODULE:open(ErlData, [ram, read, binary, cooked]), + true = is_pid(Fd3), + {ok, Tokens, _} = io:scan_erl_form(Fd3, '', 1), + true = is_list(Tokens), + ok = ?FILE_MODULE:close(Fd3), + + %% Without cooked, ram files return a raw fd (not a pid). + {ok, RawFd} = ?FILE_MODULE:open(Data, [ram, read, binary]), + false = is_pid(RawFd), + ok = ?FILE_MODULE:close(RawFd), + + %% io:setopts and io:getopts work on cooked ram files. + {ok, Fd4} = ?FILE_MODULE:open(Data, [ram, read, binary, cooked]), + Opts = io:getopts(Fd4), + true = is_list(Opts), + ok = ?FILE_MODULE:close(Fd4), + + [] = flush(), + ok. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% From f14ebeae435a2c933c03512a68cebbe2a8761081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Fri, 13 Mar 2026 11:19:51 +0100 Subject: [PATCH 03/16] compiler: Add compile:string/1,2 for compiling source code strings 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. --- lib/compiler/src/compile.erl | 119 ++++++++++++++++++++ lib/compiler/test/compile_SUITE.erl | 169 +++++++++++++++++++++++++++- 2 files changed, 285 insertions(+), 3 deletions(-) diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index f532df2329ad..80b4950e9316 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -205,6 +205,7 @@ source code. %% High-level interface. -export([file/1,file/2,noenv_file/2,format_error/1]). -export([forms/1,forms/2,noenv_forms/2]). +-export([string/1,string/2,noenv_string/2]). -export([output_generated/1,noenv_output_generated/1]). -export([options/0]). -export([env_compiler_options/0]). @@ -999,6 +1000,51 @@ noenv_forms(Forms, Opts) when is_list(Opts) -> noenv_forms(Forms, Opt) when is_atom(Opt) -> noenv_forms(Forms, [Opt|?DEFAULT_OPTIONS]). +-doc #{ equiv => string(String, []) }. +-spec string(String :: unicode:chardata()) -> CompRet :: comp_ret(). + +string(String) -> string(String, ?DEFAULT_OPTIONS). + +-doc """ +Compiles an Erlang source code string. + +Analogous to [`file/1`](`file/1`), but takes a `t:unicode:chardata/0` containing +Erlang source code as first argument. The source code is run through the +Erlang preprocessor (`epp`) just as when compiling a file, so directives +such as `-include`, `-define`, and `-ifdef` are supported. + +Option `binary` is implicit, that is, no object code file is produced. + +The `source` option can be used to set the source file name that will +appear in error messages and the `module_info/1` function. If not given, +it defaults to the module name with a `.erl` extension. + +The `{include_path_open, Fun}` option can be used to provide a custom function +for opening include files (see `epp:open/1`), allowing compilation +entirely in memory without touching the file system. + +```erlang +1> compile:string("-module(foo). -export([bar/0]). bar() -> ok."). +{ok,foo,<<...>>} +``` +""". +-spec string(String :: unicode:chardata(), Options :: [option()] | option()) -> + CompRet :: comp_ret(). + +string(String, Opts) when is_list(Opts) -> + do_compile({string,String}, [binary|Opts++env_default_opts()]); +string(String, Opt) when is_atom(Opt) -> + string(String, [Opt|?DEFAULT_OPTIONS]). + +-doc #{ equiv => noenv_string(String, []) }. +-spec noenv_string(String :: unicode:chardata(), Options :: [option()] | option()) -> + comp_ret(). + +noenv_string(String, Opts) when is_list(Opts) -> + do_compile({string,String}, [binary|Opts]); +noenv_string(String, Opt) when is_atom(Opt) -> + noenv_string(String, [Opt|?DEFAULT_OPTIONS]). + -doc """ Works like `output_generated/1`, except that the environment variable `ERL_COMPILER_OPTIONS` is not consulted. @@ -1250,6 +1296,14 @@ internal({forms,Forms}, Opts0) -> 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), Compile = build_compile(Opts), @@ -1953,6 +2007,71 @@ parse_module(_Code, St) -> Ret end. +do_parse_string(Fd, Opts0) -> + StartLocation = case with_columns(Opts0) of + true -> + {1,1}; + false -> + 1 + end, + case erl_features:init_parse_state(Opts0, fun erl_scan:f_reserved_word/1) of + {ok, {Features, ResWordFun}} -> + PathOpenOpt = case proplists:get_value(include_path_open, Opts0) of + undefined -> []; + PathOpenFun -> [{include_path_open, PathOpenFun}] + end, + Name = proplists:get_value(source, Opts0, "string"), + EppOpts = [{fd, Fd}, + {name, Name}, + {includes, ["." | inc_paths(Opts0)]}, + {macros, pre_defs(Opts0)}, + {default_encoding, utf8}, + {location, StartLocation}, + {reserved_word_fun, ResWordFun}, + {features, Features}, + extra | + PathOpenOpt ++ + case member(check_ssa, Opts0) of + true -> + [{compiler_internal, [ssa_checks]}]; + false -> + [] + end], + {ok, Epp, Extra0} = epp:open(EppOpts), + try + Forms0 = epp:parse_file(Epp), + Epp ! {get_features, self()}, + UsedFtrs = receive {features, X} -> X end, + Extra = [{features, UsedFtrs} | Extra0], + Encoding = proplists:get_value(encoding, Extra), + {_, Ps} = passes(forms, Opts0), + Source = proplists:get_value(source, Opts0, source_from_forms(Forms0)), + Opts1 = proplists:delete(source, Opts0), + Compile0 = build_compile(Opts1), + St0 = metadata_add_features(UsedFtrs, Compile0), + Opts2 = [{features, UsedFtrs} | St0#compile.options], + St1 = St0#compile{encoding=Encoding, options=Opts2}, + Forms = case with_columns(Opts2 ++ compile_options(Forms0)) of + true -> + Forms0; + false -> + strip_columns(Forms0) + end, + internal_comp(Ps, Forms, Source, "", St1) + after + epp:close(Epp) + end; + {error, {Mod, Reason}} -> + Source = proplists:get_value(source, Opts0, "string"), + Es = [{Source, [{none, Mod, Reason}]}], + {error, {errors, Es, []}} + end. + +source_from_forms([{attribute,_,module,Mod}|_]) -> + atom_to_list(Mod) ++ ".erl"; +source_from_forms([_|T]) -> source_from_forms(T); +source_from_forms([]) -> "string". + deterministic_filename(#compile{ifile=File,options=Opts}) -> SourceName0 = proplists:get_value(source, Opts, File), case member(deterministic, Opts) of diff --git a/lib/compiler/test/compile_SUITE.erl b/lib/compiler/test/compile_SUITE.erl index 7e448dd8ef20..9b5e3e57cda2 100644 --- a/lib/compiler/test/compile_SUITE.erl +++ b/lib/compiler/test/compile_SUITE.erl @@ -26,11 +26,11 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("stdlib/include/erl_compile.hrl"). --export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2, app_test/1,appup_test/1,bigE_roundtrip/1, debug_info/4, custom_debug_info/1, custom_compile_info/1, - file_1/1, forms_2/1, module_mismatch/1, outdir/1, + file_1/1, forms_2/1, string_1/1, module_mismatch/1, outdir/1, binary/1, makedep/1, cond_and_ifdef/1, listings/1, listings_big/1, other_output/1, encrypted_abstr/1, strict_record/1, utf8_atoms/1, utf8_functions/1, extra_chunks/1, @@ -55,7 +55,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [app_test, appup_test, bigE_roundtrip, file_1, - forms_2, module_mismatch, outdir, + forms_2, string_1, module_mismatch, outdir, binary, makedep, cond_and_ifdef, listings, listings_big, other_output, encrypted_abstr, tuple_calls, strict_record, utf8_atoms, utf8_functions, extra_chunks, @@ -321,6 +321,169 @@ forms_compile_and_load(Code, Opts) -> false = code:purge(simple), ok. +string_1(_Config) -> + %% Basic compilation from string. + {ok,foo,BinFoo} = compile:string("-module(foo). -export([bar/0]). bar() -> hello."), + {module,foo} = code:load_binary(foo, "foo.erl", BinFoo), + hello = foo:bar(), + true = code:delete(foo), + false = code:purge(foo), + + %% Binary input. + {ok,binmod,BinMod} = compile:string(<<"-module(binmod). -export([g/0]). g() -> ok.">>), + {module,binmod} = code:load_binary(binmod, "binmod.erl", BinMod), + ok = binmod:g(), + true = code:delete(binmod), + false = code:purge(binmod), + + %% Preprocessor: macros with -define. + {ok,macmod,BinMac} = compile:string( + "-module(macmod). -export([f/0]).\n" + "-define(X, 42).\n" + "f() -> ?X.\n"), + {module,macmod} = code:load_binary(macmod, "macmod.erl", BinMac), + 42 = macmod:f(), + true = code:delete(macmod), + false = code:purge(macmod), + + %% Preprocessor: records. + {ok,recmod,BinRec} = compile:string( + "-module(recmod). -export([new/0]).\n" + "-record(point, {x = 0, y = 0}).\n" + "new() -> #point{x = 1, y = 2}.\n"), + {module,recmod} = code:load_binary(recmod, "recmod.erl", BinRec), + {point,1,2} = recmod:new(), + true = code:delete(recmod), + false = code:purge(recmod), + + %% Preprocessor: -ifdef/-endif. + {ok,ifmod,BinIf} = compile:string( + "-module(ifmod). -export([f/0]).\n" + "-ifdef(NOTDEFINED).\n" + "f() -> wrong.\n" + "-else.\n" + "f() -> right.\n" + "-endif.\n"), + {module,ifmod} = code:load_binary(ifmod, "ifmod.erl", BinIf), + right = ifmod:f(), + true = code:delete(ifmod), + false = code:purge(ifmod), + + %% Preprocessor: predefined macros via options. + {ok,defmod,BinDef} = compile:string( + "-module(defmod). -export([f/0]).\n" + "f() -> ?MY_VALUE.\n", + [{d,'MY_VALUE',99}]), + {module,defmod} = code:load_binary(defmod, "defmod.erl", BinDef), + 99 = defmod:f(), + true = code:delete(defmod), + false = code:purge(defmod), + + %% Preprocessor: ?MODULE. + {ok,modmac,BinModMac} = compile:string( + "-module(modmac). -export([name/0]).\n" + "name() -> ?MODULE.\n"), + {module,modmac} = code:load_binary(modmac, "modmac.erl", BinModMac), + modmac = modmac:name(), + true = code:delete(modmac), + false = code:purge(modmac), + + %% Source option. + {ok,srcmod,BinSrc} = compile:string( + "-module(srcmod). -export([f/0]). f() -> ok.", + [{source,"my_source.erl"}]), + {module,srcmod} = code:load_binary(srcmod, "srcmod.erl", BinSrc), + Info = srcmod:module_info(compile), + SrcInfo = proplists:get_value(source, Info), + true = lists:suffix("my_source.erl", SrcInfo), + true = code:delete(srcmod), + false = code:purge(srcmod), + + %% Syntax error returns error. + error = compile:string("-module(bad). f( ->"), + + %% Syntax error with return_errors option. + {error,[{"string",[{_,_,_}|_]}],_} = + compile:string("-module(bad). f( ->", [return_errors]), + + %% Source option affects error filenames. + {error,[{"bad.erl",[{_,_,_}|_]}],_} = + compile:string("-module(bad). f( ->", + [return_errors, {source, "bad.erl"}]), + + %% Cover: option not in a list (undocumented feature). + {ok,smod,_} = compile:string("-module(smod). -export([f/0]). f() -> ok.", binary), + + %% noenv_string/2. + {ok,nmod,_} = compile:noenv_string( + "-module(nmod). -export([f/0]). f() -> ok.", []), + + %% include_path_open: include from in-memory files. + Headers = #{ + "my_header.hrl" => + <<"-record(point, {x, y}).\n" + "-define(ORIGIN, #point{x=0, y=0}).\n">> + }, + PathOpen = fun(_Path, Name, _Modes) -> + case maps:find(Name, Headers) of + {ok, Content} -> + {ok, Fd} = file:open(Content, [ram, read, binary, cooked]), + {ok, Fd, Name}; + error -> + {error, enoent} + end + end, + {ok,incmod,BinInc} = compile:string( + "-module(incmod).\n" + "-include(\"my_header.hrl\").\n" + "-export([f/0]).\n" + "f() -> ?ORIGIN.\n", + [{include_path_open, PathOpen}]), + {module,incmod} = code:load_binary(incmod, "incmod.erl", BinInc), + {point,0,0} = incmod:f(), + true = code:delete(incmod), + false = code:purge(incmod), + + %% include_path_open: nested includes from in-memory files. + Headers2 = #{ + "types.hrl" => + <<"-type my_int() :: integer().\n">>, + "all.hrl" => + <<"-include(\"types.hrl\").\n" + "-define(DEFAULT, 0).\n">> + }, + PathOpen2 = fun(_Path, Name, _Modes) -> + case maps:find(Name, Headers2) of + {ok, Content} -> + {ok, Fd} = file:open(Content, [ram, read, binary, cooked]), + {ok, Fd, Name}; + error -> + {error, enoent} + end + end, + {ok,nestmod,BinNest} = compile:string( + "-module(nestmod).\n" + "-include(\"all.hrl\").\n" + "-export([f/0]).\n" + "-spec f() -> my_int().\n" + "f() -> ?DEFAULT.\n", + [{include_path_open, PathOpen2}]), + {module,nestmod} = code:load_binary(nestmod, "nestmod.erl", BinNest), + 0 = nestmod:f(), + true = code:delete(nestmod), + false = code:purge(nestmod), + + %% include_path_open: missing include returns error. + PathOpenNone = fun(_Path, _Name, _Modes) -> {error, enoent} end, + {error, _, _} = compile:string( + "-module(missinc).\n" + "-include(\"nonexistent.hrl\").\n" + "-export([f/0]).\n" + "f() -> ok.\n", + [{include_path_open, PathOpenNone}, return_errors]), + + ok. + module_mismatch(Config) when is_list(Config) -> DataDir = proplists:get_value(data_dir, Config), File = filename:join(DataDir, "wrong_module_name.erl"), From 77f2c53562101a14592b1f269cb20e8abb8c23c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Fri, 13 Mar 2026 10:27:48 +0100 Subject: [PATCH 04/16] ct_doctest: Use compile:string to compile modules --- lib/common_test/src/ct_doctest.erl | 43 ++------- lib/common_test/test/ct_doctest_SUITE.erl | 15 +++- .../ct_doctest_module_preproc_mod.erl | 87 +++++++++++++++++++ 3 files changed, 105 insertions(+), 40 deletions(-) create mode 100644 lib/common_test/test/ct_doctest_SUITE_data/ct_doctest_module_preproc_mod.erl diff --git a/lib/common_test/src/ct_doctest.erl b/lib/common_test/src/ct_doctest.erl index da30c21201eb..50b5082df129 100644 --- a/lib/common_test/src/ct_doctest.erl +++ b/lib/common_test/src/ct_doctest.erl @@ -639,54 +639,25 @@ run_test(Code, InitialBindings, Verbose) -> end end, InitialBindings, Tests), [ok]; - {match, [_Line_Number, _Prefix = <<"-module(">>, _Code]} -> - [compile_string(Code)]; + {match, [_Line_Number, _Prefix = <<"-module(">>, ModContent]} -> + [ModName | _] = binary:split(ModContent, [<<")">>]), + [compile_string(Code, ModName)]; _ -> [] end end. -compile_string(Code) -> - - Toks = - case erl_scan:string(unicode:characters_to_list(Code), - 0, - [text]) of - {ok, T, _} -> - T; - {error, {Line,Mod,Reason}, _} -> - Message = io_lib:format("unknown:~p: ~ts",[Line, Mod:format_error(Reason)]), - throw({error,#{ message => Message, context => Code}}) - end, - - Forms = parse_tokens(Code, Toks), - - {attribute,_,module,ModuleName} = lists:keyfind(module, 3, Forms), - - case compile:forms(Forms, [binary, return_errors, {source, atom_to_list(ModuleName) ++ ".erl"}]) of +compile_string(Code, ModName) -> + FileName = unicode:characters_to_list(ModName) ++ ".erl", + case compile:string(Code, [binary, return_errors, {source, FileName}]) of {ok, Module, Binary} -> - {module, Module} = code:load_binary(Module, "nofile", Binary), + {module, Module} = code:load_binary(Module, FileName, Binary), ok; {error, Errors, Warnings} -> Messages = [begin [{_, M}] = sys_messages:format_messages(File, "", Msgs, []), M end || {File, Msgs} <- Errors ++ Warnings], throw({error,#{ message => Messages, context => Code}}) end. -parse_tokens(_Code, []) -> []; -parse_tokens(Code, Toks) -> - case lists:splitwith(fun(T) -> - element(1, T) =/= dot - end, Toks) of - {Ts, [{dot, _} = Dot | Rest]} -> - case erl_parse:parse_form(Ts ++ [Dot]) of - {ok, Forms} -> - [Forms | parse_tokens(Code, Rest)]; - {error, {Line, Mod, Reason}} -> - Message = io_lib:format("unknown:~p: ~ts",[Line, Mod:format_error(Reason)]), - throw({error,#{ message => Message, context => Code}}) - end - end. - check_prompt_numbers(Tests) -> check_prompt_numbers(Tests, 1). diff --git a/lib/common_test/test/ct_doctest_SUITE.erl b/lib/common_test/test/ct_doctest_SUITE.erl index 66cde049df72..0d8926865525 100644 --- a/lib/common_test/test/ct_doctest_SUITE.erl +++ b/lib/common_test/test/ct_doctest_SUITE.erl @@ -28,7 +28,8 @@ -export([api_branches/1, module_result_modes/1, docs_filtering_and_error_formatting/1, parser_prompt_parsing/1, runtime_failure_matching/1, parse_rewrite_helpers/1, file_support/1, - external_parser/1, module/1, type_and_callback_docs/1, verbose_option/1, + external_parser/1, module/1, module_preprocessor/1, + type_and_callback_docs/1, verbose_option/1, skipped_blocks_option/1, integration_smoke/1]). @@ -45,6 +46,7 @@ all() -> file_support, external_parser, module, + module_preprocessor, type_and_callback_docs, verbose_option, skipped_blocks_option, @@ -128,12 +130,17 @@ file_support(Config) -> module(_Config) -> - ExpectedSubstrings = ["unknown:2: unterminated atom starting with 'ok.\\n'", - "unknown:2: syntax error before: ok", - "test.erl:2: function f/0 undefined"], + ExpectedSubstrings = ["test.erl:3:11: unterminated atom starting with 'ok.", + "test.erl:3:7: syntax error before: ok", + "test.erl:3:2: function f/0 undefined"], expect_error_count(ct_doctest_module_mod, [], 3, ExpectedSubstrings). +module_preprocessor(_Config) -> + %% Test that module code blocks support preprocessor features: + %% ?MODULE macro, -define macros, and -record definitions. + ok = ct_doctest:module(ct_doctest_module_preproc_mod). + external_parser(Config) -> DataDir = ?config(data_dir, Config), ParserOpt = {parser, fun ct_doctest_external_parser_mod:parse_doc/1}, diff --git a/lib/common_test/test/ct_doctest_SUITE_data/ct_doctest_module_preproc_mod.erl b/lib/common_test/test/ct_doctest_SUITE_data/ct_doctest_module_preproc_mod.erl new file mode 100644 index 000000000000..7bb2ce20dd60 --- /dev/null +++ b/lib/common_test/test/ct_doctest_SUITE_data/ct_doctest_module_preproc_mod.erl @@ -0,0 +1,87 @@ +%% +%% %CopyrightBegin% +%% +%% SPDX-License-Identifier: Apache-2.0 +%% +%% Copyright Ericsson AB 2026. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +-module(ct_doctest_module_preproc_mod). +-moduledoc """ +Test that module code blocks in doctests support preprocessor features +like macros and records via epp. +""". + +-export([uses_module_macro/0, uses_define/0, uses_record/0]). + +-doc """ +Module code block using ?MODULE macro: + +```erlang +-module(macro_mod). +-export([name/0]). +name() -> ?MODULE. +``` + +```erlang +1> macro_mod:name(). +macro_mod +``` +""". +uses_module_macro() -> + ok. + +-doc """ +Module code block using -define macro: + +```erlang +-module(define_mod). +-export([value/0]). +-define(VALUE, 42). +value() -> ?VALUE. +``` + +```erlang +1> define_mod:value(). +42 +``` +""". +uses_define() -> + ok. + +-doc """ +Module code block using -record: + +```erlang +-module(record_mod). +-export([new/2, name/1, age/1]). +-record(person, {name, age}). +new(Name, Age) -> #person{name = Name, age = Age}. +name(#person{name = N}) -> N. +age(#person{age = A}) -> A. +``` + +```erlang +1> P = record_mod:new(alice, 30). +2> record_mod:name(P). +alice +3> record_mod:age(P). +30 +``` +""". +uses_record() -> + ok. From 42a7030ba2967434212ca53cbe988928b02cbe75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Fri, 13 Mar 2026 15:01:54 +0100 Subject: [PATCH 05/16] ct_doctest: Refactor options handling to use a record --- lib/common_test/src/ct_doctest.erl | 125 +++++++++++++++-------------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/lib/common_test/src/ct_doctest.erl b/lib/common_test/src/ct_doctest.erl index 50b5082df129..db018b0ee7f1 100644 --- a/lib/common_test/src/ct_doctest.erl +++ b/lib/common_test/src/ct_doctest.erl @@ -315,6 +315,11 @@ Options for doctest execution. {skipped_blocks, non_neg_integer() | false} | {verbose, boolean()}]. +-record(options, + { parser = fun parse_markdown_builtin/1 :: fun((unicode:unicode_binary()) -> [unicode:unicode_binary()] | {error, term()}), + skipped_blocks = false :: non_neg_integer() | false, + verbose = false :: boolean() }). + -doc #{equiv => module(Module, [])}. -spec module(module()) -> ok | {comment, string()} | {error, term()} | no_return(). @@ -344,21 +349,19 @@ Use `Bindings` to provide prebound variables for a specific doc entry. Use See `t:options/0` for available options. """. --spec module(module(), Bindings, options()) -> +-spec module(Module :: module(), Bindings, Options :: options()) -> ok | {comment, string()} | {error, term()} | no_return() when KFA :: {Kind :: function | type | callback, atom(), arity()}, Bindings :: [{KFA | moduledoc, erl_eval:binding_struct()}]. -module(Module, Bindings, Options) -> - HasParserKey = proplists:is_defined(parser, Options), - ParserFun = options_parser(Options), - ExpectedSkipped = options_skipped_blocks(Options), - Verbose = options_verbose(Options), +module(Module, Bindings, OptionsList) -> + Options = options(OptionsList), + HasParserKey = proplists:is_defined(parser, OptionsList), case code:get_doc(Module) of {ok, #docs_v1{ format = ~"text/markdown" } = Docs} when not HasParserKey -> - run_module_docs(Docs, Bindings, ParserFun, ExpectedSkipped, Verbose); + run_module_docs(Docs, Bindings, Options); {ok, #docs_v1{} = Docs} when HasParserKey -> - run_module_docs(Docs, Bindings, ParserFun, ExpectedSkipped, Verbose); + run_module_docs(Docs, Bindings, Options); {ok, _} -> {error, unsupported_format}; Else -> @@ -392,19 +395,17 @@ code blocks to be tested. See `t:options/0` for available options. """. --spec file(file:filename(), Bindings :: [{atom(), term()}], options()) -> +-spec file(File :: file:filename(), Bindings :: [{atom(), term()}], Options :: options()) -> ok | {comment, string()} | {error, term()} | no_return(). -file(File, Bindings, Options) -> - ParserFun = options_parser(Options), - ExpectedSkipped = options_skipped_blocks(Options), - Verbose = options_verbose(Options), +file(File, Bindings, OptionsList) -> + Options = options(OptionsList), case file:read_file(File) of {ok, Content} -> try - Blocks = inspect(parse(Content, ParserFun)), + Blocks = inspect(parse(Content, Options#options.parser)), {_RunResult, Skipped} = run_blocks(Blocks, Bindings, - {file, File}, Verbose), - ensure_skipped_blocks(ExpectedSkipped, Skipped), + {file, File}, Options), + ensure_skipped_blocks(Options#options.skipped_blocks, Skipped), ok catch throw:{error, Error} -> @@ -419,11 +420,11 @@ file(File, Bindings, Options) -> end. run_module_docs(#docs_v1{ docs = Docs, module_doc = MD }, - Bindings, ParserFun, ExpectedSkipped, Verbose) -> - MDRes = parse_and_run(moduledoc, MD, Bindings, ParserFun, Verbose), + Bindings, Options) -> + MDRes = parse_and_run(moduledoc, MD, Bindings, Options), Res = lists:append( - [parse_and_run(KFA, EntryDocs, Bindings, ParserFun, Verbose) || + [parse_and_run(KFA, EntryDocs, Bindings, Options) || {KFA, _Anno, _Sig, EntryDocs, _Meta} <- Docs, is_map(EntryDocs)]), Errors = @@ -433,10 +434,10 @@ run_module_docs(#docs_v1{ docs = Docs, module_doc = MD }, case length(Errors) of 0 -> Skipped = lists:sum([Count || {_, _, Count} <- MDRes ++ Res]), - verbose_log(Verbose, + verbose_log(Options, "module complete; total skipped blocks: ~p (expected ~p)", - [Skipped, ExpectedSkipped]), - ensure_skipped_blocks(ExpectedSkipped, Skipped), + [Skipped, Options#options.skipped_blocks]), + ensure_skipped_blocks(Options#options.skipped_blocks, Skipped), NoTests = lists:sort([io_lib:format(" ~p/~p\n", [F,A]) || {{function,F,A},[],_} <- Res]), case length(NoTests) of @@ -475,17 +476,17 @@ format_error_context(#{ message := Message, context := Context }) -> format_error_context(#{ message := Message }) -> io_lib:format("~ts~n", [string:trim(Message)]). -parse_and_run(_, hidden, _, _, _) -> []; -parse_and_run(_, none, _, _, _) -> []; -parse_and_run(KFA, #{} = Ds, Bindings, ParserFun, Verbose) -> - [do_parse_and_run(KFA, D, Bindings, ParserFun, Verbose) || _ := D <- Ds]. +parse_and_run(_, hidden, _, _) -> []; +parse_and_run(_, none, _, _) -> []; +parse_and_run(KFA, #{} = Ds, Bindings, Options) -> + [do_parse_and_run(KFA, D, Bindings, Options) || _ := D <- Ds]. -do_parse_and_run(KFA, Docs, Bindings, ParserFun, Verbose) -> +do_parse_and_run(KFA, Docs, Bindings, Options) -> try InitialBindings = proplists:get_value(KFA, Bindings, []), - Blocks = inspect(parse(Docs, ParserFun)), + Blocks = inspect(parse(Docs, Options#options.parser)), {RunResult, Skipped} = run_blocks(Blocks, InitialBindings, - {module, KFA}, Verbose), + {module, KFA}, Options), {KFA, RunResult, Skipped} catch throw:{error,_}=Error -> @@ -495,42 +496,42 @@ do_parse_and_run(KFA, Docs, Bindings, ParserFun, Verbose) -> erlang:raise(C, R, ST) end. -run_blocks(Blocks, Bindings, Context, Verbose) -> +run_blocks(Blocks, Bindings, Context, Options) -> {_Index, Result} = lists:foldl(fun(Test, {Index, {Acc, Skipped}}) -> {Result0, NewSkipped} = test_block(Test, Bindings, - Context, Index, Skipped, Verbose), + Context, Index, Skipped, Options), {Index + 1, {Acc ++ Result0, NewSkipped}} end, {1, {[], 0}}, Blocks), Result. -test_block(Code, Bindings, Context, Index, Skipped, Verbose) when is_binary(Code) -> +test_block(Code, Bindings, Context, Index, Skipped, Options) when is_binary(Code) -> ContextLabel = context_label(Context), FirstLines = first_lines(Code), - verbose_log(Verbose, "running block ~p in ~ts:~n~ts", + verbose_log(Options, "running block ~p in ~ts:~n~ts", [Index, ContextLabel, Code]), - try run_test(Code, Bindings, Verbose) of + try run_test(Code, Bindings, Options) of [] -> - verbose_log(Verbose, "skipped block ~p in ~ts (no runnable prompt, ~p skipped): ~ts", + verbose_log(Options, "skipped block ~p in ~ts (no runnable prompt, ~p skipped): ~ts", [Index, ContextLabel, Skipped + 1, FirstLines]), {[], Skipped + 1}; Result -> - verbose_log(Verbose, "passed block ~p in ~ts", [Index, ContextLabel]), + verbose_log(Options, "passed block ~p in ~ts", [Index, ContextLabel]), {Result, Skipped} catch throw:{error, ErrorContext} = Error -> - verbose_log(Verbose, + verbose_log(Options, "failed block ~p in ~ts:~n~ts~n", [Index, ContextLabel, format_error_context(ErrorContext)]), throw(Error); C:R:ST -> - verbose_log(Verbose, + verbose_log(Options, "failed block ~p in ~ts with ~p:~tp~nblock snippet:~n~ts", [Index, ContextLabel, C, R, Code]), erlang:raise(C, R, ST) end; -test_block(Other, _Bindings, _Context, _Index, _Skipped, _Verbose) -> +test_block(Other, _Bindings, _Context, _Index, _Skipped, _Options) -> throw({error, {invalid_code_block, Other}}). context_label({module, moduledoc}) -> @@ -546,12 +547,12 @@ first_lines(Code) -> Lines = string:split(Code, "\n", all), lists:join($\n, lists:sublist(Lines, 5)). -verbose_log(false, _Fmt, _Args) -> - ok; -verbose_log(true, Fmt, Args) -> +verbose_log(#options{ verbose = true }, Fmt, Args) -> Str = io_lib:format("ct_doctest(verbose): " ++ Fmt, Args), [First | Rest] = string:split(string:trim(Str), "\n", all), - io:put_chars([First, [["\n ", Line] || Line <- Rest], "\n"]). + io:put_chars([First, [["\n ", Line] || Line <- Rest], "\n"]); +verbose_log(_, _, _) -> + ok. parse(Content, ParserFun) -> validate_code_blocks(run_parser(ParserFun, Content)). @@ -576,9 +577,17 @@ validate_code_block(Block) when is_binary(Block) -> validate_code_block(Other) -> throw({error, #{ message => io_lib:format("Invalid code block: ~p.", [Other]) }}). -options_parser(Options) -> - proplists:get_value(parser, Options, - fun parse_markdown_builtin/1). +options(OptionsList) -> + lists:foldl(fun + (parser, Acc) -> + Acc#options{ parser = proplists:get_value(parser, OptionsList) }; + (skipped_blocks, Acc) -> + Acc#options{ skipped_blocks = proplists:get_value(skipped_blocks, OptionsList) }; + (verbose, Acc) -> + Acc#options{ verbose = proplists:get_value(verbose, OptionsList) }; + (_Key, Acc) -> + Acc + end, #options{}, proplists:get_keys(OptionsList)). parse_markdown_builtin(Markdown) -> extract_erlang_code_blocks(inspect(shell_docs_markdown:parse_md(Markdown))). @@ -599,12 +608,6 @@ extract_erlang_code_blocks({_Tag, _Attrs, Content}) -> extract_erlang_code_blocks(_Other) -> []. -options_skipped_blocks(Options) -> - proplists:get_value(skipped_blocks, Options, false). - -options_verbose(Options) -> - proplists:get_value(verbose, Options, false) =:= true. - ensure_skipped_blocks(false, _Actual) -> ok; ensure_skipped_blocks(Expected, Actual) when is_integer(Expected), Expected >= 0 -> @@ -618,7 +621,7 @@ ensure_skipped_blocks(Expected, Actual) when is_integer(Expected), Expected >= 0 -define(RE_CAPTURE, ~B"(?:(?'line_number'[0-9]+)(?'prefix'>\s)|(?'prefix'\-module\())?(?'content'.*)"). -define(RE_OPTIONS, [{capture, [line_number, prefix, content], binary}, dupnames, unicode]). -run_test(Code, InitialBindings, Verbose) -> +run_test(Code, InitialBindings, Options) -> Lines = string:split(Code, "\n", all), CollapsedComments = [re:replace(Line, ~B"^\s*%.*$", <<"">>, [global, unicode]) || Line <- Lines], case lists:search(fun(Line) -> @@ -633,7 +636,7 @@ run_test(Code, InitialBindings, Verbose) -> Tests = inspect(parse_tests(ReLines, [], 1)), check_prompt_numbers(Tests), _ = lists:foldl(fun(Test, Bindings) -> - try run_tests(Test, Bindings, Verbose) + try run_tests(Test, Bindings, Options) catch throw:{error, Error} -> throw({error, Error#{ test => Test}}) end @@ -697,19 +700,19 @@ parse_match([{match, [<<>>, <<>>, <<" ", _/binary>> = More]} | T], Acc) -> parse_match(Rest, Acc) -> {Acc, Rest}. -run_tests({test, _Index, Test0, Match0}, Bindings, Verbose) -> +run_tests({test, _Index, Test0, Match0}, Bindings, Options) -> Test1 = unicode:characters_to_list(Test0), Test = string:trim(string:trim(Test1), trailing, "."), case Match0 of [<<"** ", _/binary>> | _] -> Match = unicode:characters_to_list(Match0), - run_failing(Test, Match, Bindings, Verbose); + run_failing(Test, Match, Bindings, Options); _ -> - run_successful(Test, Match0, Bindings, Verbose) + run_successful(Test, Match0, Bindings, Options) end. -run_successful(Test, Match, Bindings, Verbose) -> - verbose_log(Verbose, "Running: ~ts = ~ts", [Match, Test]), +run_successful(Test, Match, Bindings, Options) -> + verbose_log(Options, "Running: ~ts = ~ts", [Match, Test]), Ast = parse_exprs(Test, Match), try {value, _Res, NewBindings} = inspect(erl_eval:exprs(Ast, Bindings)), @@ -718,8 +721,8 @@ run_successful(Test, Match, Bindings, Verbose) -> throw({error,#{ message => format_exception(C, R, ST) }}) end. -run_failing(Test, Match, Bindings, Verbose) -> - verbose_log(Verbose, "Running: ~ts", [Test]), +run_failing(Test, Match, Bindings, Options) -> + verbose_log(Options, "Running: ~ts", [Test]), Ast = parse_exprs(Test, "_"), try inspect(erl_eval:exprs(Ast, Bindings)) of {value, Res, _} -> From 186eae06e3bffe3aa8cb02ad7270637cfa522731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Fri, 13 Mar 2026 15:02:18 +0100 Subject: [PATCH 06/16] ct_doctest: Add option to pass compile options --- lib/common_test/src/ct_doctest.erl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/common_test/src/ct_doctest.erl b/lib/common_test/src/ct_doctest.erl index db018b0ee7f1..3ada867ec65c 100644 --- a/lib/common_test/src/ct_doctest.erl +++ b/lib/common_test/src/ct_doctest.erl @@ -313,12 +313,14 @@ Options for doctest execution. """. -type options() :: [{parser, fun((unicode:unicode_binary()) -> [unicode:unicode_binary()] | {error, term()}) } | {skipped_blocks, non_neg_integer() | false} | - {verbose, boolean()}]. + {verbose, boolean()} | + {compile_options, [compile:option()]}]. -record(options, { parser = fun parse_markdown_builtin/1 :: fun((unicode:unicode_binary()) -> [unicode:unicode_binary()] | {error, term()}), skipped_blocks = false :: non_neg_integer() | false, - verbose = false :: boolean() }). + verbose = false :: boolean(), + compile_options = [] :: [compile:option()] }). -doc #{equiv => module(Module, [])}. -spec module(module()) -> @@ -585,6 +587,8 @@ options(OptionsList) -> Acc#options{ skipped_blocks = proplists:get_value(skipped_blocks, OptionsList) }; (verbose, Acc) -> Acc#options{ verbose = proplists:get_value(verbose, OptionsList) }; + (compile_options, Acc) -> + Acc#options{ compile_options = proplists:get_value(compile_options, OptionsList) }; (_Key, Acc) -> Acc end, #options{}, proplists:get_keys(OptionsList)). @@ -644,15 +648,15 @@ run_test(Code, InitialBindings, Options) -> [ok]; {match, [_Line_Number, _Prefix = <<"-module(">>, ModContent]} -> [ModName | _] = binary:split(ModContent, [<<")">>]), - [compile_string(Code, ModName)]; + [compile_string(Code, ModName, Options#options.compile_options)]; _ -> [] end end. -compile_string(Code, ModName) -> +compile_string(Code, ModName, CompileOptions) -> FileName = unicode:characters_to_list(ModName) ++ ".erl", - case compile:string(Code, [binary, return_errors, {source, FileName}]) of + case compile:string(Code, [binary, return_errors, {source, FileName} | CompileOptions]) of {ok, Module, Binary} -> {module, Module} = code:load_binary(Module, FileName, Binary), ok; From c9118aa6476439d5746c5b0e5d07a429e38be4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Thu, 12 Mar 2026 14:26:07 +0100 Subject: [PATCH 07/16] erts: Add doctests to counters --- erts/emulator/test/counters_SUITE.erl | 7 +- erts/preloaded/src/counters.erl | 137 +++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 6 deletions(-) diff --git a/erts/emulator/test/counters_SUITE.erl b/erts/emulator/test/counters_SUITE.erl index ab3b2184ad2c..f921e44c4aaf 100644 --- a/erts/emulator/test/counters_SUITE.erl +++ b/erts/emulator/test/counters_SUITE.erl @@ -25,12 +25,12 @@ -export([suite/0, all/0]). -export([basic/1, bad/1, limits/1, indep/1, write_concurrency/1, - error_info/1]). + error_info/1, doctests/1]). suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> - [basic, bad, limits, indep, write_concurrency, error_info]. + [basic, bad, limits, indep, write_concurrency, error_info, doctests]. basic(Config) when is_list(Config) -> Size = 10, @@ -240,6 +240,9 @@ rand_log64() -> 2 -> Uint end. +doctests(_Config) -> + ct_doctest:module(counters). + error_info(_Config) -> Counters = counters:new(10, [write_concurrency]), Huge = 1 bsl 64, diff --git a/erts/preloaded/src/counters.erl b/erts/preloaded/src/counters.erl index 06c77fc89dd5..f432026bc23f 100644 --- a/erts/preloaded/src/counters.erl +++ b/erts/preloaded/src/counters.erl @@ -43,6 +43,39 @@ organized into arrays with the following semantics: inconsistencies. See `new/2`. - Indexes into counter arrays are one-based. A counter array of size N contains N counters with index from 1 to N. + +## Examples + +```erlang +1> Ref = counters:new(3, []). +2> counters:add(Ref, 1, 10). +ok +3> counters:get(Ref, 1). +10 +4> counters:sub(Ref, 1, 3). +ok +5> counters:get(Ref, 1). +7 +6> counters:put(Ref, 1, 42). +ok +7> counters:get(Ref, 1). +42 +8> counters:info(Ref). +#{size => 3, memory => ...} +``` + +Counters can be updated concurrently from multiple processes: + +```erlang +1> Ref = counters:new(1, []). +2> Self = self(). +3> N = 1000. +1000 +4> Pids = [spawn(fun() -> counters:add(Ref, 1, 1), Self ! self() end) || _ <- lists:seq(1, N)]. +5> [receive Pid -> ok end || Pid <- Pids]. +6> counters:get(Ref, 1). +1000 +``` """. -moduledoc(#{since => "OTP 21.2"}). @@ -91,6 +124,19 @@ Argument `Opts` is a list of the following possible options: Counters are not tied to the current process and are automatically garbage collected when they are no longer referenced. + +## Examples + +```erlang +1> counters:new(5, []). +2> counters:new(5, [write_concurrency]). +3> counters:new(5, [atomics]). +4> counters:new(5, [atomics, write_concurrency]). +** exception error: bad argument + in function counters:new/2 + called as counters:new(5,[atomics,write_concurrency]) + *** argument 2: invalid option in list +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec new(Size, Opts) -> counters_ref() when @@ -119,7 +165,21 @@ new(Size, Options) -> end. --doc "Read counter value.". +-doc """ +Read counter value. + +## Examples + +```erlang +1> Ref = counters:new(2, []). +2> counters:get(Ref, 1). +0 +3> counters:add(Ref, 1, 100). +ok +4> counters:get(Ref, 1). +100 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec get(Ref, Ix) -> integer() when Ref :: counters_ref(), @@ -140,7 +200,36 @@ get(Ref, Ix) -> end. --doc "Add `Incr` to counter at index `Ix`.". +-doc """ +Add `Incr` to counter at index `Ix`. + +## Examples + +```erlang +1> Ref = counters:new(1, []). +2> counters:add(Ref, 1, 5). +ok +3> counters:add(Ref, 1, 3). +ok +4> counters:get(Ref, 1). +8 +5> counters:add(Ref, 1, -10). +ok +6> counters:get(Ref, 1). +-2 +7> counters:add(Ref, 1, 1 bsl 63 - 1). %% Add max positive signed 64 bit integer. +ok +8> counters:get(Ref, 1). +9223372036854775805 +9> counters:add(Ref, 1, 4). %% Add 4 to cause overflow. +ok +10> counters:get(Ref, 1). %% Counter wraps around at overflow. +-9223372036854775807 +11> counters:add(Ref, 1, 1 bsl 64). +** exception error: bad argument + in function counters:add/3 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec add(Ref, Ix, Incr) -> ok when Ref :: counters_ref(), @@ -162,7 +251,25 @@ add(Ref, Ix, Incr) -> end. --doc "Subtract `Decr` from counter at index `Ix`.". +-doc """ +Subtract `Decr` from counter at index `Ix`. + +## Examples + +```erlang +1> Ref = counters:new(1, []). +2> counters:put(Ref, 1, 10). +ok +3> counters:sub(Ref, 1, 3). +ok +4> counters:get(Ref, 1). +7 +5> counters:sub(Ref, 1, 7). +ok +6> counters:get(Ref, 1). +0 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec sub(Ref, Ix, Decr) -> ok when Ref :: counters_ref(), @@ -193,8 +300,22 @@ Write `Value` to counter at index `Ix`. > Despite its name, the `write_concurrency` optimization does not improve `put`. > A call to `put` is a relatively heavy operation compared to the very > lightweight and scalable [`add`](`add/3`) and [`sub`](`sub/3`). The cost for a -> `put` with `write_concurrency` is like a [`get` ](`get/2`)plus a `put` without +> `put` with `write_concurrency` is like a [`get` ](`get/2`) plus a `put` without > `write_concurrency`. + +## Examples + +```erlang +1> Ref = counters:new(1, []). +2> counters:put(Ref, 1, 42). +ok +3> counters:get(Ref, 1). +42 +4> counters:put(Ref, 1, -10). +ok +5> counters:get(Ref, 1). +-10 +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec put(Ref, Ix, Value) -> ok when @@ -224,6 +345,14 @@ The map has the following keys (at least): - **`size`** - The number of counters in the array. - **`memory`** - Approximate memory consumption for the array in bytes. + +## Examples + +```erlang +1> Ref = counters:new(10, []). +2> counters:info(Ref). +#{size => 10, memory => ...} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec info(Ref) -> Info when From 4636aa9ee0972901fb854846cadbeabbba86d59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Thu, 12 Mar 2026 14:54:38 +0100 Subject: [PATCH 08/16] erts: Add doctests to atomics --- erts/emulator/test/atomics_SUITE.erl | 7 +- erts/preloaded/src/atomics.erl | 181 ++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 8 deletions(-) diff --git a/erts/emulator/test/atomics_SUITE.erl b/erts/emulator/test/atomics_SUITE.erl index be39246a4b93..d3560d5c9699 100644 --- a/erts/emulator/test/atomics_SUITE.erl +++ b/erts/emulator/test/atomics_SUITE.erl @@ -22,7 +22,7 @@ -module(atomics_SUITE). -export([suite/0, all/0, signed/1, unsigned/1, bad/1, signed_limits/1, unsigned_limits/1, - error_info/1]). + error_info/1, doctests/1]). -include_lib("common_test/include/ct.hrl"). @@ -30,7 +30,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [signed, unsigned, bad, signed_limits, unsigned_limits, - error_info]. + error_info, doctests]. signed(Config) when is_list(Config) -> Size = 10, @@ -157,6 +157,9 @@ max_atomic_sz() -> end end. +doctests(_Config) -> + ct_doctest:module(atomics). + error_info(_Config) -> Atomics = atomics:new(10, []), Huge = 1 bsl 64, diff --git a/erts/preloaded/src/atomics.erl b/erts/preloaded/src/atomics.erl index 729ab6d36555..64cc8f7ee43b 100644 --- a/erts/preloaded/src/atomics.erl +++ b/erts/preloaded/src/atomics.erl @@ -41,6 +41,41 @@ access. The atomics are organized into arrays with the following semantics: can read the new value of B and then read the old value of A. - Indexes into atomic arrays are one-based. An atomic array of arity N contains N atomics with index from 1 to N. + +## Examples + +```erlang +1> Ref = atomics:new(3, [{signed, true}]). +2> atomics:put(Ref, 1, 10). +ok +3> atomics:get(Ref, 1). +10 +4> atomics:add(Ref, 1, 5). +ok +5> atomics:get(Ref, 1). +15 +6> atomics:exchange(Ref, 1, 42). +15 +7> atomics:compare_exchange(Ref, 1, 42, 100). +ok +8> atomics:get(Ref, 1). +100 +9> atomics:info(Ref). +#{size => 3, max => ..., min => ..., memory => ...} +``` + +Atomics can be updated concurrently from multiple processes: + +```erlang +1> Ref = atomics:new(1, []). +2> Self = self(). +3> N = 1000. +1000 +4> Pids = [spawn(fun() -> atomics:add(Ref, 1, 1), Self ! self() end) || _ <- lists:seq(1, N)]. +5> [receive Pid -> ok end || Pid <- Pids]. +6> atomics:get(Ref, 1). +1000 +``` """. -moduledoc(#{since => "OTP 21.2"}). @@ -77,6 +112,14 @@ Argument `Opts` is a list of the following possible options: Atomics are not tied to the current process and are automatically garbage collected when they are no longer referenced. + +## Examples + +```erlang +1> atomics:new(5, []). +2> atomics:new(5, [{signed, false}]). +3> atomics:new(5, [{signed, true}]). +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec new(Arity, Opts) -> atomics_ref() when @@ -105,7 +148,23 @@ encode_opts([], Acc) -> encode_opts(_, _) -> throw(badopt). --doc "Set atomic to `Value`.". +-doc """ +Set atomic to `Value`. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 42). +ok +3> atomics:get(Ref, 1). +42 +4> atomics:put(Ref, 1, -10). +ok +5> atomics:get(Ref, 1). +-10 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec put(Ref, Ix, Value) -> ok when Ref :: atomics_ref(), @@ -114,7 +173,21 @@ encode_opts(_, _) -> put(_Ref, _Ix, _Value) -> erlang:nif_error(undef). --doc "Read atomic value.". +-doc """ +Read atomic value. + +## Examples + +```erlang +1> Ref = atomics:new(2, []). +2> atomics:get(Ref, 1). +0 +3> atomics:put(Ref, 1, 100). +ok +4> atomics:get(Ref, 1). +100 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec get(Ref, Ix) -> integer() when Ref :: atomics_ref(), @@ -122,7 +195,25 @@ put(_Ref, _Ix, _Value) -> get(_Ref, _Ix) -> erlang:nif_error(undef). --doc "Add `Incr` to atomic.". +-doc """ +Add `Incr` to atomic. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:add(Ref, 1, 5). +ok +3> atomics:add(Ref, 1, 3). +ok +4> atomics:get(Ref, 1). +8 +5> atomics:add(Ref, 1, -2). +ok +6> atomics:get(Ref, 1). +6 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec add(Ref, Ix, Incr) -> ok when Ref :: atomics_ref(), @@ -131,7 +222,21 @@ get(_Ref, _Ix) -> add(_Ref, _Ix, _Incr) -> erlang:nif_error(undef). --doc "Atomically add `Incr` to atomic and return the result.". +-doc """ +Atomically add `Incr` to atomic and return the result. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:add_get(Ref, 1, 5). +5 +3> atomics:add_get(Ref, 1, 10). +15 +4> atomics:add_get(Ref, 1, -3). +12 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec add_get(Ref, Ix, Incr) -> integer() when Ref :: atomics_ref(), @@ -140,7 +245,21 @@ add(_Ref, _Ix, _Incr) -> add_get(_Ref, _Ix, _Incr) -> erlang:nif_error(undef). --doc "Subtract `Decr` from atomic.". +-doc """ +Subtract `Decr` from atomic. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 10). +ok +3> atomics:sub(Ref, 1, 3). +ok +4> atomics:get(Ref, 1). +7 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec sub(Ref, Ix, Decr) -> ok when Ref :: atomics_ref(), @@ -154,7 +273,21 @@ sub(Ref, Ix, Decr) -> error_with_info(Error, [Ref, Ix, Decr]) end. --doc "Atomically subtract `Decr` from atomic and return the result.". +-doc """ +Atomically subtract `Decr` from atomic and return the result. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 20). +ok +3> atomics:sub_get(Ref, 1, 5). +15 +4> atomics:sub_get(Ref, 1, 10). +5 +``` +""". -doc(#{since => <<"OTP 21.2">>}). -spec sub_get(Ref, Ix, Decr) -> integer() when Ref :: atomics_ref(), @@ -170,6 +303,18 @@ sub_get(Ref, Ix, Decr) -> -doc """ Atomically replace the value of the atomic with `Desired` and return the previous value. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 42). +ok +3> atomics:exchange(Ref, 1, 100). +42 +4> atomics:get(Ref, 1). +100 +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec exchange(Ref, Ix, Desired) -> integer() when @@ -185,6 +330,22 @@ atomic to `Desired`. Return `ok` if `Desired` was written. Return the actual atomic value if not equal to `Expected`. + +## Examples + +```erlang +1> Ref = atomics:new(1, []). +2> atomics:put(Ref, 1, 42). +ok +3> atomics:compare_exchange(Ref, 1, 42, 100). +ok +4> atomics:get(Ref, 1). +100 +5> atomics:compare_exchange(Ref, 1, 42, 200). +100 +6> atomics:get(Ref, 1). +100 +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec compare_exchange(Ref, Ix, Expected, Desired) -> ok | integer() when @@ -204,6 +365,14 @@ The map has the following keys: - **`max`** - The highest possible value an atomic in this array can hold. - **`min`** - The lowest possible value an atomic in this array can hold. - **`memory`** - Approximate memory consumption for the array in bytes. + +## Examples + +```erlang +1> Ref = atomics:new(10, []). +2> atomics:info(Ref). +#{size => 10, max => ..., min => ..., memory => ...} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec info(Ref) -> Info when From 65916828a8f1c7cb83c677191ab8c850f03dd5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Thu, 12 Mar 2026 15:14:18 +0100 Subject: [PATCH 09/16] erts: Add doctests to persistent_term --- erts/emulator/test/persistent_term_SUITE.erl | 14 ++- erts/preloaded/src/persistent_term.erl | 107 ++++++++++++++++++- 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/erts/emulator/test/persistent_term_SUITE.erl b/erts/emulator/test/persistent_term_SUITE.erl index 019a429f6f2f..532bb153a93c 100644 --- a/erts/emulator/test/persistent_term_SUITE.erl +++ b/erts/emulator/test/persistent_term_SUITE.erl @@ -39,7 +39,8 @@ shared_magic_ref/1, non_message_signal/1, get_put_colliding_bucket/1, - gc_binary_orig/1]). + gc_binary_orig/1, + doctests/1]). %% -export([test_init_restart_cmd/1]). @@ -60,7 +61,8 @@ all() -> shared_magic_ref, non_message_signal, get_put_colliding_bucket, - gc_binary_orig]. + gc_binary_orig, + doctests]. init_per_suite(Config) -> erts_debug:set_internal_state(available_internal_state, true), @@ -1266,3 +1268,11 @@ gpcb_updater(CollidesWith) -> persistent_term:erase(CollidesWith), persistent_term:put(CollidesWith, unexpected), gpcb_updater(CollidesWith). + +doctests(_Config) -> + Keys = [scheduler_ets, my_key, foo, bar, {my_app, config}], + try + ct_doctest:module(persistent_term) + after + [persistent_term:erase(K) || K <- Keys] + end. diff --git a/erts/preloaded/src/persistent_term.erl b/erts/preloaded/src/persistent_term.erl index e886c8498474..a8aadb727588 100644 --- a/erts/preloaded/src/persistent_term.erl +++ b/erts/preloaded/src/persistent_term.erl @@ -83,7 +83,7 @@ Here is an example how the reserved virtual address space for literals can be raised to 2 GB (2048 MB): ```text - erl +MIscs 2048 +$ erl +MIscs 2048 ``` ## Best Practices for Using Persistent Terms @@ -124,17 +124,35 @@ the system less responsive N times longer than deleting a single persistent term. Therefore, terms that are to be updated at the same time should be collected into a larger term, for example, a map or a tuple. -## Example +## Examples The following example shows how lock contention for ETS tables can be minimized by having one ETS table for each scheduler. The table identifiers for the ETS tables are stored as a single persistent term: ```erlang - %% There is one ETS table for each scheduler. +-module(scheduler_ets). +-export([init/0, update_counter/2]). + +init() -> + Tids = list_to_tuple( + [ets:new(table, [public]) || + _ <- lists:seq(1, erlang:system_info(schedulers))]), + persistent_term:put(?MODULE, Tids). + +update_counter(Key, Incr) -> Sid = erlang:system_info(scheduler_id), Tid = element(Sid, persistent_term:get(?MODULE)), - ets:update_counter(Tid, Key, 1). + ets:update_counter(Tid, Key, Incr, {Key, 0}). +``` + +```erlang +1> scheduler_ets:init(). +ok +2> scheduler_ets:update_counter(my_counter, 1). +1 +3> scheduler_ets:update_counter(my_counter, 5). +6 ``` """. -moduledoc(#{since => "OTP 21.2"}). @@ -155,6 +173,17 @@ and `false` if there was no persistent term associated with the key. If there existed a previous persistent term associated with key `Key`, a global GC has been initiated when [`erase/1`](`erase/1`) returns. See [Description](`m:persistent_term`). + +## Examples + +```erlang +1> persistent_term:put(my_key, hello). +ok +2> persistent_term:erase(my_key). +true +3> persistent_term:erase(my_key). +false +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec erase(Key) -> Result when @@ -168,6 +197,20 @@ Retrieve the keys and values for all persistent terms. The keys will be copied to the heap for the process calling `get/0`, but the values will not. + +## Examples + +```erlang +1> persistent_term:put(foo, 1). +ok +2> persistent_term:put(bar, 2). +ok +3> List = persistent_term:get(). +4> {foo, 1} = lists:keyfind(foo, 1, List). +{foo,1} +5> {bar, 2} = lists:keyfind(bar, 1, List). +{bar,2} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec get() -> List when @@ -187,6 +230,22 @@ the key `Key`. If the calling process holds on to the value of the persistent term and the persistent term is deleted in the future, the term will be copied to the process. + +## Examples + +```erlang +1> persistent_term:put(my_key, #{data => [1, 2, 3]}). +ok +2> persistent_term:get(my_key). +#{data => [1,2,3]} +3> persistent_term:erase(my_key). +true +4> persistent_term:get(my_key). +** exception error: bad argument + in function persistent_term:get/1 + called as persistent_term:get(my_key) + *** argument 1: no persistent term stored with this key +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec get(Key) -> Value when @@ -206,6 +265,17 @@ This function returns `Default` if no term has been stored with the key `Key`. If the calling process holds on to the value of the persistent term and the persistent term is deleted in the future, the term will be copied to the process. + +## Examples + +```erlang +1> persistent_term:put(my_key, hello). +ok +2> persistent_term:get(my_key, default). +hello +3> persistent_term:get(nonexistent_key, default). +default +``` """. -doc(#{since => <<"OTP 21.3">>}). -spec get(Key, Default) -> Value when @@ -224,6 +294,13 @@ The map has the following keys: - **`memory`** - The total amount of memory (measured in bytes) used by all persistent terms. + +## Examples + +```erlang +1> persistent_term:info(). +#{count => ..., memory => ...} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec info() -> Info when @@ -243,6 +320,15 @@ If the value `Value` is equal to the value previously stored for the key, If there existed a previous persistent term associated with key `Key`, a global GC has been initiated when [`put/2`](`put/2`) returns. See [Description](`m:persistent_term`). + +## Examples + +```erlang +1> persistent_term:put({my_app, config}, #{timeout => 5000, retries => 3}). +ok +2> persistent_term:get({my_app, config}). +#{retries => 3,timeout => 5000} +``` """. -doc(#{since => <<"OTP 21.2">>}). -spec put(Key, Value) -> 'ok' when @@ -260,6 +346,19 @@ If the value `Value` is equal to the value previously stored for the key, If there existed a previous persistent term associated with key `Key`, the function fails with a `badarg` exception. + +## Examples + +```erlang +1> persistent_term:put_new(my_key, first). +ok +2> persistent_term:put_new(my_key, second). +** exception error: bad argument + in function persistent_term:put_new/2 + called as persistent_term:put_new(my_key,second) +3> persistent_term:get(my_key). +first +``` """. -doc(#{since => <<"OTP 28.4">>}). -spec put_new(Key, Value) -> 'ok' when From c5e28a49bd53d8333c0a89e8e118ebe46a507e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Fri, 13 Mar 2026 08:59:07 +0100 Subject: [PATCH 10/16] kernel: Add doctests to file --- lib/kernel/src/file.erl | 518 ++++++++++++++++++++++++++++++++- lib/kernel/test/file_SUITE.erl | 58 +++- 2 files changed, 564 insertions(+), 12 deletions(-) diff --git a/lib/kernel/src/file.erl b/lib/kernel/src/file.erl index deaa1180ba9e..857fe5af39b7 100644 --- a/lib/kernel/src/file.erl +++ b/lib/kernel/src/file.erl @@ -347,6 +347,14 @@ runtime libraries of most C compilers. Returns the filename encoding mode. If it is `latin1`, the system translates no filenames. If it is `utf8`, filenames are converted back and forth to the native filename encoding (usually UTF-8, but UTF-16 on Windows). + +## Examples + +```erlang +1> Enc = file:native_name_encoding(). +2> is_atom(Enc). +true +``` """. -doc(#{since => <<"OTP R14B01">>}). -spec native_name_encoding() -> latin1 | utf8. @@ -363,6 +371,17 @@ native_name_encoding() -> -doc """ Given the error reason returned by any function in this module, returns a descriptive string of the error in English. + +## Examples + +```erlang +1> file:format_error(enoent). +"no such file or directory" +2> file:format_error(eacces). +"permission denied" +3> file:format_error(eisdir). +"illegal operation on a directory" +``` """. -spec format_error(Reason) -> Chars when Reason :: posix() | badarg | terminated | system_limit @@ -405,6 +424,12 @@ A typical error reason: - **`eacces`** - Missing read permission for one of the parents of the current directory. + +## Examples + +```erlang +1> {ok, Dir} = file:get_cwd(). +``` """. -spec get_cwd() -> {ok, Dir} | {error, Reason} when Dir :: filename(), @@ -429,6 +454,7 @@ Typical error reasons: - **`eacces`** - The drive does not exist. - **`einval`** - The format of `Drive` is invalid. + """. -spec get_cwd(Drive) -> {ok, Dir} | {error, Reason} when Drive :: string(), @@ -466,6 +492,16 @@ Typical error reasons are: > > In a future release, a bad type for argument `Dir` will probably generate an > exception. + +## Examples + +```erlang +1> {ok, Original} = file:get_cwd(). +2> ok = file:set_cwd(Dir). +3> {ok, _} = file:get_cwd(). +4> file:set_cwd(Original). +ok +``` """. -spec set_cwd(Dir) -> ok | {error, Reason} when Dir :: name() | EncodedBinary, @@ -507,6 +543,17 @@ Typical error reasons: > > In a future release, a bad type for argument `Filename` will probably generate > an exception. + +## Examples + +```erlang +1> File = filename:join(Dir, "delete_example.txt"). +2> ok = file:write_file(File, "temporary data"). +3> file:delete(File). +ok +4> file:delete(File). +{error,enoent} +``` """. -doc(#{since => <<"OTP 24.0">>}). -spec delete(Filename, Opts) -> ok | {error, Reason} when @@ -560,6 +607,20 @@ Typical error reasons: - **`enotdir`** - `Source` is a directory, but `Destination` is not. - **`exdev`** - `Source` and `Destination` are on different file systems. + +## Examples + +```erlang +1> Src = filename:join(Dir, "rename_src.txt"). +2> Dst = filename:join(Dir, "rename_dst.txt"). +3> ok = file:write_file(Src, "data"). +4> file:rename(Src, Dst). +ok +5> file:read_file(Dst). +{ok,<<"data">>} +6> file:read_file(Src). +{error,enoent} +``` """. -spec rename(Source, Destination) -> ok | {error, Reason} when Source :: name_all(), @@ -586,6 +647,16 @@ Typical error reasons: - **`enotdir`** - A component of `Dir` is not a directory. On some platforms, `enoent` is returned instead. + +## Examples + +```erlang +1> NewDir = filename:join(Dir, "my_new_dir"). +2> file:make_dir(NewDir). +ok +3> file:make_dir(NewDir). +{error,eexist} +``` """. -spec make_dir(Dir) -> ok | {error, Reason} when Dir :: name_all(), @@ -612,6 +683,15 @@ Typical error reasons: - **`einval`** - Attempt to delete the current directory. On some platforms, `eacces` is returned instead. + +## Examples + +```erlang +1> EmptyDir = filename:join(Dir, "empty_dir"). +2> ok = file:make_dir(EmptyDir). +3> file:del_dir(EmptyDir). +ok +``` """. -spec del_dir(Dir) -> ok | {error, Reason} when Dir :: name_all(), @@ -629,6 +709,18 @@ first recursively deleted. Returns: - **`{error, posix()}`** - An error occurred when accessing or deleting `File`. If some file or directory under `File` could not be deleted, `File` cannot be deleted as it is non-empty, and `{error, eexist}` is returned. + +## Examples + +```erlang +1> Parent = filename:join(Dir, "parent"). +2> ok = file:make_dir(Parent). +3> ok = file:write_file(filename:join(Parent, "a.txt"), ""). +4> ok = file:make_dir(filename:join(Parent, "sub")). +5> ok = file:write_file(filename:join([Parent, "sub", "b.txt"]), ""). +6> file:del_dir_r(Parent). +ok +``` """. -doc(#{since => <<"OTP 23.0">>}). -spec del_dir_r(File) -> ok | {error, Reason} when @@ -775,6 +867,20 @@ Typical error reasons: - **`enotdir`** - A component of the filename is not a directory. On some platforms, `enoent` is returned instead. + +## Examples + +```erlang +1> File = filename:join(Dir, "info_example.txt"). +2> ok = file:write_file(File, <<"Hello!">>). +3> {ok, Info} = file:read_file_info(File). +4> element(2, Info). +6 +5> element(3, Info). +regular +6> element(4, Info). +read_write +``` """. -doc(#{since => <<"OTP R15B">>}). -spec read_file_info(File, Opts) -> {ok, FileInfo} | {error, Reason} when @@ -833,6 +939,7 @@ If `Name` is not a symbolic link, this function returns the same result as [`read_file_info/1`](`read_file_info/1`). On platforms that do not support symbolic links, this function is always equivalent to [`read_file_info/1`](`read_file_info/1`). + """. -doc(#{since => <<"OTP R15B">>}). -spec read_link_info(Name, Opts) -> {ok, FileInfo} | {error, Reason} when @@ -870,6 +977,7 @@ Typical error reasons: - **`enoent`** - The file does not exist. - **`enotsup`** - Symbolic links are not supported on this platform. + """. -spec read_link(Name) -> {ok, Filename} | {error, Reason} when Name :: name_all(), @@ -893,6 +1001,7 @@ Typical error reasons: - **`enoent`** - The file does not exist. - **`enotsup`** - Symbolic links are not supported on this platform. + """. -doc(#{since => <<"OTP R16B">>}). -spec read_link_all(Name) -> {ok, Filename} | {error, Reason} when @@ -994,6 +1103,7 @@ Typical error reasons: - **`enotdir`** - A component of the filename is not a directory. On some platforms, `enoent` is returned instead. + """. -doc(#{since => <<"OTP R15B">>}). -spec write_file_info(Filename, FileInfo, Opts) -> ok | {error, Reason} when @@ -1031,6 +1141,18 @@ Typical error reasons: - **`{no_translation, Filename}`** - `Filename` is a `t:binary/0` with characters coded in ISO Latin-1 and the VM was started with parameter `+fnue`. + +## Examples + +```erlang +1> SubDir = filename:join(Dir, "listdir_test"). +2> ok = file:make_dir(SubDir). +3> ok = file:write_file(filename:join(SubDir, "a.txt"), ""). +4> ok = file:write_file(filename:join(SubDir, "b.txt"), ""). +5> {ok, Files} = file:list_dir(SubDir). +6> lists:sort(Files). +["a.txt","b.txt"] +``` """. -spec list_dir(Dir) -> {ok, Filenames} | {error, Reason} when Dir :: name_all(), @@ -1054,6 +1176,17 @@ Typical error reasons: parent directories. - **`enoent`** - The directory does not exist. + +## Examples + +```erlang +1> SubDir = filename:join(Dir, "listdirall_test"). +2> ok = file:make_dir(SubDir). +3> ok = file:write_file(filename:join(SubDir, "a.txt"), ""). +4> {ok, Files} = file:list_dir_all(SubDir). +5> lists:sort(Files). +["a.txt"] +``` """. -doc(#{since => <<"OTP R16B">>}). -spec list_dir_all(Dir) -> {ok, Filenames} | {error, Reason} when @@ -1092,6 +1225,17 @@ Typical error reasons: platforms, `enoent` is returned instead. - **`enomem`** - There is not enough memory for the contents of the file. + +## Examples + +```erlang +1> File = filename:join(Dir, "read_example.txt"). +2> ok = file:write_file(File, <<"Hello!">>). +3> file:read_file(File). +{ok,<<"Hello!">>} +4> file:read_file("nonexistent_file"). +{error,enoent} +``` """. -doc(#{since => <<"OTP 27.0">>}). -spec read_file(Filename, Opts) -> {ok, Binary} | {error, Reason} when @@ -1128,6 +1272,18 @@ Typical error reasons: - **`eexist`** - `New` already exists. - **`enotsup`** - Hard links are not supported on this platform. + +## Examples + +```erlang +1> File = filename:join(Dir, "hardlink_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> Link = filename:join(Dir, "hardlink"). +4> file:make_link(File, Link). +ok +5> file:read_file(Link). +{ok,<<"data">>} +``` """. -spec make_link(Existing, New) -> ok | {error, Reason} when Existing :: name_all(), @@ -1155,6 +1311,7 @@ Typical error reasons: - **`eperm`** - User does not have privileges to create symbolic links (`SeCreateSymbolicLinkPrivilege` on Windows). + """. -spec make_symlink(Existing, New) -> ok | {error, Reason} when Existing :: name_all(), @@ -1182,6 +1339,20 @@ Typical error reasons: parent directories. - **`eisdir`** - The named file is a directory. + +## Examples + +```erlang +1> File = filename:join(Dir, "write_example.txt"). +2> file:write_file(File, "Hello, world!"). +ok +3> file:read_file(File). +{ok,<<"Hello, world!">>} +4> file:write_file(File, [<<"line 1\n">>, <<"line 2\n">>]). +ok +5> file:read_file(File). +{ok,<<"line 1\nline 2\n">>} +``` """. -spec write_file(Filename, Bytes) -> ok | {error, Reason} when Filename :: name_all(), @@ -1199,6 +1370,18 @@ write_file(Name, Bin) -> Same as [`write_file/2`](`write_file/2`), but takes a third argument `Modes`, a list of possible modes, see `open/2`. The mode flags `binary` and `write` are implicit, so they are not to be used. + +## Examples + +Append to an existing file: + +```erlang +1> File = filename:join(Dir, "write_file3_example.txt"). +2> ok = file:write_file(File, <<"Hello">>). +3> ok = file:write_file(File, <<", world!">>, [append]). +4> file:read_file(File). +{ok,<<"Hello, world!">>} +``` """. -spec write_file(Filename, Bytes, Modes) -> ok | {error, Reason} when Filename :: name_all(), @@ -1473,6 +1656,20 @@ Typical error reasons: - **`enospc`** - There is no space left on the device (if `write` access was specified). + +## Examples + +```erlang +1> File = filename:join(Dir, "open_example.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> ok = file:write(Fd, "Hello, world!"). +4> ok = file:close(Fd). +5> {ok, Fd2} = file:open(File, [read, binary]). +6> file:read(Fd2, 13). +{ok,<<"Hello, world!">>} +7> file:close(Fd2). +ok +``` """. -spec open(File, Modes) -> {ok, IoDevice} | {error, Reason} when File :: Filename | iodata(), @@ -1525,6 +1722,16 @@ some severe errors such as out of memory. Notice that if option `delayed_write` was used when opening the file, [`close/1`](`close/1`) can return an old write error and not even try to close the file. See `open/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "close_example.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> ok = file:write(Fd, "data"). +4> file:close(Fd). +ok +``` """. -spec close(IoDevice) -> ok | {error, Reason} when IoDevice :: io_device(), @@ -1551,6 +1758,18 @@ data in a specific pattern in the future, thus allowing the operating system to perform appropriate optimizations. On some platforms, this function might have no effect. + +## Examples + +```erlang +1> File = filename:join(Dir, "advise_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> {ok, Fd} = file:open(File, [read]). +4> file:advise(Fd, 0, 0, sequential). +ok +5> file:close(Fd). +ok +``` """. -doc(#{since => <<"OTP R14B">>}). -spec advise(IoDevice, Offset, Length, Advise) -> ok | {error, Reason} when @@ -1617,6 +1836,22 @@ Typical error reasons: - **`{no_translation, unicode, latin1}`** - The file is opened with another `encoding` than `latin1` and the data in the file cannot be translated to the byte-oriented data that this function returns. + +## Examples + +```erlang +1> File = filename:join(Dir, "read_example2.txt"). +2> ok = file:write_file(File, <<"Hello, world!">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:read(Fd, 5). +{ok,<<"Hello">>} +5> file:read(Fd, 100). +{ok,<<", world!">>} +6> file:read(Fd, 1). +eof +7> file:close(Fd). +ok +``` """. -spec read(IoDevice, Number) -> {ok, Data} | eof | {error, Reason} when IoDevice :: io_device() | io:device(), @@ -1680,6 +1915,24 @@ Typical error reasons: - **`{no_translation, unicode, latin1}`** - The file is opened with another `encoding` than `latin1` and the data on the file cannot be translated to the byte-oriented data that this function returns. + +## Examples + +```erlang +1> File = filename:join(Dir, "readline_example.txt"). +2> ok = file:write_file(File, <<"line 1\nline 2\nline 3">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:read_line(Fd). +{ok,<<"line 1\n">>} +5> file:read_line(Fd). +{ok,<<"line 2\n">>} +6> file:read_line(Fd). +{ok,<<"line 3">>} +7> file:read_line(Fd). +eof +8> file:close(Fd). +ok +``` """. -spec read_line(IoDevice) -> {ok, Data} | eof | {error, Reason} when IoDevice :: io_device() | io:device(), @@ -1711,6 +1964,18 @@ requested position is beyond end of file. As the position is specified as a byte-offset, take special caution when working with files where `encoding` is set to something else than `latin1`, as not every byte position is a valid character boundary on such a file. + +## Examples + +```erlang +1> File = filename:join(Dir, "pread2_example.txt"). +2> ok = file:write_file(File, <<"HelloWorld">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:pread(Fd, [{0, 5}, {5, 5}]). +{ok,[<<"Hello">>,<<"World">>]} +5> file:close(Fd). +ok +``` """. -spec pread(IoDevice, LocNums) -> {ok, DataL} | eof | {error, Reason} when IoDevice :: io_device(), @@ -1752,6 +2017,20 @@ and unchanged for `ram` mode. As the position is specified as a byte-offset, take special caution when working with files where `encoding` is set to something else than `latin1`, as not every byte position is a valid character boundary on such a file. + +## Examples + +```erlang +1> File = filename:join(Dir, "pread_example.txt"). +2> ok = file:write_file(File, <<"Hello, world!">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:pread(Fd, 0, 5). +{ok,<<"Hello">>} +5> file:pread(Fd, 7, 5). +{ok,<<"world">>} +6> file:close(Fd). +ok +``` """. -spec pread(IoDevice, Location, Number) -> {ok, Data} | eof | {error, Reason} when @@ -1785,6 +2064,20 @@ Typical error reasons: - **`ebadf`** - The file is not opened for writing. - **`enospc`** - No space is left on the device. + +## Examples + +```erlang +1> File = filename:join(Dir, "write_example2.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> file:write(Fd, "Hello!"). +ok +4> file:write(Fd, [<<" More">>, <<" data">>]). +ok +5> ok = file:close(Fd). +6> file:read_file(File). +{ok,<<"Hello! More data">>} +``` """. -spec write(IoDevice, Bytes) -> ok | {error, Reason} when IoDevice :: io_device() | io:device(), @@ -1812,6 +2105,20 @@ the failure. When positioning in a file with other `encoding` than `latin1`, caution must be taken to set the position on a correct character boundary. For details, see `position/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "pwrite2_example.txt"). +2> ok = file:write_file(File, <<"HelloWorld">>). +3> {ok, Fd} = file:open(File, [write, read, binary]). +4> file:pwrite(Fd, [{0, <<"HELLO">>}, {5, <<"WORLD">>}]). +ok +5> file:pread(Fd, 0, 10). +{ok,<<"HELLOWORLD">>} +6> file:close(Fd). +ok +``` """. -spec pwrite(IoDevice, LocBytes) -> ok | {error, {N, Reason}} when IoDevice :: io_device(), @@ -1850,6 +2157,20 @@ and unchanged for `ram` mode. When positioning in a file with other `encoding` than `latin1`, caution must be taken to set the position on a correct character boundary. For details, see `position/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "pwrite_example.txt"). +2> {ok, Fd} = file:open(File, [write, read, binary]). +3> ok = file:write(Fd, <<"Hello, world!">>). +4> file:pwrite(Fd, 7, <<"Erlang!">>). +ok +5> file:pread(Fd, 0, 14). +{ok,<<"Hello, Erlang!">>} +6> file:close(Fd). +ok +``` """. -spec pwrite(IoDevice, Location, Bytes) -> ok | {error, Reason} when IoDevice :: io_device(), @@ -1880,6 +2201,17 @@ newly written data and another one to update the modification time stored in the Available only in some POSIX systems, this call results in a call to `fsync()`, or has no effect in systems not providing the `fdatasync()` syscall. + +## Examples + +```erlang +1> File = filename:join(Dir, "datasync_example.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> ok = file:write(Fd, "important data"). +4> ok = file:datasync(Fd). +5> file:close(Fd). +ok +``` """. -doc(#{since => <<"OTP R14B">>}). -spec datasync(IoDevice) -> ok | {error, Reason} when @@ -1901,6 +2233,17 @@ effect. A typical error reason is: - **`enospc`** - Not enough space left to write the file. + +## Examples + +```erlang +1> File = filename:join(Dir, "sync_example.txt"). +2> {ok, Fd} = file:open(File, [write]). +3> ok = file:write(Fd, "important data"). +4> ok = file:sync(Fd). +5> file:close(Fd). +ok +``` """. -spec sync(IoDevice) -> ok | {error, Reason} when IoDevice :: io_device(), @@ -1941,6 +2284,24 @@ A typical error reason is: - **`einval`** - Either `Location` is illegal, or it is evaluated to a negative offset in the file. Notice that if the resulting position is a negative value, the result is an error, and after the call the file position is undefined. + +## Examples + +```erlang +1> File = filename:join(Dir, "position_example.txt"). +2> ok = file:write_file(File, <<"Hello, world!">>). +3> {ok, Fd} = file:open(File, [read, binary]). +4> file:position(Fd, {bof, 7}). +{ok,7} +5> file:read(Fd, 5). +{ok,<<"world">>} +6> file:position(Fd, bof). +{ok,0} +7> file:read(Fd, 5). +{ok,<<"Hello">>} +8> file:close(Fd). +ok +``` """. -spec position(IoDevice, Location) -> {ok, NewPosition} | {error, Reason} when IoDevice :: io_device(), @@ -1958,6 +2319,20 @@ position(_, _) -> -doc """ Truncates the file referenced by `IoDevice` at the current position. Returns `ok` if successful, otherwise `{error, Reason}`. + +## Examples + +```erlang +1> File = filename:join(Dir, "truncate_example.txt"). +2> ok = file:write_file(File, <<"Hello, world!">>). +3> {ok, Fd} = file:open(File, [read, write, binary]). +4> {ok, 5} = file:position(Fd, 5). +5> file:truncate(Fd). +ok +6> ok = file:close(Fd). +7> file:read_file(File). +{ok,<<"Hello">>} +``` """. -spec truncate(IoDevice) -> ok | {error, Reason} when IoDevice :: io_device(), @@ -2006,6 +2381,18 @@ source. If the operation fails, `{error, Reason}` is returned. Typical error reasons: as for `open/2` if a file had to be opened, and as for `read/2` and `write/2`. + +## Examples + +```erlang +1> Src = filename:join(Dir, "copy_src.txt"). +2> Dst = filename:join(Dir, "copy_dst.txt"). +3> ok = file:write_file(Src, <<"Hello, world!">>). +4> file:copy(Src, Dst). +{ok,13} +5> file:read_file(Dst). +{ok,<<"Hello, world!">>} +``` """. -spec copy(Source, Destination, ByteCount) -> {ok, BytesCopied} | {error, Reason} when @@ -2260,15 +2647,12 @@ following: Erlang terms in the file. To convert the three-element tuple to an English description of the error, use `format_error/1`. -_Example:_ - -```text -f.txt: {person, "kalle", 25}. - {person, "pelle", 30}. -``` +## Examples ```erlang -1> file:consult("f.txt"). +1> File = filename:join(Dir, "terms.txt"). +2> ok = file:write_file(File, "{person, \"kalle\", 25}.\n{person, \"pelle\", 30}.\n"). +3> file:consult(File). {ok,[{person,"kalle",25},{person,"pelle",30}]} ``` @@ -2313,6 +2697,15 @@ Returns one of the following: The encoding of `Filename` can be set by a comment as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "config.terms"), "{key, value}.\n"). +2> {ok, Terms, _FullName} = file:path_consult([Dir], "config.terms"). +3> Terms. +[{key,value}] +``` """. -spec path_consult(Path, Filename) -> {ok, Terms, FullName} | {error, Reason} when Path :: [Dir], @@ -2357,6 +2750,15 @@ Returns one of the following: The encoding of `Filename` can be set by a comment, as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> File = filename:join(Dir, "eval_example.erl"). +2> ok = file:write_file(File, "io:format(\"hello\\n\").\n"). +3> file:eval(File). +ok +``` """. -spec eval(Filename) -> ok | {error, Reason} when Filename :: name_all(), @@ -2370,6 +2772,16 @@ eval(File) -> The same as [`eval/1`](`eval/1`), but the variable bindings `Bindings` are used in the evaluation. For information about the variable bindings, see `m:erl_eval`. + +## Examples + +```erlang +1> File = filename:join(Dir, "eval2_example.erl"). +2> ok = file:write_file(File, "X + 1.\n"). +3> Bs = erl_eval:add_binding('X', 10, erl_eval:new_bindings()). +4> file:eval(File, Bs). +ok +``` """. -spec eval(Filename, Bindings) -> ok | {error, Reason} when Filename :: name_all(), @@ -2412,6 +2824,13 @@ Returns one of the following: The encoding of `Filename` can be set by a comment as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "path_eval.erl"), "1 + 1.\n"). +2> {ok, _FullName} = file:path_eval([Dir], "path_eval.erl"). +``` """. -spec path_eval(Path, Filename) -> {ok, FullName} | {error, Reason} when Path :: [Dir :: name_all()], @@ -2466,6 +2885,15 @@ Returns one of the following: The encoding of `Filename` can be set by a comment as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> File = filename:join(Dir, "script_example.erl"). +2> ok = file:write_file(File, "lists:seq(1, 5).\n"). +3> file:script(File). +{ok,[1,2,3,4,5]} +``` """. -spec script(Filename) -> {ok, Value} | {error, Reason} when Filename :: name_all(), @@ -2479,6 +2907,16 @@ script(File) -> -doc """ The same as [`script/1`](`script/1`) but the variable bindings `Bindings` are used in the evaluation. See `m:erl_eval` about variable bindings. + +## Examples + +```erlang +1> File = filename:join(Dir, "script2_example.erl"). +2> ok = file:write_file(File, "X * 2.\n"). +3> Bs = erl_eval:add_binding('X', 21, erl_eval:new_bindings()). +4> file:script(File, Bs). +{ok,42} +``` """. -spec script(Filename, Bindings) -> {ok, Value} | {error, Reason} when Filename :: name_all(), @@ -2520,6 +2958,13 @@ Returns one of the following: The encoding of `Filename` can be set by a comment as described in [`epp`](`m:epp#encoding`). + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "path_script.erl"), "1 + 2.\n"). +2> {ok, 3, _FullName} = file:path_script([Dir], "path_script.erl"). +``` """. -spec path_script(Path, Filename) -> {ok, Value, FullName} | {error, Reason} when @@ -2536,6 +2981,14 @@ path_script(Path, File) -> -doc """ The same as [`path_script/2`](`path_script/2`) but the variable bindings `Bindings` are used in the evaluation. See `m:erl_eval` about variable bindings. + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "path_script3.erl"), "X + Y.\n"). +2> Bs = erl_eval:add_binding('Y', 3, erl_eval:add_binding('X', 4, erl_eval:new_bindings())). +3> {ok, 7, _FullName} = file:path_script([Dir], "path_script3.erl", Bs). +``` """. -spec path_script(Path, Filename, Bindings) -> {ok, Value, FullName} | {error, Reason} when @@ -2585,6 +3038,17 @@ Returns one of the following: `Path`. - **`{error, atom()}`** - The file cannot be opened. + +## Examples + +```erlang +1> ok = file:write_file(filename:join(Dir, "path_open.txt"), <<"data">>). +2> {ok, Fd, _FullName} = file:path_open([Dir], "path_open.txt", [read]). +3> file:close(Fd). +ok +4> file:path_open([Dir], "nonexistent.txt", [read]). +{error,enoent} +``` """. -spec path_open(Path, Filename, Modes) -> {ok, IoDevice, FullName} | {error, Reason} when @@ -2613,7 +3077,18 @@ path_open(PathList, Name, Mode) -> end end. --doc "Changes permissions of a file. See `write_file_info/2`.". +-doc """ +Changes permissions of a file. See `write_file_info/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "chmod_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> file:change_mode(File, 8#00644). +ok +``` +""". -doc(#{since => <<"OTP R14B">>}). -spec change_mode(Filename, Mode) -> ok | {error, Reason} when Filename :: name_all(), @@ -2655,7 +3130,21 @@ change_group(Name, GroupId) when is_integer(GroupId) -> write_file_info(Name, #file_info{gid=GroupId}). --doc "Changes the modification and access times of a file. See `write_file_info/2`.". +-doc """ +Changes the modification and access times of a file. See `write_file_info/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "ctime_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> file:change_time(File, {{2024, 1, 1}, {0, 0, 0}}). +ok +4> {ok, Info} = file:read_file_info(File). +5> element(6, Info). +{{2024,1,1},{0,0,0}} +``` +""". -spec change_time(Filename, Mtime) -> ok | {error, Reason} when Filename :: name_all(), Mtime :: date_time(), @@ -2669,6 +3158,17 @@ change_time(Name, {{Y, M, D}, {H, Min, Sec}}=Time) -doc """ Changes the modification and last access times of a file. See `write_file_info/2`. + +## Examples + +```erlang +1> File = filename:join(Dir, "ctime3_example.txt"). +2> ok = file:write_file(File, <<"data">>). +3> Atime = {{2024, 6, 15}, {12, 0, 0}}. +4> Mtime = {{2024, 6, 15}, {13, 0, 0}}. +5> file:change_time(File, Atime, Mtime). +ok +``` """. -spec change_time(Filename, Atime, Mtime) -> ok | {error, Reason} when Filename :: name_all(), diff --git a/lib/kernel/test/file_SUITE.erl b/lib/kernel/test/file_SUITE.erl index a746a616040c..ad2ce592db04 100644 --- a/lib/kernel/test/file_SUITE.erl +++ b/lib/kernel/test/file_SUITE.erl @@ -45,10 +45,11 @@ -module(?FILE_SUITE). --export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2, init_per_testcase/2, end_per_testcase/2, - read_write_file/1, names/1]). + read_write_file/1, names/1, + doctests/1]). -export([cur_dir_0/1, cur_dir_1/1, make_del_dir/1, make_del_dir_r/1, list_dir/1,list_dir_error/1, untranslatable_names/1, untranslatable_names_error/1, @@ -141,7 +142,8 @@ all() -> ipread, interleaved_read_write, otp_5814, otp_10852, large_file, large_write, read_line_1, read_line_2, read_line_3, read_line_4, standard_io, old_io_protocol, - unicode_mode, {group, bench} + unicode_mode, {group, bench}, + doctests ]. groups() -> @@ -4967,3 +4969,53 @@ memsize() -> Available -> Available end. + +doctests(Config) -> + Dir = filename:join(proplists:get_value(priv_dir, Config), "doctests"), + ok = file:make_dir(Dir), + Bindings = #{'Dir' => Dir}, + FunsNeedingDir = + [{function, delete, 2}, + {function, rename, 2}, + {function, set_cwd, 1}, + {function, make_dir, 1}, + {function, del_dir, 1}, + {function, del_dir_r, 1}, + {function, list_dir, 1}, + {function, list_dir_all, 1}, + {function, read_file, 2}, + {function, read_file_info, 2}, + {function, write_file, 2}, + {function, write_file, 3}, + {function, make_link, 2}, + {function, open, 2}, + {function, close, 1}, + {function, advise, 4}, + {function, read, 2}, + {function, write, 2}, + {function, read_line, 1}, + {function, pread, 2}, + {function, pread, 3}, + {function, pwrite, 2}, + {function, pwrite, 3}, + {function, datasync, 1}, + {function, sync, 1}, + {function, position, 2}, + {function, truncate, 1}, + {function, consult, 1}, + {function, path_consult, 2}, + {function, eval, 1}, + {function, eval, 2}, + {function, path_eval, 2}, + {function, script, 1}, + {function, script, 2}, + {function, path_script, 2}, + {function, path_script, 3}, + {function, path_open, 3}, + {function, change_mode, 2}, + {function, change_time, 2}, + {function, change_time, 3}, + {function, copy, 3}], + ct_doctest:module(file, + [{KFA, Bindings} || KFA <- FunsNeedingDir], + []). From c137daaa600f8ad5fc449d2b1baee696a7565246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Fri, 13 Mar 2026 15:02:49 +0100 Subject: [PATCH 11/16] compiler: Add doctests to compile --- lib/compiler/src/compile.erl | 61 ++++++++++++++++++++++------- lib/compiler/test/compile_SUITE.erl | 15 ++++++- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index 80b4950e9316..9cc01abae438 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -932,15 +932,12 @@ file(File, Opts) when is_list(Opts) -> file(File, Opt) -> file(File, [Opt|?DEFAULT_OPTIONS]). --doc """ -Is the same as -[`forms(Forms, [verbose,report_errors,report_warnings])`](`forms/2`). -""". +-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. @@ -948,7 +945,38 @@ 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. -""". + +## Examples + +```erlang +1> Program = """ + -module(test). + -export([foo/0]). + foo() -> bar. + """. +2> ParseForms = + fun + Parse("", Loc) -> + []; + Parse(String, Loc) -> + case erl_scan:tokens("", String, Loc) of + {done, {ok, Tokens, NewLoc}, Cont} -> + {ok, Form} = erl_parse:parse_form(Tokens), + [Form | Parse(Cont, NewLoc)] + end + end. +3> Forms = ParseForms(Program ++ "\n", {1,1}). +[{attribute,{1,2},module,test}, + {attribute,{2,2},export,[{foo,0}]}, + {function,{3,1}, + foo,0, + [{clause,{3,1},[],[],[{atom,{3,10},bar}]}]}] +4> compile:forms(Forms). +{ok,test, + <<70,79,82,49,0,0,1,196,66,69,65,77,65,116,85,56,0,0,0, + 52,255,255,255,250,64,116,...>>} +``` +"""". -spec forms(Forms :: forms(), Options :: [option()] | option()) -> CompRet :: comp_ret(). forms(Forms, Opts) when is_list(Opts) -> @@ -1000,9 +1028,8 @@ noenv_forms(Forms, Opts) when is_list(Opts) -> noenv_forms(Forms, Opt) when is_atom(Opt) -> noenv_forms(Forms, [Opt|?DEFAULT_OPTIONS]). --doc #{ equiv => string(String, []) }. +-doc #{ equiv => string(String, ?DEFAULT_OPTIONS) }. -spec string(String :: unicode:chardata()) -> CompRet :: comp_ret(). - string(String) -> string(String, ?DEFAULT_OPTIONS). -doc """ @@ -1013,24 +1040,28 @@ Erlang source code as first argument. The source code is run through the Erlang preprocessor (`epp`) just as when compiling a file, so directives such as `-include`, `-define`, and `-ifdef` are supported. -Option `binary` is implicit, that is, no object code file is produced. - -The `source` option can be used to set the source file name that will -appear in error messages and the `module_info/1` function. If not given, -it defaults to the module name with a `.erl` extension. +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. The `{include_path_open, Fun}` option can be used to provide a custom function for opening include files (see `epp:open/1`), allowing compilation entirely in memory without touching the file system. +## Examples + ```erlang -1> compile:string("-module(foo). -export([bar/0]). bar() -> ok."). +1> {ok, foo, Bin} = compile:string("-module(foo). -export([bar/0]). bar() -> ok."). {ok,foo,<<...>>} +2> code:load_binary(foo, "foo.erl", Bin). +{module, foo} +3> foo:bar(). +ok ``` """. -spec string(String :: unicode:chardata(), Options :: [option()] | option()) -> CompRet :: comp_ret(). - string(String, Opts) when is_list(Opts) -> do_compile({string,String}, [binary|Opts++env_default_opts()]); string(String, Opt) when is_atom(Opt) -> diff --git a/lib/compiler/test/compile_SUITE.erl b/lib/compiler/test/compile_SUITE.erl index 9b5e3e57cda2..8bd3c01e56f5 100644 --- a/lib/compiler/test/compile_SUITE.erl +++ b/lib/compiler/test/compile_SUITE.erl @@ -44,7 +44,7 @@ compile_attribute/1, message_printing/1, other_options/1, transforms/1, erl_compile_api/1, types_pp/1, bs_init_writable/1, annotations_pp/1, option_order/1, - sys_coverage/1 + sys_coverage/1, doctests/1 ]). suite() -> [{ct_hooks,[ts_install_cth]}]. @@ -67,7 +67,7 @@ all() -> deterministic_docs, compile_attribute, message_printing, other_options, transforms, erl_compile_api, types_pp, bs_init_writable, annotations_pp, - option_order, sys_coverage]. + option_order, sys_coverage, doctests]. groups() -> []. @@ -2525,6 +2525,17 @@ sys_coverage_2(DataDir) -> ok. +doctests(_Config) -> + PathOpen = fun(Path, Filename, Modes) -> + case file:path_open(Path, Filename, Modes) of + {error, enoent} -> + {ok, FD} = file:open(~"", [ram, cooked | Modes]), + {ok, FD, filename:basename(Filename)}; + Else -> Else + end + end, + ct_doctest:module(compile, [], [{compile_options, [{include_path_open, PathOpen}]}]). + %%% %%% Utilities. %%% From 43aa856fac6d839aa9eee0ad6b6c836c5cfbc0b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Sat, 14 Mar 2026 08:46:43 +0100 Subject: [PATCH 12/16] Fix dialyzer --- lib/common_test/src/ct_doctest.erl | 2 +- lib/kernel/src/ram_file.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/common_test/src/ct_doctest.erl b/lib/common_test/src/ct_doctest.erl index 3ada867ec65c..e7645832647a 100644 --- a/lib/common_test/src/ct_doctest.erl +++ b/lib/common_test/src/ct_doctest.erl @@ -317,7 +317,7 @@ Options for doctest execution. {compile_options, [compile:option()]}]. -record(options, - { parser = fun parse_markdown_builtin/1 :: fun((unicode:unicode_binary()) -> [unicode:unicode_binary()] | {error, term()}), + { parser = fun parse_markdown_builtin/1, %:: fun((unicode:unicode_binary()) -> [unicode:unicode_binary()] | {error, term()}), skipped_blocks = false :: non_neg_integer() | false, verbose = false :: boolean(), compile_options = [] :: [compile:option()] }). diff --git a/lib/kernel/src/ram_file.erl b/lib/kernel/src/ram_file.erl index 20625e196dcf..dd9b7eda133f 100644 --- a/lib/kernel/src/ram_file.erl +++ b/lib/kernel/src/ram_file.erl @@ -162,7 +162,7 @@ read_line(Fd, Acc) -> %% Seek back the unread portion after the \n. case byte_size(After) of 0 -> ok; - N -> {ok, _} = position(Fd, {cur, -N}) + N -> {ok, _} = position(Fd, {cur, -N}), ok end, {ok, iolist_to_binary(lists:reverse(Acc, [Before, <<"\n">>]))}; nomatch -> From ee1e78c7a2e6ec1f6400576dfcef8b87c0401223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Tue, 17 Mar 2026 15:59:51 +0100 Subject: [PATCH 13/16] Add doctests to epp --- lib/stdlib/src/epp.erl | 253 ++++++++++++++------- lib/stdlib/test/epp_SUITE.erl | 15 +- lib/stdlib/test/epp_SUITE_data/example.erl | 27 +++ 3 files changed, 217 insertions(+), 78 deletions(-) create mode 100644 lib/stdlib/test/epp_SUITE_data/example.erl diff --git a/lib/stdlib/src/epp.erl b/lib/stdlib/src/epp.erl index 858ccb09d355..065007070e99 100644 --- a/lib/stdlib/src/epp.erl +++ b/lib/stdlib/src/epp.erl @@ -89,6 +89,101 @@ Module:format_error(ErrorDescriptor) -type epp_handle() :: pid(). -type source_encoding() :: latin1 | utf8. +-doc """ +The `t:option/0` are the options that can be used to customize the preprocessing. + +* `{default_encoding, DefEncoding}` - sets the default encoding of the file. The default encoding is + used if no valid encoding is found in the file. The valid encodings are `latin1` and `utf8`, + where the case of the characters can be chosen freely. If unset, it defaults to `utf8`. + +* `{includes, IncludePath}` - sets the include path for the file. The include path is used to resolve + `-include`, `-include_lib` directives. + +* `{source_name, SourceName}` - sets the file name of the implicit -file() attributes inserted + during preprocessing. If unset it will default to the name of the opened file. + +* `{macros, PredefMacros}` - sets the predefined `t:macros/0` for the file. `PredefMacros` is a list of + macros that are defined before preprocessing starts. + +* `{deterministic, Enabled}` - if set to `true`, will reduce the file name of the + implicit -file() attributes inserted during preprocessing to only the basename of the path. + +* `{extra, Enabled}` - if set to `true`, the return value is `{ok, Epp, Extra}` instead of `{ok, Epp}`, + where `Extra` contains which encoding was detected from the file. + +* `{location, StartLocation}` - sets the initial location of the file. The option `location` is forwarded + to the Erlang token scanner, see [`erl_scan:tokens/3,4`](`erl_scan:tokens/3`). For example: + + ```erlang + 1> file:read_file("example.erl"). + {ok, <<"-module(example).\n\n-export([foo/0]).\n\nfoo() -> ?MODULE.", ...>>} + 2> epp:parse_file("example.erl", [{location, {1, 1}}]). + {ok,[{attribute,{1,1},file,{"example.erl",1}}, + {attribute,{1,2},module,example}, + {attribute,{3,2},export,[{foo,0}]}, + {function,{5,1}, + foo,0, + [{clause,{5,1},[],[],[{atom,{5,11},example}]}]}, + {eof,{28,1}}]} + ``` + +* `{fd, FileDescriptor}` - use an already opened file descriptor to read from instead of a file name. + The file descriptor is expected to be an `t:file:io_server/0`. This enables in-memory preprocessing + using [ram files](`m:file#ram`), where the main source file can be served from memory without touching + disk. For example: + + See `parse_file/2` for an example of how to use this option. + +* `{include_path_open, Fun}` - provide a custom function for opening include files. The function has the + same signature as `file:path_open/3` and is used when resolving `-include`, `-include_lib`, and + `-doc {file, ...}` directives. This enables fully in-memory preprocessing using [ram files](`m:file#ram`). + For example: + + ```erlang + 1> {ok, IoDevice} = file:open(<<"-module(foo).\n-include(\"bar.hrl\").">>, + [read, binary, ram, cooked]). + 2> IncludeFileFun = fun + (_Path, "bar.hrl", Modes) -> + {ok, RamFD} = file:open(<<"-export([bar/0]).\nbar() ->\n?MODULE.">>, + [read, binary, ram, cooked | Modes]), + {ok, RamFD, "bar.hrl"}; + (Path, Name, Modes) -> + file:path_open(Path, Name, Modes) + end. + 3> {ok, EPP} = epp:parse_file("example.erl", + [{fd, IoDevice}, {include_path_open, IncludeFileFun}]). + {ok,[{attribute,1,file,{"example.erl",1}}, + {attribute,1,module,foo}, + {attribute,1,file,{"bar.hrl",1}}, + {attribute,1,export,[{bar,0}]}, + {function,2,bar,0,[{clause,2,[],[],[{atom,3,foo}]}]}, + {attribute,2,file,{"example.erl",2}}, + {eof,2}]} + ``` + + Since OTP @OTP-12345678@ +* `{compiler_internal,term()}` - forwarded to the Erlang token + scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`) in `erl_scan:string/3`. +""". +-type option() :: + {default_encoding, DefEncoding :: source_encoding()} | + {includes, IncludePath :: [ DirectoryName :: file:name()]} | + {macros, PredefMacros :: macros()} | + {source_name, SourceName :: file:name()} | + {deterministic, boolean()} | + {location, StartLocation :: erl_anno:location()} | + {reserved_word_fun, Fun :: fun((atom()) -> boolean())} | + {features, [Feature :: atom()]} | + {fd, OpenedFile :: file:io_server()} | + {include_path_open, fun((Path :: [file:name_all()], + FileName :: file:name_all(), + Modes :: [file:mode()]) -> + {ok, IoDevice :: file:io_device(), + FullName :: file:filename_all()} | + {error, term()})} | + 'extra' | + {'compiler_internal', [term()]}. + -type ifdef() :: 'ifdef' | 'ifndef' | 'if' | 'else'. -type name() :: atom(). @@ -146,7 +241,7 @@ Module:format_error(ErrorDescriptor) %% parse_file(FileName, IncludePath, PreDefMacros) %% macro_defs(Epp) --doc "Equivalent to `epp:open([{name, FileName}, {includes, IncludePath}])`.". +-doc #{ equiv => open([{name, FileName}, {includes, IncludePath}]) }. -spec open(FileName, IncludePath) -> {'ok', Epp} | {'error', ErrorDescriptor} when FileName :: file:name(), @@ -157,10 +252,7 @@ Module:format_error(ErrorDescriptor) open(Name, Path) -> open(Name, Path, []). --doc """ -Equivalent to -`epp:open([{name, FileName}, {includes, IncludePath}, {macros, PredefMacros}])`. -""". +-doc #{ equiv => open([{name, FileName}, {includes, IncludePath}, {macros, PredefMacros}]) }. -spec open(FileName, IncludePath, PredefMacros) -> {'ok', Epp} | {'error', ErrorDescriptor} when FileName :: file:name(), @@ -175,49 +267,37 @@ open(Name, Path, Pdm) -> -doc """ Opens a file for preprocessing. -If you want to change the file name of the implicit -file() attributes inserted -during preprocessing, you can do with `{source_name, SourceName}`. If unset it -will default to the name of the opened file. +The function is used to start parsing of an Erlang source file. To get the forms from the file +use `scan_erl_form/1` or `parse_erl_form/1`. When finished, the `m:epp` server should be closed with +`close/1`. Use this function if you want to scan or parse a file incrementally. To scan or parse +a whole file at once, use `scan_file/2` and `parse_file/3` respectively. -Setting `{deterministic, Enabled}` will additionally reduce the file name of the -implicit -file() attributes inserted during preprocessing to only the basename -of the path. +When using `open/1` the option `name` must always be specified and is the name of the file to +preprocess. This name is used in error messages and in the implicit `-file()` attributes +generated during preprocessing. -If `extra` is specified in `Options`, the return value is `{ok, Epp, Extra}` -instead of `{ok, Epp}`. +See `t:option/0` for the other available options and what they do. -The option `location` is forwarded to the Erlang token scanner, see -[`erl_scan:tokens/3,4`](`erl_scan:tokens/3`). +## Examples -The `{compiler_internal,term()}` option is forwarded to the Erlang token -scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`). +```erlang +1> file:read_file("example.erl"). +{ok, <<"-module(example).\n", ...>>} +2> {ok, EPP} = epp:open("example.erl", []). +3> epp:scan_erl_form(EPP). +{ok,[{'-',1},{atom,1,file},{'(',1},{string,1,"example.erl"}, + {',',1},{integer,1,1},{')',1},{dot,1}]} +4> epp:parse_erl_form(EPP). +{ok,{attribute,1,module,example}} +5> epp:close(EPP). +ok +``` -The `{include_path_open, Fun}` option 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 when resolving `-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. """. -doc(#{since => <<"OTP 17.0">>}). -spec open(Options) -> {'ok', Epp} | {'ok', Epp, Extra} | {'error', ErrorDescriptor} when - Options :: [{'default_encoding', DefEncoding :: source_encoding()} | - {'includes', IncludePath :: [DirectoryName :: file:name()]} | - {'source_name', SourceName :: file:name()} | - {'deterministic', Enabled :: boolean()} | - {'macros', PredefMacros :: macros()} | - {'name',FileName :: file:name()} | - {'location',StartLocation :: erl_anno:location()} | - {'fd',FileDescriptor :: file:io_device()} | - {'include_path_open', IncludePathOpenFun :: fun((Path :: [file:name()], - FileName :: file:name(), - Modes :: [mode]) -> - {ok, IoDevice :: file:io_device(), - FullName :: file:name()} | - {error, term()})} | - 'extra' | - {'compiler_internal', [term()]}], + Options :: [option() | {'name',FileName :: file:name()}], Epp :: epp_handle(), Extra :: [{'encoding', source_encoding() | 'none'}], ErrorDescriptor :: term(). @@ -243,8 +323,7 @@ open(Options) -> end. -doc "Closes the preprocessing of a file.". --spec close(Epp) -> 'ok' when - Epp :: epp_handle(). +-spec close(Epp :: epp_handle()) -> 'ok'. close(Epp) -> %% Make sure that close is synchronous as a courtesy to test @@ -256,7 +335,9 @@ close(Epp) -> -doc """ Returns the raw tokens of the next Erlang form from the opened Erlang source -file. A tuple `{eof, Line}` is returned at the end of the file. The first form +file. + +A tuple `{eof, Line}` is returned at the end of the file. The first form corresponds to an implicit attribute `-file(File,1).`, where `File` is the file name. """. @@ -274,8 +355,9 @@ scan_erl_form(Epp) -> epp_request(Epp, scan_erl_form). -doc """ -Returns the next Erlang form from the opened Erlang source file. Tuple -`{eof, Location}` is returned at the end of the file. The first form corresponds +Returns the next Erlang form from the opened Erlang source file. + +Tuple `{eof, Location}` is returned at the end of the file. The first form corresponds to an implicit attribute `-file(File,1).`, where `File` is the file name. """. -spec parse_erl_form(Epp) -> @@ -379,18 +461,30 @@ format_error_1(E) -> file:format_error(E). -doc """ Preprocesses an Erlang source file returning a list of the lists of raw tokens -of each form. Notice that the tuple `{eof, Line}` returned at the end of the -file is included as a "form", and any failures to scan a form are included in -the list as tuples `{error, ErrorInfo}`. +of each form. + +Notice that the tuple `{eof, Line}` returned at the end of the file is included +as a "form", and any failures to scan a form are included in the list as tuples +`{error, ErrorInfo}`. + +For details on what each `Option` does, see `t:option/0`. + +## Examples + +``` +1> file:read_file("example.erl"). +{ok, <<"-module(example).\n\n-export([foo/0]).\n\nfoo() -> ?MODULE.", ...>>} +2> epp:scan_file("example.erl", []). +{ok,[[{'-',1},{atom,1,file},{'(',1},{string,1,"example.erl"}, + {',',1},{integer,1,1},{')',1},{dot,1}], ...], + [{encoding,none}]} +``` """. -doc(#{since => <<"OTP 24.0">>}). -spec scan_file(FileName, Options) -> {'ok', [Form], Extra} | {error, OpenError} when FileName :: file:name(), - Options :: [{'includes', IncludePath :: [DirectoryName :: file:name()]} | - {'source_name', SourceName :: file:name()} | - {'macros', PredefMacros :: macros()} | - {'default_encoding', DefEncoding :: source_encoding()}], + Options :: [option()], Form :: erl_scan:tokens() | {'error', ErrorInfo} | {'eof', Loc}, Loc :: erl_anno:location(), ErrorInfo :: erl_scan:error_info(), @@ -418,10 +512,7 @@ scan_file(Epp) -> [{eof,Location}] end. --doc """ -Equivalent to -`epp:parse_file(FileName, [{includes, IncludePath}, {macros, PredefMacros}])`. -""". +-doc #{ equiv => parse_file(Filename, [{includes, IncludePath}, {macros, PredefMacros}]) }. -spec parse_file(FileName, IncludePath, PredefMacros) -> {'ok', [Form]} | {error, OpenError} when FileName :: file:name(), @@ -438,36 +529,46 @@ parse_file(Ifile, Path, Predefs) -> parse_file(Ifile, [{includes, Path}, {macros, Predefs}]). -doc """ -Preprocesses and parses an Erlang source file. Notice that tuple -`{eof, Location}` returned at the end of the file is included as a "form". +Preprocesses and parses an Erlang source file. + +Notice that tuple `{eof, Location}` returned at the end of the file is included as +a "form". -If you want to change the file name of the implicit -file() attributes inserted -during preprocessing, you can do with `{source_name, SourceName}`. If unset it -will default to the name of the opened file. +See `t:option/0` for the available options and what they do. -If `extra` is specified in `Options`, the return value is `{ok, [Form], Extra}` -instead of `{ok, [Form]}`. +## Examples -The option `location` is forwarded to the Erlang token scanner, see -[`erl_scan:tokens/3,4`](`erl_scan:tokens/3`). +Parse a file: -The `{compiler_internal,term()}` option is forwarded to the Erlang token -scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`). +```erlang +1> file:read_file("example.erl"). +{ok, <<"-module(example).\n\n-export([foo/0]).\n\nfoo() -> ?MODULE.", ...>>} +2> epp:parse_file("example.erl", []). +{ok,[{attribute,1,file,{"example.erl",1}}, + {attribute,1,module,example}, + {attribute,3,export,[{foo,0}]}, + {function,5,foo,0,[{clause,5,[],[],[{atom,5,example}]}]}, + {eof,28}]} +``` + +Parse a ram file: + +```erlang +1> {ok, IoDevice} = file:open(<<"-module(foo).\n-export([bar/0]).\nbar() ->\n?MODULE.">>, + [read, binary, ram, cooked]). +2> epp:parse_file("foo.erl", [{fd, IoDevice}]). +{ok,[{attribute,1,file,{"foo.erl",1}}, + {attribute,1,module,foo}, + {attribute,2,export,[{bar,0}]}, + {function,3,bar,0,[{clause,3,[],[],[{atom,4,foo}]}]}, + {eof,4}]} +``` """. -doc(#{since => <<"OTP 17.0">>}). -spec parse_file(FileName, Options) -> {'ok', [Form]} | {'ok', [Form], Extra} | {error, OpenError} when FileName :: file:name(), - Options :: [{'includes', IncludePath :: [DirectoryName :: file:name()]} | - {'source_name', SourceName :: file:name()} | - {'macros', PredefMacros :: macros()} | - {'default_encoding', DefEncoding :: source_encoding()} | - {'location',StartLocation :: erl_anno:location()} | - {'reserved_word_fun', Fun :: fun((atom()) -> boolean())} | - {'features', [Feature :: atom()]} | - {'deterministic', boolean()} | - 'extra' | - {'compiler_internal', [term()]}], + Options :: [option()], Form :: erl_parse:abstract_form() | {'error', ErrorInfo} | {'eof',Location}, diff --git a/lib/stdlib/test/epp_SUITE.erl b/lib/stdlib/test/epp_SUITE.erl index 7f4cc0cae6c7..4dc904289357 100644 --- a/lib/stdlib/test/epp_SUITE.erl +++ b/lib/stdlib/test/epp_SUITE.erl @@ -37,7 +37,8 @@ moduledoc_include/1, stringify/1, fun_type_arg/1, - include_path_open/1 + include_path_open/1, + doctests/1 ]). -export([epp_parse_erl_form/2]). @@ -86,7 +87,8 @@ all() -> moduledoc_include, stringify, fun_type_arg, - include_path_open]. + include_path_open, + doctests]. groups() -> [{upcase_mac, [], [upcase_mac_1, upcase_mac_2]}, @@ -2447,6 +2449,15 @@ ram_include_path_open_try([Candidate | Rest], Files, Modes) -> ram_include_path_open_try(Rest, Files, Modes) end. +doctests(Config) -> + {ok, Cwd} = file:get_cwd(), + try + ok = file:set_cwd(proplists:get_value(data_dir, Config)), + ct_doctest:module(epp) + after + file:set_cwd(Cwd) + end. + fail() -> ct:fail(failed). diff --git a/lib/stdlib/test/epp_SUITE_data/example.erl b/lib/stdlib/test/epp_SUITE_data/example.erl new file mode 100644 index 000000000000..3d5d1ca5a380 --- /dev/null +++ b/lib/stdlib/test/epp_SUITE_data/example.erl @@ -0,0 +1,27 @@ +-module(example). + +-export([foo/0]). + +foo() -> ?MODULE. + +%% +%% %CopyrightBegin% +%% +%% SPDX-License-Identifier: Apache-2.0 +%% +%% Copyright Ericsson AB 2018-2025. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% From a3d3e52bfb2fd211a87c619ceaf1860b37f01636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Tue, 17 Mar 2026 16:00:22 +0100 Subject: [PATCH 14/16] Add since to compile:string --- lib/compiler/src/compile.erl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index 9cc01abae438..49051dd1c5b2 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -1029,6 +1029,7 @@ noenv_forms(Forms, Opt) when is_atom(Opt) -> noenv_forms(Forms, [Opt|?DEFAULT_OPTIONS]). -doc #{ equiv => string(String, ?DEFAULT_OPTIONS) }. +-doc #{ since => <<"OTP @OTP-1234567@">> }. -spec string(String :: unicode:chardata()) -> CompRet :: comp_ret(). string(String) -> string(String, ?DEFAULT_OPTIONS). @@ -1060,6 +1061,7 @@ entirely in memory without touching the file system. ok ``` """. +-doc #{ since => <<"OTP @OTP-1234567@">> }. -spec string(String :: unicode:chardata(), Options :: [option()] | option()) -> CompRet :: comp_ret(). string(String, Opts) when is_list(Opts) -> @@ -1068,6 +1070,7 @@ string(String, Opt) when is_atom(Opt) -> string(String, [Opt|?DEFAULT_OPTIONS]). -doc #{ equiv => noenv_string(String, []) }. +-doc #{ since => <<"OTP @OTP-1234567@">> }. -spec noenv_string(String :: unicode:chardata(), Options :: [option()] | option()) -> comp_ret(). From b38af19c32fd20e7c85e9cfb11126f5f8ec1a2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Tue, 17 Mar 2026 16:00:41 +0100 Subject: [PATCH 15/16] Add since to cooked ram file --- lib/kernel/src/file.erl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/kernel/src/file.erl b/lib/kernel/src/file.erl index 857fe5af39b7..be0a8cb01bab 100644 --- a/lib/kernel/src/file.erl +++ b/lib/kernel/src/file.erl @@ -1591,14 +1591,15 @@ more of the following options: This option is not allowed on `raw` files. -- **`ram`** - `File` must be `t:iodata/0`. Returns an `t:fd/0`, which lets - module `file` operate on the data in-memory as if it is a file. +- **`ram`** - `File` must be `t:iodata/0`. Returns an `t:fd/0` (or a `t:io_server/0` + if combined with `cooked`), which lets module `file` operate on the data in-memory + as if it is a file. - **`cooked`** - Only meaningful together with `ram`. When specified, the - ram file is wrapped in a pid-based I/O server, making it compatible with - the `m:io` module (for example, `io:get_line/2`, `io:scan_erl_form/3`). - Without `cooked`, ram files return a raw file descriptor that cannot be - used with `m:io` functions. + ram file is wrapped in a pid-based I/O server, making it compatible with the + `m:io` module (for example, `io:get_line/2`, `io:scan_erl_form/3`). + + Since OTP @OTP-123456789@ - **`sync`** - On platforms supporting it, enables the POSIX `O_SYNC` synchronous I/O flag or its platform-dependent equivalent (for example, From 13b55f864df284f685cd9acffc2510eb8784df52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Tue, 17 Mar 2026 16:08:40 +0100 Subject: [PATCH 16/16] more epp fixes --- lib/kernel/src/file.erl | 2 +- lib/stdlib/src/epp.erl | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/kernel/src/file.erl b/lib/kernel/src/file.erl index be0a8cb01bab..6add4f5b660b 100644 --- a/lib/kernel/src/file.erl +++ b/lib/kernel/src/file.erl @@ -1591,7 +1591,7 @@ more of the following options: This option is not allowed on `raw` files. -- **`ram`** - `File` must be `t:iodata/0`. Returns an `t:fd/0` (or a `t:io_server/0` +- **`ram`**{: #ram } - `File` must be `t:iodata/0`. Returns an `t:fd/0` (or a `t:io_server/0` if combined with `cooked`), which lets module `file` operate on the data in-memory as if it is a file. diff --git a/lib/stdlib/src/epp.erl b/lib/stdlib/src/epp.erl index 065007070e99..18c5f5bf1863 100644 --- a/lib/stdlib/src/epp.erl +++ b/lib/stdlib/src/epp.erl @@ -92,26 +92,26 @@ Module:format_error(ErrorDescriptor) -doc """ The `t:option/0` are the options that can be used to customize the preprocessing. -* `{default_encoding, DefEncoding}` - sets the default encoding of the file. The default encoding is +* **`{default_encoding, DefEncoding}`** - sets the default encoding of the file. The default encoding is used if no valid encoding is found in the file. The valid encodings are `latin1` and `utf8`, where the case of the characters can be chosen freely. If unset, it defaults to `utf8`. -* `{includes, IncludePath}` - sets the include path for the file. The include path is used to resolve +* **`{includes, IncludePath}`** - sets the include path for the file. The include path is used to resolve `-include`, `-include_lib` directives. -* `{source_name, SourceName}` - sets the file name of the implicit -file() attributes inserted +* **`{source_name, SourceName}`** - sets the file name of the implicit -file() attributes inserted during preprocessing. If unset it will default to the name of the opened file. -* `{macros, PredefMacros}` - sets the predefined `t:macros/0` for the file. `PredefMacros` is a list of +* **`{macros, PredefMacros}`** - sets the predefined `t:macros/0` for the file. `PredefMacros` is a list of macros that are defined before preprocessing starts. -* `{deterministic, Enabled}` - if set to `true`, will reduce the file name of the +* **`{deterministic, Enabled}`** - if set to `true`, will reduce the file name of the implicit -file() attributes inserted during preprocessing to only the basename of the path. -* `{extra, Enabled}` - if set to `true`, the return value is `{ok, Epp, Extra}` instead of `{ok, Epp}`, +* **`{extra, Enabled}`** - if set to `true`, the return value is `{ok, Epp, Extra}` instead of `{ok, Epp}`, where `Extra` contains which encoding was detected from the file. -* `{location, StartLocation}` - sets the initial location of the file. The option `location` is forwarded +* **`{location, StartLocation}`** - sets the initial location of the file. The option `location` is forwarded to the Erlang token scanner, see [`erl_scan:tokens/3,4`](`erl_scan:tokens/3`). For example: ```erlang @@ -127,14 +127,14 @@ The `t:option/0` are the options that can be used to customize the preprocessing {eof,{28,1}}]} ``` -* `{fd, FileDescriptor}` - use an already opened file descriptor to read from instead of a file name. +* **`{fd, FileDescriptor}`** - use an already opened file descriptor to read from instead of a file name. The file descriptor is expected to be an `t:file:io_server/0`. This enables in-memory preprocessing using [ram files](`m:file#ram`), where the main source file can be served from memory without touching disk. For example: See `parse_file/2` for an example of how to use this option. -* `{include_path_open, Fun}` - provide a custom function for opening include files. The function has the +* **`{include_path_open, Fun}`** - provide a custom function for opening include files. The function has the same signature as `file:path_open/3` and is used when resolving `-include`, `-include_lib`, and `-doc {file, ...}` directives. This enables fully in-memory preprocessing using [ram files](`m:file#ram`). For example: @@ -162,7 +162,7 @@ The `t:option/0` are the options that can be used to customize the preprocessing ``` Since OTP @OTP-12345678@ -* `{compiler_internal,term()}` - forwarded to the Erlang token +* **`{compiler_internal,term()}`** - forwarded to the Erlang token scanner, see [`{compiler_internal,term()}`](`m:erl_scan#compiler_interal`) in `erl_scan:string/3`. """. -type option() ::