File 5401-compiler-implementation-of-EEP-59.patch of Package erlang

From f2f48e329827b1750a39cced3cd4a27183e944af Mon Sep 17 00:00:00 2001
From: Kiko Fernandez-Reyes <kiko@erlang.org>
Date: Fri, 15 Sep 2023 16:55:47 +0200
Subject: [PATCH] compiler: implementation of EEP-59

Implementation of EEP-59 - Documentation Attributes

- Documentation attributes are added to the binary beam file, following
format of [EEP-48](https://www.erlang.org/eeps/eep-0048), via
`+beam_docs` compiler flag

- Warnings related to documentation attributes are dealt with in the
`beam_docs.erl` instead of adding them to `erl_lint.erl`

*Example 1*

```erlang
-module(warn_missing_doc).

-export([test/0, test/1, test/2]).
-export_type([test/0, test/1]).

-type test() :: ok.
-type test(N) :: N.

-callback test() -> ok.

-include("warn_missing_doc.hrl").

test() -> ok.
test(N) -> N.
```

Using the compiler flag `warn_missing_doc` will raise a warning when
doc. attributes are missing in exported functions, types, and callbacks.

*Example 2*

```erlang
-module(doc_with_file).

-export([main/1]).

-moduledoc {file, "README"}.

-doc {file, "FUN"}.
-spec main(Var) -> foo(Var).
main(X) ->
    X.
```

`moduledoc`s and `doc`s may refer to external files to be embedded.

*Example 3 - Warnings and Types*

```erlang
-export([uses_public/0]).
-export_type([public/0]).

-doc false.
-type hidden_type() :: integer().

-type intermediate() :: hidden_type().
-type public() :: intermediate().

-spec uses_public() -> public().
uses_public() ->
    ok.
```

Compiler warns about exported functions whose specs refer to hidden
types. In the example above, the `hidden_type()` is set as `hidden`
either via `-doc false` or `-doc hidden` and `public() :> intermediate()
:> hidden_type()`. When documentation attributes mark a type as hidden,
they won't be part of the documentation. Thus, the warning that the
`hidden_type()` is not part of the documentation, yet used in an
exported function.
---
 bootstrap/lib/compiler/ebin/beam_doc.beam     |  Bin 0 -> 17580 bytes
 bootstrap/lib/compiler/ebin/compile.beam      |  Bin 39052 -> 39736 bytes
 .../lib/kernel/ebin/erl_erts_errors.beam      |  Bin 23652 -> 23660 bytes
 bootstrap/lib/kernel/ebin/group.beam          |  Bin 16816 -> 17964 bytes
 bootstrap/lib/kernel/ebin/prim_tty.beam       |  Bin 22544 -> 22564 bytes
 bootstrap/lib/stdlib/ebin/argparse.beam       |  Bin 22908 -> 22908 bytes
 bootstrap/lib/stdlib/ebin/beam_lib.beam       |  Bin 19240 -> 19424 bytes
 bootstrap/lib/stdlib/ebin/c.beam              |  Bin 18820 -> 19204 bytes
 bootstrap/lib/stdlib/ebin/edlin.beam          |  Bin 16156 -> 16248 bytes
 bootstrap/lib/stdlib/ebin/edlin_context.beam  |  Bin 11864 -> 11884 bytes
 bootstrap/lib/stdlib/ebin/edlin_expand.beam   |  Bin 26256 -> 26296 bytes
 bootstrap/lib/stdlib/ebin/edlin_key.beam      |  Bin 3940 -> 3972 bytes
 bootstrap/lib/stdlib/ebin/epp.beam            |  Bin 32072 -> 33824 bytes
 bootstrap/lib/stdlib/ebin/erl_lint.beam       |  Bin 91388 -> 92028 bytes
 bootstrap/lib/stdlib/ebin/erl_parse.beam      |  Bin 179332 -> 179996 bytes
 erts/preloaded/src/Makefile                   |    2 +-
 lib/compiler/doc/src/compile.xml              |   29 +
 lib/compiler/src/Makefile                     |    1 +
 lib/compiler/src/beam_doc.erl                 | 1073 +++++++++++++++++
 lib/compiler/src/compile.erl                  |   81 +-
 lib/compiler/src/compiler.app.src             |    3 +-
 lib/compiler/test/Makefile                    |    1 +
 lib/compiler/test/beam_doc_SUITE.erl          |  603 +++++++++
 lib/compiler/test/beam_doc_SUITE_data/FUN     |    3 +
 lib/compiler/test/beam_doc_SUITE_data/README  |    3 +
 lib/compiler/test/beam_doc_SUITE_data/TYPES   |    3 +
 .../all_string_formats.erl                    |   20 +
 .../test/beam_doc_SUITE_data/callback.erl     |   69 ++
 .../test/beam_doc_SUITE_data/deprecated.erl   |   46 +
 .../beam_doc_SUITE_data/doc_with_file.erl     |   25 +
 .../doc_with_file_error.erl                   |   24 +
 .../test/beam_doc_SUITE_data/docformat.erl    |   18 +
 .../docmodule_with_doc_attributes.erl         |   36 +
 .../test/beam_doc_SUITE_data/equiv.erl        |   10 +
 .../test/beam_doc_SUITE_data/export_all.erl   |   31 +
 .../test/beam_doc_SUITE_data/folder/FILE      |    3 +
 .../folder/doc_with_file.hrl                  |    1 +
 .../beam_doc_SUITE_data/hide_moduledoc.erl    |   15 +
 .../beam_doc_SUITE_data/hide_moduledoc2.erl   |   25 +
 .../beam_doc_SUITE_data/private_types.erl     |   65 +
 .../singleton_docformat.erl                   |   21 +
 .../beam_doc_SUITE_data/singleton_meta.erl    |   17 +
 .../test/beam_doc_SUITE_data/singletondoc.erl |   19 +
 .../singletonmoduledoc.erl                    |    7 +
 .../test/beam_doc_SUITE_data/skip_doc.erl     |   19 +
 .../test/beam_doc_SUITE_data/slogan.erl       |   73 ++
 .../test/beam_doc_SUITE_data/spec.erl         |   24 +
 .../beam_doc_SUITE_data/spec_switch_order.erl |   54 +
 .../beam_doc_SUITE_data/types_and_opaques.erl |  147 +++
 .../beam_doc_SUITE_data/types_and_opaques.hrl |    2 +
 .../beam_doc_SUITE_data/user_defined_type.erl |    3 +
 .../beam_doc_SUITE_data/user_defined_type.hrl |    2 +
 .../beam_doc_SUITE_data/warn_missing_doc.erl  |   14 +
 .../beam_doc_SUITE_data/warn_missing_doc.hrl  |    2 +
 lib/compiler/test/compile_SUITE.erl           |   27 +-
 .../test/compile_SUITE_data/simple-basic1.mk  |    2 +-
 .../test/compile_SUITE_data/simple-basic2.mk  |    2 +-
 .../test/compile_SUITE_data/simple-missing.mk |    2 +-
 .../test/compile_SUITE_data/simple-phony.mk   |    4 +-
 .../test/compile_SUITE_data/simple-target1.mk |    2 +-
 .../test/compile_SUITE_data/simple-target2.mk |    2 +-
 .../test/compile_SUITE_data/simple.erl        |    4 +
 .../test/compile_SUITE_data/unicode-0.md      |    1 +
 .../priv/bin/validate_links.escript           |   13 +-
 lib/stdlib/doc/src/beam_lib.xml               |   12 +-
 lib/stdlib/src/beam_lib.erl                   |   24 +-
 lib/stdlib/src/epp.erl                        |   78 +-
 lib/stdlib/src/erl_lint.erl                   |   87 +-
 lib/stdlib/src/erl_parse.yrl                  |   41 +
 lib/stdlib/src/stdlib.app.src                 |    2 +-
 lib/stdlib/test/beam_lib_SUITE.erl            |    2 +-
 lib/stdlib/test/epp_SUITE.erl                 |   53 +-
 lib/stdlib/test/erl_lint_SUITE.erl            |  104 ++
 make/otp.mk.in                                |    2 +-
 system/doc/reference_manual/Makefile          |    3 +
 system/doc/reference_manual/documentation.md  |  414 +++++++
 system/doc/reference_manual/modules.xml       |   66 +-
 system/doc/reference_manual/part.xml          |    1 +
 system/doc/reference_manual/xmlfiles.mk       |    1 +
 79 files changed, 3460 insertions(+), 83 deletions(-)
 create mode 100644 bootstrap/lib/compiler/ebin/beam_doc.beam
 create mode 100644 lib/compiler/src/beam_doc.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/FUN
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/README
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/TYPES
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/all_string_formats.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/callback.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/deprecated.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/doc_with_file.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/doc_with_file_error.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/docformat.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/docmodule_with_doc_attributes.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/equiv.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/export_all.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/folder/FILE
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/folder/doc_with_file.hrl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc2.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/private_types.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/singleton_docformat.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/singleton_meta.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/singletondoc.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/singletonmoduledoc.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/skip_doc.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/slogan.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/spec.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/spec_switch_order.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.hrl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/user_defined_type.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/user_defined_type.hrl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.erl
 create mode 100644 lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.hrl
 create mode 100644 lib/compiler/test/compile_SUITE_data/unicode-0.md
 create mode 100644 system/doc/reference_manual/documentation.md

diff --git a/erts/preloaded/src/Makefile b/erts/preloaded/src/Makefile
index 1994aa1302..4d742b06dc 100644
--- a/erts/preloaded/src/Makefile
+++ b/erts/preloaded/src/Makefile
@@ -84,7 +84,7 @@ KERNEL_SRC=$(ERL_TOP)/lib/kernel/src
 KERNEL_INCLUDE=$(ERL_TOP)/lib/kernel/include
 STDLIB_INCLUDE=$(ERL_TOP)/lib/stdlib/include
 
-ERL_COMPILE_FLAGS += +debug_info -I$(KERNEL_SRC) -I$(KERNEL_INCLUDE)
+ERL_COMPILE_FLAGS += +debug_info +no_docs -I$(KERNEL_SRC) -I$(KERNEL_INCLUDE)
 
 ifeq ($(ERL_DETERMINISTIC),yes)
 	ERL_COMPILE_FLAGS += +deterministic
diff --git a/lib/compiler/doc/src/compile.xml b/lib/compiler/doc/src/compile.xml
index d7c5e34dd2..b601baf07d 100644
--- a/lib/compiler/doc/src/compile.xml
+++ b/lib/compiler/doc/src/compile.xml
@@ -146,6 +146,16 @@
 	      exception at runtime).</p>
           </item>
 
+          <tag><c>no_docs</c><marker id="beam_docs"/></tag>
+          <item>
+            <p>The compiler by default extracts <seeguide marker="system/reference_manual:documentation">documentation</seeguide> from
+              <seeguide marker="system/reference_manual:modules#documentation-attributes"><c>-doc</c> attributes</seeguide>
+              and places them in the <seetype marker="stdlib:beam_lib#chunkid"><c>Docs</c> chunk</seetype> according to <seeguide marker="kernel:eep48_chapter">EEP-48</seeguide>.
+            </p>
+            <p>This option switches off the placement of <seeguide marker="system/reference_manual:modules#documentation-attributes"><c>-doc</c> attributes</seeguide>
+            in the <seetype marker="stdlib:beam_lib#chunkid"><c>Docs</c> chunk</seetype></p>.
+          </item>
+
           <tag><c>binary</c></tag>
           <item>
             <p>The compiler returns the object code in a
@@ -875,6 +885,25 @@ module.beam: module.erl \
                 warnings.</p>
           </item>
 
+          <tag><c>warn_missing_doc</c><marker id="warn_missing_doc"/></tag>
+          <item>
+              <p>By default, warnings are not emitted when <c>-doc</c>
+                attribute for an exported function is not given. Use this
+                option to turn on this kind of warning.</p>
+          </item>
+
+          <tag><c>nowarn_hidden_doc</c> | <c>{nowarn_hidden_doc,NAs}</c>
+          <marker id="nowarn_hidden_doc"/></tag>
+          <item>
+              <p>By default, warnings are emitted when <c>-doc false</c>
+              attribute is set on a <seeguide marker="system/reference_manual:documentation#What-is-visible-versus-hidden">callback or referenced type</seeguide>. You can set
+              <c>nowarn_hidden_doc</c> to suppress all those warnings,
+              or <c>{nowarn_hidden_doc, NAs}</c> to suppress specific
+              callbacks or types. <c>NAs</c> is a tuple <c>{Name, Arity}</c>
+              or a list of such tuples.
+              </p>
+          </item>
+
           <tag><c>warn_missing_spec</c></tag>
           <item>
               <p>By default, warnings are not emitted when a specification
diff --git a/lib/compiler/src/Makefile b/lib/compiler/src/Makefile
index a33d14f2d5..297f6b1253 100644
--- a/lib/compiler/src/Makefile
+++ b/lib/compiler/src/Makefile
@@ -56,6 +56,7 @@ MODULES =  \
 	beam_dict \
 	beam_digraph \
 	beam_disasm \
+	beam_doc \
 	beam_flatten \
 	beam_jump \
 	beam_listing \
diff --git a/lib/compiler/src/beam_doc.erl b/lib/compiler/src/beam_doc.erl
new file mode 100644
index 0000000000..cf2726bbe5
--- /dev/null
+++ b/lib/compiler/src/beam_doc.erl
@@ -0,0 +1,1073 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2023-2028. 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%
+%%
+%% Purpose : Generate documentation as per EEP-48
+%%
+%% Pass to generate EEP-48 format for beam files.
+%%
+%% Example:
+%%
+%% 1> compile:file(test).
+%%
+
+-module(beam_doc).
+
+-feature(maybe_expr, enable).
+
+-export([main/4, format_error/1]).
+
+-import(lists, [foldl/3, all/2, map/2, filter/2, reverse/1, join/2, filtermap/2,
+                uniq/2, member/2, flatten/1]).
+
+-include_lib("kernel/include/eep48.hrl").
+
+-moduledoc false.
+
+-define(DEFAULT_MODULE_DOC_LOC, 1).
+-define(DEFAULT_FORMAT, <<"text/markdown">>).
+
+
+-record(docs, {%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+               %%
+               %% PREPROCESSOR FIELDS
+               %%
+               %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+               %%
+               %% These fields are used in a first pass to preprocess the AST.
+               %% The fields are considered the source of truth.
+               %%
+               %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+               cwd                 :: file:filename(),             % Cwd
+               filename            :: file:filename(),
+               curr_filename       :: file:filename(),
+               opts                :: [opt()],
+
+               module              :: module(),
+               deprecated = #{}    :: map(),
+
+               docformat = ?DEFAULT_FORMAT :: binary(),
+               moduledoc = {?DEFAULT_MODULE_DOC_LOC, none} :: {integer() | erl_anno:anno(), none | map() | hidden},
+               moduledoc_meta = none :: none | #{ otp_doc_vsn => tuple() },
+
+               %% tracks exported functions from multiple `-export([...])`
+               exported_functions = sets:new() :: sets:set({FunName :: atom(), Arity :: non_neg_integer()}),
+
+               %% tracks exported type from multiple `-export_type([...])`
+               exported_types     = sets:new() :: sets:set({TypeName :: atom(), Arity :: non_neg_integer()}),
+
+               %% tracks type_defs to point to their creation annotation. used for throwing warnings
+               %% about type definitions that are unreachable
+               type_defs          = #{}        :: #{{TypeName :: atom(), Arity :: non_neg_integer()} := erl_anno:anno()},
+
+               %% helper field to track hidden types
+               hidden_types = sets:new() :: sets:set({Name :: atom(), Arity :: non_neg_integer()}),
+
+               %% user defined types that need to be shown in the documentation. these are types that are not
+               %% exported but that the documentation needs to show because exported functions referred to them.
+               user_defined_types = sets:new() :: sets:set({TypeName :: atom(), Arity :: non_neg_integer()}),
+
+               %% used to report warnings of types in exported functions where the types
+               %% may have been set to hidden with a documentation attribute.
+               types_from_exported_funs = #{} :: #{{TypeName :: atom(), Arity :: non_neg_integer()} := [erl_anno:anno()]},
+
+               %% tracks the reachable type graph, i.e., type dependencies
+               type_dependency = digraph:new() :: digraph:graph(),
+
+               %% track any records found so that we can track
+               records = #{} :: #{ atom() => term() },
+
+               % keeps track of `-compile(export_all)`
+               export_all         = false :: boolean(),
+
+               %% slogans: used to create slogans from it.
+               slogans = #{} :: #{{FunName    :: atom(), Arity      :: non_neg_integer()}
+                                  => {FunName    :: atom(),
+                                      ListOfVars :: [atom()],
+                                      Arity      :: non_neg_integer()}},
+
+               %% populates all function / types, callbacks. it is updated on an ongoing basis
+               %% since a doc attribute `doc ...` is not known in a first pass to be attached
+               %% to a function / type / callback.
+               docs = #{} :: #{{Attribute :: function | type | opaque | callback,
+                                FunName :: atom(),
+                                Arity :: non_neg_integer()}
+                               =>
+                                  {Status :: none | {hidden, erl_anno:anno()}  | set,
+                                   Documentation :: none | {DocText :: unicode:chardata(), Anno :: erl_anno:anno()},
+                                   Meta :: map()}},
+
+               %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+               %%
+               %% DOCUMENTATION TRACKING
+               %%
+               %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+               %%
+               %% Documentation attributes of the form `-doc ...` are not known
+               %% to be attached to the callback / function / type until reading
+               %% the next line. The following fields keep track of this state.
+               %% As soon as this state is known to be attached to a type / callback/ function,
+               %% this state should be saved in the `docs` field, which is a mapping
+               %% of {function(), arity()} => {...} e.g. contains hidden fields, lines,
+               %% documentation text, etc.
+               %%
+               %% one cannot rely on the fields below to keep track of documentation,
+               %% as Erlang allows pretty unstructure code.
+               %%
+               %% e.g.,
+               %%
+               %% -doc false.
+               %% -spec foo() -> ok.
+               %%
+               %% -spec bar() -> ok.
+               %%
+               %% -doc #{author => "X"}.
+               %% -doc foo() -> ok.
+               %%
+               %% thus, after reading a terminal AST node (spec, type, fun declaration, opaque, callback),
+               %% the intermediate state saveed in the fields below needs to be
+               %% saved in the `docs` field.
+
+               hidden_status = none :: none | hidden,
+
+               % Function/type/callback local doc. either none of some string was added
+               %% Stateful since the documentation is a two-step process.
+               %% First the documentation is entered, and the next terminal item (callback, fun, or type)
+               %% determines to which element the documentation gets attached to.
+               %%
+               %% When getting to a terminal item, the documentation and its status gets attached
+               %% to a terminal item in the global map `docs`.
+               doc    = none  :: none
+                               | {DocText :: unicode:chardata(), Anno :: erl_anno:anno()} ,
+
+               %% track if the doc was never added (none), marked hidden (-doc hidden)
+               %% or entered (-doc "..."). If entered, doc_status = set, and doc = "...".
+               %% this field is needed because one we do the following:
+               %%
+               %% -doc hidden.
+               %% -doc "This is a hidden function".
+               %%
+               %% Alternatively, one can merge `doc` and `doc_status` as:
+               %%
+               %% doc = none | {hidden, "" | none} | "".
+               %%
+               %% The order in which `-doc hidden.` and `-doc "documentation here"` is written
+               %% is not defined, so one cannot assume that the following order:
+               %%
+               %% -doc "This is a hidden function".
+               %% -doc hidden.
+               %%
+               %% Because of this, we use two fields to keep track of documentation.
+               doc_status = none :: none  | {hidden, erl_anno:anno()} | set,
+
+               % Function/type/callback local meta.
+               %% exported => boolean(), keeps track of types that are private but used in public functions
+               %% thus, they must be considered as exported for documentation purposes.
+               %% only useful when processing types. thus, it must be remove from functions and callbacks.
+               %% Stateful, need to be fixed as docs.
+               meta   = #{exported => false} :: map(),
+
+               %% on analysing the AST, and upon finding a spec of a exported
+               %% function, the types from the spec are added to the field
+               %% below. if the function to which the spec belongs to is hidden,
+               %% we purge types from this field. if the function to which the
+               %% specs belong to are not hidden, they are added to
+               %% user_defined_types. Essentially, `last_read_user_types` is a
+               %% queue that accumulates types until they can be promoted to
+               %% `user_defined_types` or purged (removed).
+               %%
+               %% RATIONALE / DESIGN
+               %%
+               %% this field keeps track of these types until we reach the
+               %% function definition, which means that we already know if the
+               %% function sets `-doc false.`. upon having this information, we
+               %% can discard the user defined types when the function uses
+               %% `-doc false.` (hidden), since that means that the function
+               %% should not be displayed in the docs. if the function is not
+               %% hidden, we add the user defined types to the field
+               %% `user_defined_types` as these can be (non-)exported types. if
+               %% the types are exported, the docs will show the type
+               %% definition. if the types are not exported, the type definition
+               %% will be shown as not exported.
+               last_read_user_types = #{},
+
+               %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+               %%
+               %% RESULT
+               %%
+               %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+               ast_fns = [] :: list(),
+               ast_types = [] :: list(),
+               ast_callbacks = [] :: list(),
+               warnings = [] :: warnings()
+              }).
+
+-type internal_docs() :: #docs{}.
+-type opt() :: warn_missing_doc | nowarn_hidden_doc | {nowarn_hidden_doc, {atom(), arity()}}.
+-type kfa() :: {Kind :: function | type | callback, Name :: atom(), Arity :: arity()}.
+-type warnings() :: [{file:filename(),
+                      [{erl_anno:location(), beam_doc, warning()}]}].
+-type warning() :: {missing_doc, kfa()} | missing_moduledoc |
+                   {hidden_type_used_in_exported_fun | hidden_callback, {Name :: atom(), arity()}}.
+
+
+-doc "
+Transforms an Erlang abstract syntax form into EEP-48 documentation format.
+".
+-spec main(file:filename(), file:filename(), [erl_parse:abstract_form()], [opt()]) ->
+          {ok, #docs_v1{}, warnings()}.
+main(Dirname, Filename, AST, CmdLineOpts) ->
+    Opts = extract_opts(AST, CmdLineOpts),
+    State0 = new_state(Dirname, Filename, Opts),
+    State1 = preprocessing(AST, State0),
+    Docs = extract_documentation(AST, State1),
+    {ModuleDocAnno, ModuleDoc} = Docs#docs.moduledoc,
+    DocV1 = #docs_v1{},
+    Result = DocV1#docs_v1{ format = Docs#docs.docformat,
+                            anno = ModuleDocAnno,
+                            metadata = Docs#docs.moduledoc_meta,
+                            module_doc = ModuleDoc,
+                            docs = process_docs(Docs) },
+   {ok, Result, Docs#docs.warnings }.
+
+extract_opts(AST, CmdLineOpts) ->
+    CompileOpts = lists:flatten([C || {attribute,_,compile,C} <- AST]),
+    CompileOpts ++ CmdLineOpts.
+
+-spec format_error(warning()) -> io_lib:chars().
+format_error({hidden_type_used_in_exported_fun, {Type, Arity}}) ->
+    io_lib:format("hidden type '~p/~p' used in exported function",
+                  [Type, Arity]);
+format_error({hidden_callback, {Name, Arity}}) ->
+    io_lib:format("hidden callback '~p/~p' used", [Name, Arity]);
+format_error({missing_doc, {Kind, Name, Arity}}) ->
+    io_lib:format("missing -doc for ~w ~tw/~w", [Kind, Name, Arity]);
+format_error(missing_moduledoc) ->
+    io_lib:format("missing -moduledoc", []).
+
+process_docs(#docs{ast_callbacks = AstCallbacks, ast_fns = AstFns, ast_types = AstTypes}) ->
+    AstTypes ++ AstCallbacks ++ AstFns.
+
+
+preprocessing(AST, State) ->
+   PreprocessingFuns = fun (AST0, State0) ->
+                             Funs = [% Order matters
+                                     fun extract_deprecated/2,
+                                     fun extract_exported_types0/2, % done
+                                     fun extract_slogan_from_spec0/2,%done
+                                     fun track_documentation/2,      %must be before upsert_documentation_from_terminal_item/2
+                                     fun upsert_documentation_from_terminal_item/2,
+                                     fun extract_docformat0/2, %done
+                                     fun extract_moduledoc0/2, %done
+                                     fun extract_module_meta/2, %done
+                                     fun extract_exported_funs/2, %done
+                                     fun extract_file/2, %done
+                                     fun extract_record/2,
+                                     fun extract_hidden_types0/2, %done
+                                     fun extract_type_defs0/2,    %done
+                                     fun extract_type_dependencies/2],
+                             foldl(fun (F, State1) -> F(AST0, State1) end, State0, Funs)
+                       end,
+   foldl(PreprocessingFuns, State, AST).
+
+extract_deprecated({attribute, Anno, deprecated, {F, A}}, State) ->
+    extract_deprecated({attribute, Anno, deprecated, {F, A, undefined}}, State);
+extract_deprecated({attribute, _, deprecated, {F, A, Reason}}, State) ->
+    Deprecations = (State#docs.deprecated)#{ {function, F, A} => Reason },
+    State#docs{ deprecated = Deprecations };
+extract_deprecated({attribute, Anno, deprecated_type, {F, A}}, State) ->
+    extract_deprecated({attribute, Anno, deprecated_type, {F, A, undefined}}, State);
+extract_deprecated({attribute, _, deprecated_type, {F, A, Reason}}, State) ->
+    Deprecations = (State#docs.deprecated)#{ {type, F, A} => Reason },
+    State#docs{ deprecated = Deprecations };
+extract_deprecated(_, State) ->
+   State.
+
+extract_exported_types0({attribute,_ANNO,export_type,ExportedTypes}, State) ->
+   update_export_types(State, ExportedTypes);
+extract_exported_types0({attribute,_ANNO,module, Module}, State) ->
+    State#docs{ module = Module };
+extract_exported_types0({attribute,_ANNO,compile, export_all}, State) ->
+   update_export_all(State, true);
+extract_exported_types0(_AST, State) ->
+   State.
+
+extract_slogan_from_spec0({attribute, Anno, Tag, Form}, State) when Tag =:= spec; Tag =:= callback ->
+   maybe
+      {Name, Arity, Args} = extract_args_from_spec(Form),
+      true ?= is_list(Args),
+
+      Vars = foldl(fun (_, false) -> false;
+                       ({var, _, Var}, Vars) -> [Var | Vars];
+                       ({ann_type, _, [{var, _, Var} | _]}, Vars) -> [Var | Vars];
+                       (_, _) -> false
+                   end, [], Args),
+
+      true ?= is_list(Vars),
+      Arity ?= length(Vars),
+      update_slogan0(State, Anno, {Name, reverse(Vars), Arity})
+   else
+      _ ->
+         State
+   end;
+extract_slogan_from_spec0(_, State) ->
+   State.
+
+%%
+%% extract arguments for the slogan from the spec.
+%% does not accept multi-clause callbacks / specs due to the ambiguity
+%% of which spec clause to choose.
+%%
+extract_args_from_spec({{Name, Arity}, Types}) ->
+   case Types of
+      [{type, _, 'fun', [{type, _, product, Args}, _Return]}] ->
+         {Name, Arity, Args};
+      [{type, _, bounded_fun, [Args, _Constraints]}] ->
+         extract_args_from_spec({{Name, Arity}, [Args]});
+      _ ->
+         {Name, Arity, false}
+   end;
+extract_args_from_spec({{_Mod, Name, Arity}, Types}) ->
+   extract_args_from_spec({{Name, Arity}, Types}).
+
+update_slogan0(#docs{slogans = Slogans}=State, _Anno, {FunName, Vars, Arity}=Slogan)
+  when is_atom(FunName) andalso is_list(Vars) andalso is_number(Arity) ->
+   State#docs{slogans = Slogans#{{FunName, Arity} => Slogan}}.
+
+
+%% Documentation tracking is a two-step (stateful phase).
+%% First phase (this one) saves documentation attributes to fields
+%% until reaching a terminal element where the docs are gathered globally.
+track_documentation({attribute, _Anno, doc, Meta0}, State) when is_map(Meta0) ->
+   Meta1 = case Meta0 of
+              #{ equiv := {call,_,_Equiv,_Args}=Equiv} ->
+                 Meta0#{ equiv := unicode:characters_to_binary(erl_pp:expr(Equiv)) };
+              #{ equiv := {Func,Arity}} ->
+                 Meta0#{ equiv := unicode:characters_to_binary(io_lib:format("~p/~p",[Func,Arity])) };
+              _ ->
+                 Meta0
+           end,
+   State1 = update_meta(State, Meta1),
+   update_doc(State1, none);
+track_documentation({attribute, Anno, doc, DocStatus}, State)
+  when DocStatus =:= hidden; DocStatus =:= false ->
+   update_docstatus(State, {hidden, set_file_anno(Anno, State)});
+track_documentation({attribute, Anno, doc, Doc}, State) when is_list(Doc)  ->
+   update_doc(State, {Doc, Anno});
+track_documentation({attribute, Anno, doc, Doc}, State) when is_binary(Doc) ->
+   update_doc(State, {unicode:characters_to_list(Doc), Anno});
+track_documentation(_, State) ->
+   State.
+
+upsert_documentation_from_terminal_item({function, _Anno, F, Arity, _}, State) ->
+   upsert_documentation(function, F, Arity, State);
+upsert_documentation_from_terminal_item({attribute, _Anno, TypeOrOpaque, {TypeName, _TypeDef, TypeArgs}},State)
+  when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+   Arity = length(fun_to_varargs(TypeArgs)),
+   upsert_documentation(type, TypeName, Arity, State);
+upsert_documentation_from_terminal_item({attribute, _Anno, callback, {{CB, Arity}, _Form}}, State) ->
+   upsert_documentation(callback, CB, Arity, State);
+upsert_documentation_from_terminal_item(_, State) ->
+   State.
+
+upsert_documentation(Tag, Name, Arity, State) when Tag =:= function;
+                                                   Tag =:= type;
+                                                   Tag =:= opaque;
+                                                   Tag =:= callback ->
+   Docs = State#docs.docs,
+   State1 = case maps:get({Tag, Name, Arity}, Docs, none) of
+               none ->
+                  Status = State#docs.doc_status,
+                  Doc = State#docs.doc,
+                  Meta = State#docs.meta,
+                  State#docs{docs = Docs#{{Tag, Name, Arity} => {Status, Doc, Meta}}};
+               {Status, Documentation, Meta} ->
+                  Status1 = upsert_state(Status, State#docs.doc_status),
+                  Doc = upsert_doc(Documentation, State#docs.doc),
+                  Meta1 = upsert_meta(Meta, State#docs.meta),
+                  State#docs{docs = Docs#{{Tag, Name, Arity} := {Status1, Doc, Meta1}}}
+   end,
+   reset_state(State1).
+
+%% Keep status unless there is a change.
+upsert_state({hidden, _}=Hidden, _) ->
+   Hidden;
+upsert_state(Status, none) ->
+   Status;
+upsert_state(_Status, Tag) ->
+   case Tag of
+      {hidden, _} ->
+         Tag;
+      set ->
+         Tag
+   end.
+
+upsert_doc(Documentation, none) ->
+   Documentation;
+upsert_doc(_, Documentation) ->
+   Documentation.
+
+upsert_meta(Meta0, Meta1) ->
+   maps:merge(Meta0, Meta1).
+
+
+extract_docformat0({attribute, _ModuleDocAnno, moduledoc, MetaFormat}, State) when is_map(MetaFormat) ->
+    case maps:get(format, MetaFormat, not_found) of
+        not_found -> State;
+        Format when is_list(Format) -> State#docs{docformat = unicode:characters_to_binary(Format)};
+        Format when is_binary(Format) -> State#docs{docformat = Format}
+    end;
+extract_docformat0(_, State) ->
+    State.
+
+%%
+%% Sets module documentation attributes
+%%
+extract_moduledoc0({attribute, ModuleDocAnno, moduledoc, false}, State) ->
+   extract_moduledoc0({attribute, ModuleDocAnno, moduledoc, hidden}, State);
+extract_moduledoc0({attribute, ModuleDocAnno, moduledoc, hidden}, State) ->
+   State#docs{moduledoc = {ModuleDocAnno, create_module_doc(hidden)}};
+extract_moduledoc0({attribute, ModuleDocAnno, moduledoc, ModuleDoc}, State) when is_list(ModuleDoc) ->
+   Doc = unicode:characters_to_binary(string:trim(ModuleDoc)),
+   State#docs{moduledoc = {set_file_anno(ModuleDocAnno, State), create_module_doc(Doc)}};
+extract_moduledoc0(_, State) ->
+   State.
+
+
+extract_module_meta({attribute, _ModuleDocAnno, moduledoc, MetaDoc}, State) when is_map(MetaDoc) ->
+   State#docs{moduledoc_meta = maps:merge(State#docs.moduledoc_meta, MetaDoc)};
+extract_module_meta(_, State) ->
+   State.
+
+extract_exported_funs({attribute,_ANNO,export,ExportedFuns}, State) ->
+   update_export_funs(State, ExportedFuns);
+extract_exported_funs(_, State) ->
+   State.
+
+
+%% Sets the filename based on the module
+extract_file({attribute, _Anno, file, {Filename, _A}}, State) ->
+   update_filename(State, Filename);
+extract_file(_, State) ->
+   State.
+
+extract_record({attribute, Anno, record, {Name, Fields}}, State) ->
+    TypeFields = filtermap(
+                   fun({typed_record_field, RecordField, Type}) ->
+                           {true, {type, Anno, field_type, [element(3, RecordField), Type]}};
+                      (_) ->
+                           false
+                   end, Fields),
+    State#docs{ records = (State#docs.records)#{ Name => TypeFields } };
+extract_record(_, State) ->
+   State.
+
+%%
+%% Extracts types with documentation attribute set to `hidden` or `false`.
+%%
+%% E.g.:
+%%
+%%    -doc hidden.
+%%    -type foo() :: integer().
+%%
+extract_hidden_types0({attribute, _Anno, doc, DocStatus}, State) when
+   DocStatus =:= hidden; DocStatus =:= false ->
+   State#docs{hidden_status = hidden};
+extract_hidden_types0({attribute, _Anno, doc, _}, State) ->
+   State;
+extract_hidden_types0({attribute, _Anno, TypeOrOpaque, {Name, _Type, Args}}, #docs{hidden_status = hidden,
+                                                                                   hidden_types = HiddenTypes}=State)
+  when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+   State#docs{hidden_status = none,
+              hidden_types = sets:add_element({Name, length(Args)}, HiddenTypes)};
+extract_hidden_types0(_, State) ->
+   State#docs{hidden_status = none}.
+
+
+%%
+%% Adds type definitions / user-defined types to the state
+%%
+%% Necessary to provide warnings using the mapping
+%% #{{TypeName, length(Args)} => Anno}.
+%%
+extract_type_defs0({attribute, Anno, TypeOrOpaque, {TypeName, _TypeDef, TypeArgs}}, #docs{type_defs = TypeDefs}=State)
+  when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+   Args = fun_to_varargs(TypeArgs),
+   Type = {TypeName, length(Args)},
+   State#docs{type_defs = TypeDefs#{Type => Anno}};
+extract_type_defs0(_, State) ->
+   State.
+
+%%
+%% Creates a reachable type graph.
+%%
+%% Given a type `-type X(Args) :: Args2.`, `X` is a vertex that
+%% connects with vertices from Args and Args, creating a reachable
+%% type graph.
+%%
+extract_type_dependencies({attribute, _Anno, TypeOrOpaque, {TypeName, TypeDef, TypeArgs}},
+                          #docs{type_dependency = TypeDependency}=State)
+  when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+   Types = extract_user_types([TypeArgs, TypeDef], State),
+   Type = {TypeName, length(TypeArgs)},
+   digraph:add_vertex(TypeDependency, Type),
+   _ = [begin
+           digraph:add_vertex(TypeDependency, TypeAndArity),
+           digraph:add_edge(TypeDependency, Type, TypeAndArity)
+        end || TypeAndArity <- maps:keys(Types)],
+   State;
+extract_type_dependencies(_, State) ->
+   State.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% Helper functions
+%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+-spec create_module_doc(ModuleDoc :: binary() | atom()) -> map().
+create_module_doc(ModuleDoc) when is_atom(ModuleDoc) ->
+    ModuleDoc;
+create_module_doc(ModuleDoc) when not is_atom(ModuleDoc) ->
+    create_module_doc(<<"en">>, ModuleDoc).
+
+-spec create_module_doc(Lang :: binary(), ModuleDoc :: binary()) -> map().
+create_module_doc(Lang, ModuleDoc) ->
+    #{Lang => ModuleDoc}.
+
+-spec new_state(Dirname :: file:filename(), Filename :: file:filename(),
+                Opts :: [opt()]) -> internal_docs().
+new_state(Dirname, Filename, Opts) ->
+    DocsV1 = #docs_v1{},
+    reset_state(#docs{cwd = Dirname, filename = Filename,
+                      curr_filename = Filename, opts = Opts,
+                      moduledoc_meta = DocsV1#docs_v1.metadata}).
+
+
+-spec reset_state(State :: internal_docs()) -> internal_docs().
+reset_state(State) ->
+    State#docs{doc = none,
+               doc_status = none,
+               meta = #{exported => false}}.
+
+update_docstatus(State, V) ->
+    State#docs{doc_status = V}.
+
+
+update_ast(function, #docs{ast_fns=AST}=State, Fn) ->
+    State#docs{ast_fns = [Fn | AST]};
+update_ast(Type,#docs{ast_types=AST}=State, Fn) when Type =:= type; Type =:= opaque->
+    State#docs{ast_types = [Fn | AST]};
+update_ast(callback, #docs{ast_callbacks = AST}=State, Fn) ->
+    State#docs{ast_callbacks = [Fn | AST]}.
+
+-spec update_meta(State :: internal_docs(), Meta :: map()) -> internal_docs().
+update_meta(#docs{meta = Meta0}=State, Meta1) ->
+    State#docs{meta = maps:merge(Meta0, Meta1)}.
+
+-spec update_user_defined_types({type | callback | function, term(), integer()},
+                                State :: internal_docs()) -> internal_docs().
+update_user_defined_types({_Attr, _F, _A}=Key,
+                          #docs{user_defined_types = UserDefinedTypes,
+                                last_read_user_types = LastAddedTypes}=State) ->
+   Docs = State#docs.docs,
+   case maps:get(Key, Docs, none) of
+      {{hidden, _Anno}, _, _} ->
+         State#docs{last_read_user_types = #{}};
+      _ ->
+         State#docs{user_defined_types = sets:union(UserDefinedTypes, sets:from_list(maps:keys(LastAddedTypes))),
+                    last_read_user_types = #{}}
+   end.
+
+-spec update_doc(State :: internal_docs(), Doc) -> internal_docs() when
+     Doc :: {unicode:chardata(), erl_anno:anno()} | atom().
+update_doc(#docs{doc_status = DocStatus}=State, Doc0) ->
+    %% The exported := true only applies to types and should be ignored for functions.
+    %% This is because we need to export private types that are used on public
+    %% functions, or the documentation will create dead links.
+    State1 = update_docstatus(State, set_doc_status(DocStatus)),
+    State2 = update_meta(State1, #{exported => true}),
+    case Doc0 of
+        none ->
+            State2;
+        {Doc, Anno} ->
+            State2#docs{doc = {string:trim(Doc), set_file_anno(Anno, State2)}}
+    end.
+
+set_file_anno(Anno, State) ->
+        case {State#docs.curr_filename, erl_anno:file(Anno)} of
+            {ModuleName, undefined} when ModuleName =/= "",
+                                         ModuleName =/= State#docs.filename ->
+                erl_anno:set_file(ModuleName, Anno);
+            _ ->
+                Anno
+        end.
+
+%% Sets the doc status from `none` to `set`.
+%% Leave unchanged if the status was already set to something.
+%%
+set_doc_status(none) ->
+    set;
+set_doc_status(Other) ->
+    Other.
+
+-spec update_filename(State :: internal_docs(), ModuleName :: unicode:chardata()) -> internal_docs().
+update_filename(#docs{}=State, ModuleName) ->
+    State#docs{curr_filename = ModuleName}.
+
+-spec update_export_funs(State :: internal_docs(), proplists:proplist()) -> internal_docs().
+update_export_funs(State, ExportedFuns) ->
+    ExportedFuns1 = sets:union(State#docs.exported_functions, sets:from_list(ExportedFuns)),
+    State#docs{exported_functions = ExportedFuns1}.
+
+-spec update_export_types(State :: internal_docs(), proplists:proplist()) -> internal_docs().
+update_export_types(State, ExportedTypes) ->
+    ExportedTypes1 = sets:union(State#docs.exported_types, sets:from_list(ExportedTypes)),
+    State#docs{exported_types = ExportedTypes1}.
+
+update_export_all(State, ExportAll) ->
+    State#docs{ export_all = ExportAll }.
+
+remove_exported_type_info(Key, #docs{docs = Docs}=State) ->
+   {Status, Doc, Meta} = maps:get(Key, Docs),
+   Docs1 = maps:update(Key, {Status, Doc, maps:remove(exported, Meta)}, Docs),
+   State#docs{docs = Docs1}.
+
+extract_documentation(AST, State) ->
+   State1 = foldl(fun extract_documentation0/2, State, AST),
+   State2 = purge_types_not_used_from_exported_functions(State1),
+   State3 = purge_unreachable_types(State2),
+   warnings(AST, State3).
+
+%%
+%% purges types that are not used in exported functions.
+%% the type dependency field in docs does not keep track of which type
+%% is used in a public function, it simply connects all types.
+%% types of hidden functions may exist in the reachable type graph
+%% and they should be ignored unless reachable from
+purge_types_not_used_from_exported_functions(#docs{user_defined_types = UserDefinedTypes}=State) ->
+   AstTypes = filter(fun ({{_, F, A}, _Anno, _Slogan, _Doc, #{exported := Exported}}) ->
+                                  sets:is_element({F, A}, UserDefinedTypes) orelse Exported
+                            end, State#docs.ast_types),
+   State#docs{ast_types = AstTypes }.
+
+purge_unreachable_types(#docs{types_from_exported_funs = TypesFromExportedFuns,
+                              type_dependency = TypeDependency}=State) ->
+   SetTypesFromExportedFns = sets:from_list(maps:keys(TypesFromExportedFuns)),
+   SetTypes = sets:union(SetTypesFromExportedFns, State#docs.exported_types),
+   ReachableTypes = digraph_utils:reachable(sets:to_list(SetTypes), TypeDependency),
+   ReachableSet = sets:from_list(ReachableTypes),
+   AstTypes = filter(fun ({{_, F, A}, _Anno, _Slogan, _Doc, #{exported := Exported}}) ->
+                           sets:is_element({F, A}, ReachableSet) orelse Exported
+                     end, State#docs.ast_types),
+   State#docs{ast_types = AstTypes }.
+
+warnings(_AST, State) ->
+   WarnFuns = [fun warn_hidden_types_used_in_public_fns/1,
+               fun warn_missing_docs/1,
+               fun warn_missing_moduledoc/1,
+               fun warn_hidden_callback/1
+              ],
+   foldl(fun (W, State0) -> W(State0) end, State, WarnFuns).
+
+warn_missing_docs(State) ->
+   DocNodes = process_docs(State),
+   foldl(fun warn_missing_docs/2, State, DocNodes).
+
+warn_hidden_callback(State) ->
+    L = maps:to_list(State#docs.docs),
+    NoWarn = flatten(proplists:get_all_values(nowarn_hidden_doc, State#docs.opts)),
+    case member(true, NoWarn) of
+        false ->
+            foldl(fun ({{callback, Name, Arity},{{hidden, Anno}, _, _}}, State0) ->
+                          case member({Name, Arity}, NoWarn) of
+                              false ->
+                                  Warning = {hidden_callback, {Name, Arity}},
+                                  W = create_warning(Anno, Warning, State0),
+                                  State0#docs{ warnings = [W | State0#docs.warnings] };
+                              true ->
+                                  State0
+                          end;
+                      (_, State0) ->
+                          State0
+                  end, State, L);
+        true ->
+            State
+    end.
+
+%% hidden types with `-doc hidden.` or `-doc false.`, which are public (inside
+%% `export_type([])`), and used in public functions, they do not make sense. It
+%% is a type that is not documented (due to hidden property), visible in the
+%% docs (because it is in export_type), and reference / used by a public
+%% function cannot be used.
+%% A type that is hidden, private, and used in an exported function will be documented
+%% by the doc generation showing the internal type structure.
+warn_hidden_types_used_in_public_fns(#docs{types_from_exported_funs = TypesFromExportedFuns,
+                                           type_dependency = TypeDependency,
+                                           type_defs = TypeDefs}=State) ->
+    NoWarn = flatten(proplists:get_all_values(nowarn_hidden_doc, State#docs.opts)),
+    case member(true, NoWarn) of
+        false ->
+            HiddenTypes = State#docs.hidden_types,
+            Types = maps:keys(TypesFromExportedFuns),
+            ReachableTypes = digraph_utils:reachable(Types, TypeDependency),
+            ReachableSet = sets:from_list(ReachableTypes),
+            Warnings = sets:intersection(HiddenTypes, ReachableSet),
+            FilteredWarnings = sets:filter(fun(Key) -> not member(Key, NoWarn) end, Warnings),
+            WarningsWithAnno = sets:map(fun (Key) ->
+                                                Anno = maps:get(Key, TypeDefs),
+                                                Warn = {hidden_type_used_in_exported_fun, Key},
+                                                create_warning(Anno, Warn, State)
+                                        end, FilteredWarnings),
+            State#docs{warnings = State#docs.warnings ++ sets:to_list(WarningsWithAnno) };
+        true ->
+            State
+    end.
+
+create_warning(Anno, Warning, State) ->
+   Filename = erl_anno_file(Anno, State),
+   Location = erl_anno:location(Anno),
+   {Filename, [{Location, ?MODULE, Warning}]}.
+
+warn_missing_docs({KFA, Anno, _, Doc, _}, State) ->
+    case proplists:get_value(warn_missing_doc, State#docs.opts, false) of
+        true when Doc =:= none ->
+            Warning = {missing_doc, KFA},
+            State#docs{ warnings = [create_warning(Anno, Warning, State) | State#docs.warnings] };
+        _false ->
+            State
+    end.
+
+warn_missing_moduledoc(State) ->
+   {_, ModuleDoc} = State#docs.moduledoc,
+   case proplists:get_value(warn_missing_doc, State#docs.opts, false) of
+      true when ModuleDoc =:= none ->
+         Anno = erl_anno:new(?DEFAULT_MODULE_DOC_LOC),
+         Warning = missing_moduledoc,
+         State#docs{ warnings = [create_warning(Anno, Warning, State) | State#docs.warnings] };
+      _false ->
+         State
+   end.
+
+%%
+%% Extracts documentation
+%%
+%% This algorithm may use temporal state to keep track of documentation.
+%% Example: By looking at `-doc ...` one cannot know whether the doc
+%% is attached to a function, type, callback.
+extract_documentation0({attribute, _Anno, file, {Filename, _A}}, State) ->
+    update_filename(State, Filename);
+extract_documentation0({attribute, _Anno, spec, _}=AST, State) ->
+   extract_documentation_spec(AST, State);
+extract_documentation0({function, _Anno, F, A, _Body}=AST, State) ->
+    State1 = remove_exported_type_info({function, F, A}, State),
+    extract_documentation_from_funs(AST, State1);
+extract_documentation0({attribute, _Anno, TypeOrOpaque, _}=AST,State)
+  when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+    extract_documentation_from_type(AST, State);
+extract_documentation0({attribute, _Anno, callback, {{CB, A}, _Form}}=AST, State) ->
+    State1 = remove_exported_type_info({callback, CB, A}, State),
+    extract_documentation_from_cb(AST, State1);
+extract_documentation0(_, State) ->
+    State.
+
+
+extract_documentation_spec({attribute, Anno, spec, {{Name,Arity}, SpecTypes}}, #docs{exported_functions = ExpFuns}=State) ->
+%% this is because public functions may use private types and these private
+%% types need to be included in the beam and documentation.
+   case sets:is_element({Name, Arity}, ExpFuns) orelse State#docs.export_all of
+      true ->
+         add_user_types(Anno, SpecTypes, State);
+      false ->
+         State
+   end;
+extract_documentation_spec({attribute, Anno, spec, {{_Mod, Name,Arity}, SpecTypes}}, State) ->
+   extract_documentation_spec({attribute, Anno, spec, {{Name,Arity}, SpecTypes}}, State).
+
+add_user_types(_Anno, SpecTypes, State) ->
+   Types = extract_user_types(SpecTypes, State),
+   State1 = set_types_used_in_public_funs(State, Types),
+   set_last_read_user_types(State1, Types).
+
+%% pre: only call this function to add types from external functions.
+set_types_used_in_public_funs(#docs{types_from_exported_funs = TypesFromExportedFuns}=State, Types) ->
+   Combiner = fun (_Key, Value1, Value2) -> Value1 ++ Value2 end,
+   Types0 = maps:merge_with(Combiner, TypesFromExportedFuns, Types),
+   State#docs{types_from_exported_funs = Types0}.
+
+set_last_read_user_types(#docs{}=State, Types) ->
+   State#docs{last_read_user_types = Types}.
+
+extract_user_types(Args, #docs{ records = Records }) ->
+    {Types, _Records} = extract_user_types(Args, {maps:new(), Records}),
+    Types;
+extract_user_types(Types, Acc) when is_list(Types) ->
+    foldl(fun extract_user_types/2, Acc, Types);
+extract_user_types({ann_type, _, [_Name, Type]}, Acc) ->
+    extract_user_types(Type, Acc);
+extract_user_types({type, _, 'fun', Args}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type, _, map, Args}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type, _,record,[{atom, _, Name} | Args]}, {Acc, Records}) ->
+    NewArgs = uniq(fun({type, _, field_type, [{atom, _, FieldName} | _]}) ->
+                           FieldName
+                   end, Args ++ maps:get(Name, Records, [])),
+    extract_user_types(NewArgs, {Acc, maps:remove(Name, Records)});
+extract_user_types({remote_type,_,[_ModuleName,_TypeName,Args]}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type, _, tuple, Args}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type, _,union, Args}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({user_type, Anno, Name, Args}, {Acc, Records}) ->
+    %% append user type and continue iterating through lists in case of other
+    %% user-defined types to be added
+    Fun = fun (Value) -> [Anno | Value] end,
+    Acc1 = maps:update_with({Name, length(Args)}, Fun, [Anno], Acc),
+    extract_user_types(Args, {Acc1, Records});
+extract_user_types({type, _, bounded_fun, Args}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type,_,product,Args}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type,_,constraint,[{atom,_,is_subtype},Args]}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type, _, map_field_assoc, Args}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type, _, map_field_exact, Args}, Acc) ->
+    extract_user_types(Args, Acc);
+extract_user_types({type,_,field_type,[_Name, Type]}, Acc) ->
+    extract_user_types(Type, Acc);
+extract_user_types({type, _,_BuiltIn, Args}, Acc) when is_list(Args)->
+    %% Handles built-in types such as 'list', 'nil' 'range'.
+    extract_user_types(Args, Acc);
+extract_user_types(_Else, Acc) ->
+    Acc.
+
+extract_documentation_from_type({attribute, Anno, TypeOrOpaque, {TypeName, _TypeDef, TypeArgs}=Types},
+                      #docs{docs = Docs, exported_types=ExpTypes}=State)
+  when TypeOrOpaque =:= type; TypeOrOpaque =:= opaque ->
+   Args = fun_to_varargs(TypeArgs),
+   Key =  {type, TypeName, length(TypeArgs)},
+
+   % we assume it exists because a first pass must have added it
+   {Status, Doc, Meta} = maps:get(Key, Docs),
+
+   State0 = add_last_read_user_type(Anno, Types, State),
+   Type = {TypeName, length(Args)},
+
+   Docs1 = maps:update(Key, {Status, Doc, Meta#{exported := sets:is_element(Type, ExpTypes)}}, Docs),
+   State2 = State0#docs {docs = Docs1},
+   State3 = gen_doc_with_slogan({type, Anno, TypeName, length(Args), Args}, State2),
+   add_type_defs(Type, State3).
+
+add_type_defs(Type, #docs{type_defs = TypeDefs, ast_types = [{_KFA, Anno, _Slogan, _Doc, _Meta} | _]}=State) ->
+   State#docs{type_defs = TypeDefs#{Type => Anno}}.
+
+
+add_last_read_user_type(_Anno, {_TypeName, TypeDef, TypeArgs}, State) ->
+   Types = extract_user_types([TypeArgs, TypeDef], State),
+   set_last_read_user_types(State, Types).
+
+%% NOTE: Terminal elements for the documentation, such as `-type`, `-opaque`, `-callback`,
+%%       and functions always need to reset the state when they finish, so that new
+%%       new AST items start with a clean slate.
+extract_documentation_from_funs({function, Anno, F, A, [{clause, _, ClauseArgs, _, _}]},
+                      #docs{exported_functions = ExpFuns}=State) ->
+    case (sets:is_element({F, A}, ExpFuns) orelse State#docs.export_all) of
+       true ->
+          gen_doc_with_slogan({function, Anno, F, A, ClauseArgs}, State);
+       false ->
+          reset_state(State)
+    end;
+extract_documentation_from_funs({function, _Anno0, F, A, _Body}=AST,
+                                #docs{exported_functions=ExpFuns}=State) ->
+   {Doc1, Anno1} = fetch_doc_and_anno(State, AST),
+   case sets:is_element({F, A}, ExpFuns) orelse State#docs.export_all of
+      true ->
+         {Slogan, DocsWithoutSlogan} = extract_slogan(Doc1, State, F, A),
+         AttrBody = {function, F, A},
+         gen_doc(Anno1, AttrBody, Slogan, DocsWithoutSlogan, State);
+      false ->
+         reset_state(State)
+   end.
+
+extract_documentation_from_cb({attribute, Anno, callback, {{CB, A}, Form}}, State) ->
+   %% adds user types as part of possible types that need to be exported
+   State2 = add_user_types(Anno, Form, State),
+   Args = case Form of
+              [Fun] ->
+                  fun_to_varargs(Fun);
+              _ -> %% multi-clause
+                  Form
+          end,
+   gen_doc_with_slogan({callback, Anno, CB, A, Args}, State2).
+
+%% Generates documentation
+-spec gen_doc(Anno, AttrBody, Slogan, Docs, State) -> Response when
+      Anno      :: erl_anno:anno(),
+      AttrBody  :: {function | type | callback, term(), integer()},
+      Slogan    :: unicode:chardata(),
+      Docs      :: none | hidden | unicode:chardata() | #{ <<_:16>> => unicode:chardata() },
+      State     :: internal_docs(),
+      Response  :: internal_docs().
+gen_doc(Anno, AttrBody, Slogan, Docs, State) when not is_atom(Docs), not is_map(Docs) ->
+    gen_doc(Anno, AttrBody, Slogan, #{ <<"en">> => unicode:characters_to_binary(string:trim(Docs)) }, State);
+gen_doc(Anno, {Attr, _F, _A}=AttrBody, Slogan, Docs, #docs{docs=DocsMap}=State) ->
+   {_Status, _Doc, Meta} = maps:get(AttrBody, DocsMap),
+   Result = {AttrBody, Anno, [unicode:characters_to_binary(Slogan)], Docs,
+             maybe_add_deprecation(AttrBody, Meta, State)},
+   State1 = update_user_defined_types(AttrBody, State),
+   reset_state(update_ast(Attr, State1, Result)).
+
+erl_anno_file(Anno, State) ->
+    case erl_anno:file(Anno) of
+        undefined ->
+            State#docs.filename;
+        FN -> FN
+    end.
+
+maybe_add_deprecation(_KNA, #{ deprecated := Deprecated } = Meta, _State) ->
+    Meta#{ deprecated := unicode:characters_to_binary(Deprecated) };
+maybe_add_deprecation({Kind, Name, Arity}, Meta, #docs{ module = Module,
+                                                        deprecated = Deprecations }) ->
+    maybe
+        error ?= maps:find({Kind, Name, Arity}, Deprecations),
+        error ?= maps:find({Kind, Name, '_'}, Deprecations),
+        error ?= maps:find({Kind, '_', Arity}, Deprecations),
+        error ?= maps:find({Kind, '_', '_'}, Deprecations),
+        Meta
+    else
+        {ok, Value} ->
+            Text =
+                if Kind =:= function ->
+                        erl_lint:format_error({deprecated, {Module,Name,Arity},
+                                               info_string(Value)});
+                   Kind =:= type ->
+                        erl_lint:format_error({deprecated_type, {Module,Name,Arity},
+                                               info_string(Value)})
+                end,
+            Meta#{ deprecated => unicode:characters_to_binary(Text) }
+    end.
+
+%% Copies from lib/stdlib/scripts/update_deprecations
+info_string(undefined) ->
+    "see the documentation for details";
+info_string(next_version) ->
+    "will be removed in the next version. "
+        "See the documentation for details";
+info_string(next_major_release) ->
+    "will be removed in the next major release. "
+        "See the documentation for details";
+info_string(eventually) ->
+    "will be removed in a future release. "
+        "See the documentation for details";
+info_string(String) when is_list(String) ->
+    String.
+
+%% Generates the documentation inferring the slogan from the documentation.
+gen_doc_with_slogan({Attr, _Anno0, F, A, Args}=AST, State) ->
+    {Doc1, Anno1} = fetch_doc_and_anno(State, AST),
+    {Slogan, DocsWithoutSlogan} = extract_slogan(Doc1, State, F, A, Args),
+    AttrBody = {Attr, F, A},
+    gen_doc(Anno1, AttrBody, Slogan, DocsWithoutSlogan, State).
+
+fetch_doc_and_anno(#docs{docs = DocsMap}=State, {Attr, Anno0, F, A, _Args}) ->
+    %% a first pass guarantees that DocsMap cannot be empty
+    {DocStatus, Doc, _Meta} = maps:get({Attr, F, A}, DocsMap),
+    case {DocStatus, Doc} of
+        {{hidden, Anno}, _} -> {hidden, Anno};
+        {_, none} -> {none, set_file_anno(Anno0, State)};
+        {_, {Doc1, Anno}} -> {Doc1, Anno}
+    end.
+
+-spec fun_to_varargs(tuple() | term()) -> list(term()).
+fun_to_varargs({type, _, bounded_fun, [T|_]}) ->
+   fun_to_varargs(T);
+fun_to_varargs({type, _, 'fun', [{type,_,product,Args}|_] }) ->
+   map(fun fun_to_varargs/1, Args);
+fun_to_varargs({ann_type, _, [Name|_]}) ->
+   Name;
+fun_to_varargs({var,_,_} = Name) ->
+   Name;
+fun_to_varargs(Else) ->
+   Else.
+
+extract_slogan(Doc, State, F, A) ->
+   extract_slogan(Doc, State, F, A, [invalid]).
+extract_slogan(Doc, State, F, A, Args) ->
+   %% order of the strategy matters
+   StrategyOrder = [fun slogan_strategy_doc_attr/5,
+                    fun slogan_strategy_spec/5,
+                    fun slogan_strategy_args/5,
+                    fun slogan_strategy_default/5],
+
+   %% selection alg. tries strategy until one strategy
+   %% returns value =/= false
+   SloganSelection = fun (Fun, false) -> Fun(Doc, State, F, A, Args);
+                         (_F, Acc) -> Acc
+                     end,
+   foldl(SloganSelection, false, StrategyOrder).
+
+
+slogan_strategy_doc_attr(Doc, _State, F, A, _Args) ->
+   maybe
+      false ?= Doc =:= none orelse Doc =:= hidden,
+      [MaybeSlogan | Rest] = string:split(Doc, "\n"),
+      {ok, Toks, _} ?= erl_scan:string(unicode:characters_to_list([MaybeSlogan,"."])),
+      {ok, [{call,_,{atom,_,F},SloganArgs}]} ?= erl_parse:parse_exprs(Toks),
+      A ?= length(SloganArgs),
+      {MaybeSlogan, Rest}
+   else
+      _ ->
+         false
+   end.
+
+slogan_strategy_spec(Doc, State, F, A, _Args) ->
+   case maps:get({F, A}, State#docs.slogans, none) of
+      {F, Vars, A} ->
+         VarString = join(", ",[atom_to_list(Var) || Var <- Vars]),
+         Slogan = unicode:characters_to_list(io_lib:format("~p(~s)", [F, VarString])),
+         {Slogan, Doc};
+      none ->
+         false
+   end.
+
+slogan_strategy_args(Doc, _State, F, _A, Args) ->
+   case all(fun is_var_without_underscore/1, Args)  of
+      true ->
+         {extract_slogan_from_args(F, Args), Doc};
+      false ->
+         false
+   end.
+
+slogan_strategy_default(Doc, _State, F, A, _Args) ->
+   {io_lib:format("~p/~p",[F,A]), Doc}.
+
+
+is_var_without_underscore({var, _, N}) ->
+   N =/= '_';
+is_var_without_underscore(_) ->
+   false.
+
+extract_slogan_from_args(F, Args) ->
+   io_lib:format("~p(~ts)",[F, join(", ",[string:trim(atom_to_list(Arg),leading,"_") || {var, _, Arg} <- Args])]).
diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl
index d4e693aee0..43d55bb5d9 100644
--- a/lib/compiler/src/compile.erl
+++ b/lib/compiler/src/compile.erl
@@ -340,19 +340,19 @@ format_error_reason(Class, Reason, Stack) ->
     erl_error:format_exception(Class, Reason, Stack, Opts).
 
 %% The compile state record.
--record(compile, {filename="" :: file:filename(),
-		  dir=""      :: file:filename(),
-		  base=""     :: file:filename(),
-		  ifile=""    :: file:filename(),
-		  ofile=""    :: file:filename(),
-		  module=[]   :: module() | [],
-		  abstract_code=[] :: abstract_code(), %Abstract code for debugger.
-		  options=[]  :: [option()],  %Options for compilation
-		  mod_options=[]  :: [option()], %Options for module_info
-                  encoding=none :: none | epp:source_encoding(),
-		  errors=[]     :: errors(),
-		  warnings=[]   :: warnings(),
-		  extra_chunks=[] :: [{binary(), binary()}]}).
+-record(compile, {filename=""      :: file:filename(),
+                  dir=""           :: file:filename(),
+                  base=""          :: file:filename(),
+                  ifile=""         :: file:filename(),
+                  ofile=""         :: file:filename(),
+                  module=[]        :: module() | [],
+                  abstract_code=[] :: abstract_code(), %Abstract code for debugger.
+                  options=[]       :: [option()],  %Options for compilation
+                  mod_options=[]   :: [option()], %Options for module_info
+                  encoding=none    :: none | epp:source_encoding(),
+                  errors=[]        :: errors(),
+                  warnings=[]      :: warnings(),
+                  extra_chunks=[]  :: [{binary(), binary()}]}).
 
 internal({forms,Forms}, Opts0) ->
     {_,Ps} = passes(forms, Opts0),
@@ -812,6 +812,8 @@ standard_passes() ->
 
      {iff,'dpp',{listing,"pp"}},
      ?pass(lint_module),
+     {unless,no_docs,?pass(beam_docs)},
+     ?pass(remove_doc_attributes),
 
      {iff,'P',{src_listing,"P"}},
      {iff,'to_pp',{done,"P"}},
@@ -821,7 +823,9 @@ standard_passes() ->
 
 abstr_passes(AbstrStatus) ->
     case AbstrStatus of
-        non_verified_abstr -> [{unless, no_lint, ?pass(lint_module)}];
+        non_verified_abstr -> [{unless, no_lint, ?pass(lint_module)},
+                               {unless,no_docs,?pass(beam_docs)},
+                               ?pass(remove_doc_attributes)];
         verified_abstr -> []
     end ++
         [
@@ -1027,17 +1031,20 @@ parse_module(_Code, St) ->
 	    Ret
     end.
 
-do_parse_module(DefEncoding, #compile{ifile=File,options=Opts,dir=Dir}=St) ->
+deterministic_filename(#compile{ifile=File,options=Opts}) ->
     SourceName0 = proplists:get_value(source, Opts, File),
-    SourceName = case member(deterministic, Opts) of
-                     true ->
-                         filename:basename(SourceName0);
-                     false ->
-                         case member(absolute_source, Opts) of
-                             true -> paranoid_absname(SourceName0);
-                             false -> SourceName0
-                         end
-                 end,
+    case member(deterministic, Opts) of
+        true ->
+            filename:basename(SourceName0);
+        false ->
+            case member(absolute_source, Opts) of
+                true -> paranoid_absname(SourceName0);
+                false -> SourceName0
+            end
+    end.
+
+do_parse_module(DefEncoding, #compile{ifile=File,options=Opts,dir=Dir}=St) ->
+    SourceName = deterministic_filename(St),
     StartLocation = case with_columns(Opts) of
                         true ->
                             {1,1};
@@ -1589,6 +1596,31 @@ core_inline_module(Code0, #compile{options=Opts}=St) ->
 save_abstract_code(Code, St) ->
     {ok,Code,St#compile{abstract_code=erl_parse:anno_to_term(Code)}}.
 
+-define(META_DOC_CHUNK, <<"Docs">>).
+
+
+%% Adds documentation attributes to extra_chunks (beam file)
+beam_docs(Code, #compile{dir = Dir, options = Options,
+                         extra_chunks = ExtraChunks }=St) ->
+    SourceName = deterministic_filename(St),
+    {ok, Docs, Ws} = beam_doc:main(Dir, SourceName, Code, Options),
+    MetaDocs = [{?META_DOC_CHUNK, term_to_binary(Docs)} | ExtraChunks],
+    {ok, Code, St#compile{extra_chunks = MetaDocs,
+                          warnings = St#compile.warnings ++ Ws}}.
+
+%% Strips documentation attributes from the code
+remove_doc_attributes(Code, St) ->
+    {ok, [Attr || Attr <- Code, not is_doc_attribute(Attr)], St}.
+
+
+is_doc_attribute(Attr) ->
+    case Attr of
+        {attribute, _Anno, DocAttr, _Meta}
+          when DocAttr =:= doc; DocAttr =:= moduledoc; DocAttr =:= docformat ->
+            true;
+        _ -> false
+    end.
+
 debug_info(#compile{module=Module,ofile=OFile}=St) ->
     {DebugInfo,Opts2} = debug_info_chunk(St),
     case member(encrypt_debug_info, Opts2) of
@@ -2123,6 +2155,7 @@ pre_load() ->
 	 beam_clean,
 	 beam_dict,
 	 beam_digraph,
+     beam_doc,
 	 beam_flatten,
 	 beam_jump,
 	 beam_kernel_to_ssa,
diff --git a/lib/compiler/src/compiler.app.src b/lib/compiler/src/compiler.app.src
index 71588c0826..3190b43468 100644
--- a/lib/compiler/src/compiler.app.src
+++ b/lib/compiler/src/compiler.app.src
@@ -31,6 +31,7 @@
 	     beam_dict,
 	     beam_digraph,
 	     beam_disasm,
+         beam_doc,
 	     beam_flatten,
 	     beam_jump,
              beam_kernel_to_ssa,
diff --git a/lib/compiler/test/Makefile b/lib/compiler/test/Makefile
index 43c7ccec7a..925f6ba969 100644
--- a/lib/compiler/test/Makefile
+++ b/lib/compiler/test/Makefile
@@ -12,6 +12,7 @@ MODULES= \
 	beam_bounds_SUITE \
 	beam_validator_SUITE \
 	beam_disasm_SUITE \
+	beam_doc_SUITE \
 	beam_except_SUITE \
 	beam_jump_SUITE \
 	beam_reorder_SUITE \
diff --git a/lib/compiler/test/beam_doc_SUITE.erl b/lib/compiler/test/beam_doc_SUITE.erl
new file mode 100644
index 0000000000..217698c13b
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE.erl
@@ -0,0 +1,603 @@
+
+-module(beam_doc_SUITE).
+-export([all/0, groups/0, init_per_group/2, end_per_group/2, singleton_moduledoc/1, singleton_doc/1,
+         docmodule_with_doc_attributes/1, hide_moduledoc/1, docformat/1,
+         singleton_docformat/1, singleton_meta/1, slogan/1,
+         types_and_opaques/1, callback/1, hide_moduledoc2/1,
+         private_types/1, export_all/1, equiv/1, spec/1, deprecated/1, warn_missing_doc/1,
+         doc_with_file/1, doc_with_file_error/1, all_string_formats/1,
+         docs_from_ast/1, spec_switch_order/1, user_defined_type/1, skip_doc/1]).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("kernel/include/eep48.hrl").
+-include_lib("stdlib/include/assert.hrl").
+
+-define(get_name(), atom_to_list(?FUNCTION_NAME)).
+
+all() ->
+    [{group, documentation_generation_tests}, doc_with_file].
+
+groups() ->
+    [{documentation_generation_tests, [parallel], documentation_generation_tests()}].
+
+init_per_group(_, Config) ->
+    Config.
+
+end_per_group(_, _Config) ->
+    ok.
+
+documentation_generation_tests() ->
+    [singleton_moduledoc,
+     singleton_doc,
+     docmodule_with_doc_attributes,
+     hide_moduledoc,
+     hide_moduledoc2,
+     docformat,
+     singleton_docformat,
+     singleton_meta,
+     slogan,
+     types_and_opaques,
+     callback,
+     private_types,
+     export_all,
+     equiv,
+     spec,
+     deprecated,
+     warn_missing_doc,
+     doc_with_file_error,
+     all_string_formats,
+     spec_switch_order,
+     docs_from_ast,
+     user_defined_type,
+     skip_doc
+    ].
+
+singleton_moduledoc(Conf) ->
+    ModuleName = "singletonmoduledoc",
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+
+    Mime = <<"text/markdown">>,
+    ModuleDoc = #{<<"en">> => <<"Moduledoc test module">>},
+    {ok, {docs_v1, _,_, Mime,ModuleDoc, _,_}} = code:get_doc(ModName),
+    ok.
+
+singleton_doc(Conf) ->
+    ModuleName = "singletondoc",
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    Mime = <<"text/markdown">>,
+    Doc = #{<<"en">> => <<"Doc test module">>},
+    FooDoc = #{<<"en">> => <<"Tests multi-clauses">>},
+    {ok, {docs_v1, 1,_, Mime, none, _,
+          [{{function, foo,1},_, [<<"foo(ok)">>], FooDoc, _},
+           {{function, main,0},_, [<<"main()">>], Doc, _}]}} = code:get_doc(ModName),
+    ok.
+
+docmodule_with_doc_attributes(Conf) ->
+    ModuleName = "docmodule_with_doc_attributes",
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    Mime = <<"text/markdown">>,
+    ModuleDoc = #{<<"en">> => <<"Moduledoc test module">>},
+    Doc = #{<<"en">> => <<"Doc test module">>},
+    FileDocs =  #{<<"en">> => <<"# README\n\nThis is a test">>},
+    {ok, #docs_v1{ anno = ModuleAnno,
+                   beam_language = erlang,
+                   format = Mime,
+                   module_doc = ModuleDoc,
+                   metadata = #{},
+                   docs = Docs
+                 }} = code:get_doc(ModName),
+
+    
+    [{{function,no_docs_multi,1},NoDocsMultiAnno,[<<"no_docs_multi/1">>],none,#{}},
+     {{function,with_file_docs,0},FileDocsAnno, [<<"with_file_docs()">>],FileDocs,#{}},
+     {{function,no_docs,0},NoDocsAnno, [<<"no_docs()">>],none,#{}},
+     {{function,ok,0}, OkAnno, [<<"ok()">>],none,#{authors := "Someone"}},
+     {{function, main,_},MainAnno, _, Doc, _}] = Docs,
+    
+    ?assertEqual(5, erl_anno:line(ModuleAnno)),
+    ?assertEqual(10, erl_anno:line(MainAnno)),
+    ?assertEqual(18, erl_anno:line(OkAnno)),
+    ?assertEqual(21, erl_anno:line(NoDocsAnno)),
+    ?assertEqual(1, erl_anno:line(FileDocsAnno)),
+    ?assertEqual("README", filename:basename(erl_anno:file(FileDocsAnno))),
+    ?assertEqual(28, erl_anno:line(NoDocsMultiAnno)),
+
+    ok.
+
+hide_moduledoc(Conf) ->
+    {ok, ModName} = default_compile_file(Conf, "hide_moduledoc"),
+    {ok, {docs_v1, _,_, _Mime, hidden, _,
+          [{{function, main, 0}, _, [<<"main()">>],
+            #{ <<"en">> := <<"Doc test module">> }, #{}}]}} = code:get_doc(ModName),
+    ok.
+
+%% TODO: crashes
+hide_moduledoc2(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    {ok, {docs_v1, _,_, _Mime, hidden, _,
+          [{{function,handle_call,1},{19,2},[<<"handle_call/1">>],hidden,#{}},
+           {{function, main, 0}, _, [<<"main()">>], hidden, #{}}]}} = code:get_doc(ModName),
+    ok.
+
+docformat(Conf) ->
+    {ok, ModName} = default_compile_file(Conf, "docformat"),
+    ModuleDoc = #{<<"en">> => <<"Moduledoc test module">>},
+    Meta = #{format => "text/asciidoc",
+             deprecated => "Use something else",
+             otp_doc_vsn => {1,0,0},
+             since => "1.0"},
+    Doc = #{<<"en">> => <<"Doc test module">>},
+    {ok, {docs_v1, _,_, <<"text/asciidoc">>, ModuleDoc, Meta,
+          [{{function, main,_},_, _, Doc, _}]}} = code:get_doc(ModName),
+    ok.
+
+singleton_docformat(Conf) ->
+    {ok, ModName} = default_compile_file(Conf, "singleton_docformat"),
+    ModuleDoc = #{<<"en">> => <<"Moduledoc test module">>},
+    Meta = #{format => <<"text/asciidoc">>,
+             deprecated => "Use something else",
+             otp_doc_vsn => {1,0,0},
+             since => "1.0"},
+    Doc = #{<<"en">> => <<"Doc test module\n\nMore info here">>},
+    FunMeta = #{ authors => [<<"Beep Bop">>], equiv => <<"main/3">> },
+    {ok, {docs_v1, _,erlang, <<"text/asciidoc">>, ModuleDoc, Meta,
+          [{{function, main,0},_, [<<"main()">>], Doc, FunMeta}]}} = code:get_doc(ModName),
+    ok.
+
+singleton_meta(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    Meta = #{ authors => [<<"Beep Bop">>], equiv => <<"main/3">>},
+    DocMain1 = #{<<"en">> => <<"Returns always ok.">>},
+    {ok, {docs_v1, _,erlang, <<"text/markdown">>, none, _,
+          [{{function, main1,0},_, [<<"main1()">>], DocMain1, #{equiv := <<"main(_)">>}},
+           {{function, main,0},_, [<<"main()">>], none, Meta}]}}
+        = code:get_doc(ModName),
+    ok.
+
+slogan(Conf) ->
+  ModuleName = ?get_name(),
+  {ok, ModName} = default_compile_file(Conf, ModuleName),
+  Doc = #{<<"en">> => <<"Returns ok.">>},
+  BarDoc = #{ <<"en">> => <<"foo()\nNot a slogan since foo =/= bar">> },
+  NoSloganDoc = #{ <<"en">> => <<"Not a slogan\n\nTests slogans in multi-clause">>},
+  {ok, {docs_v1, _,_, _, none, _,
+          [Connect, MulticlauseSloganIgnored, SpecNoDocSlogan, NoDocSlogan,
+           Slogan2, Slogan1, NoSlogan, Bar, Main]}} = code:get_doc(ModName),
+
+  {{function,connect,2},_,
+   [<<"connect(TCPSocket, TLSOptions)">>],none,#{equiv := <<"connect/3">>,since := <<"OTP R14B">>}} = Connect,
+  {{function,spec_multiclause_slogan_ignored,1},_,[<<"spec_multiclause_slogan_ignored(X)">>],none,#{}} = MulticlauseSloganIgnored,
+  {{function, spec_no_doc_slogan, 1}, _, [<<"spec_no_doc_slogan(Y)">>], none, #{}} = SpecNoDocSlogan,
+  {{function, no_doc_slogan, 1}, _, [<<"no_doc_slogan(X)">>], none, #{}}= NoDocSlogan,
+  {{function, spec_slogan, 2}, _, [<<"spec_slogan(Y, Z)">>], _, #{}} = Slogan2,
+  {{function, spec_slogan, 1}, _, [<<"spec_slogan(Y)">>], _, #{}} = Slogan1,
+  {{function, no_slogan,1},_,[<<"no_slogan/1">>], NoSloganDoc, #{}} = NoSlogan,
+  {{function, bar,0},_,[<<"bar()">>], BarDoc, #{}} = Bar,
+  {{function, main,1},_,[<<"main(Foo)">>], Doc, #{}} = Main,
+  ok.
+
+types_and_opaques(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName, Warnings} = default_compile_file(Conf, ModuleName, [return_warnings]),
+    TypeDoc = #{<<"en">> => <<"Represents the name of a person.">>},
+    GenericsDoc = #{<<"en">> => <<"Tests generics">>},
+    OpaqueDoc = #{<<"en">> =>
+                      <<"Represents the name of a person that cannot be named.">>},
+    MaybeOpaqueDoc = #{<<"en">> => <<"mmaybe(X) ::= nothing | X.\n\nRepresents a maybe type.">>},
+    MaybeMeta = #{ authors => "Someone else", exported => true },
+    NaturalNumberMeta = #{since => "1.0", equiv => <<"non_neg_integer/0">>, exported => true},
+
+    {ok, {docs_v1, _,_, _, none, _,
+          [%% Type Definitions
+           Public, Intermediate, HiddenNoWarnType, HiddenType, OtherPrivateType, MyPrivateType,
+           MyMap, StateEnter, CallbackMode,CallbackResult, EncodingFunc, Three,
+           Two, One, Hidden, HiddenFalse, MMaybe, Unnamed, Param,NatNumber, Name,
+           HiddenIncludedType,
+           %% Functions
+           UsesPublic, Ignore, MapFun, PrivateEncoding, Foo
+          ]}} = code:get_doc(ModName),
+
+    {{type,public,0},{128,2},[<<"public()">>],none,#{exported := true}} = Public,
+    {{type,intermediate,0},{127,2},[<<"intermediate()">>],none,#{exported := false}} = Intermediate,
+    {{type,hidden_nowarn_type,0},{123,2},[<<"hidden_nowarn_type()">>],hidden,#{exported := false}} = HiddenNoWarnType,
+    {{type,hidden_type,0},{120,2},[<<"hidden_type()">>],hidden,#{exported := false}} = HiddenType,
+    {{type,my_other_private_type,0},MyOtherPrivateTypeLine,
+              [<<"my_other_private_type()">>],none,#{exported := false}} = OtherPrivateType,
+    {{type,my_private_type,0},MyPrivateTypeLine,
+     [<<"my_private_type()">>],none,#{exported := false}} = MyPrivateType,
+    {{type,mymap,0},MyMapLine,[<<"mymap()">>],none,#{exported := false}} = MyMap,
+    {{type,state_enter,0},StateEnterLine,[<<"state_enter()">>],none,#{exported := false}}=StateEnter,
+    {{type,callback_mode,0},CallbackModeLine, [<<"callback_mode()">>],none,#{exported := false}} = CallbackMode,
+    {{type,callback_mode_result,0},CallbackResultLine,
+               [<<"callback_mode_result()">>],none,#{exported := true}} = CallbackResult,
+    {{type,encoding_func,0},_,[<<"encoding_func()">>],none,#{exported := false}} = EncodingFunc,
+    {{type,three,0},_,[<<"three()">>],none,#{exported := false}} = Three,
+    {{type,two,0},_,[<<"two()">>],none,#{exported := false}} = Two,
+    {{type,one,0},_,[<<"one()">>],none,#{exported := false}} = One,
+    {{type,hidden,0},_,[<<"hidden()">>],hidden,#{exported := true}} = Hidden,
+    {{type,hidden_false,0},_,[<<"hidden_false()">>],hidden,
+     #{exported := true, authors := "Someone else"}} = HiddenFalse,
+    {{type, mmaybe,1},_,[<<"mmaybe(X)">>], MaybeOpaqueDoc, MaybeMeta} = MMaybe,
+    {{type, unnamed,0},{30,2},[<<"unnamed()">>], OpaqueDoc,
+     #{equiv := <<"non_neg_integer()">>, exported := true}} = Unnamed,
+    {{type, param,1},_,[<<"param(X)">>], GenericsDoc,
+     #{equiv := <<"madeup()">>, exported := true}} = Param,
+    {{type, natural_number,0},_,[<<"natural_number()">>], none, NaturalNumberMeta} = NatNumber,
+    {{type, name,1},_,[<<"name(_)">>], TypeDoc, #{exported := true}} = Name,
+    {{type, hidden_included_type, 0}, _, _, hidden, #{exported := false }} = HiddenIncludedType,
+
+    {{function,uses_public,0},{131,1},[<<"uses_public()">>],none,#{}} = UsesPublic,
+    {{function,ignore_type_from_hidden_fun,0},_,[<<"ignore_type_from_hidden_fun()">>],hidden,#{}} = Ignore,
+    {{function,map_fun,0},_,[<<"map_fun()">>],none,#{}} = MapFun,
+    {{function,private_encoding_func,2},_,[<<"private_encoding_func(Data, Options)">>],none,#{}} = PrivateEncoding,
+    {{function,foo,0},_,[<<"foo()">>],none,#{}} = Foo,
+
+    ?assertEqual(106, erl_anno:line(MyOtherPrivateTypeLine)),
+    ?assertEqual(105, erl_anno:line(MyPrivateTypeLine)),
+    ?assertEqual(102, erl_anno:line(MyMapLine)),
+    ?assertEqual(99, erl_anno:line(StateEnterLine)),
+    ?assertEqual(98, erl_anno:line(CallbackModeLine)),
+    ?assertEqual(96, erl_anno:line(CallbackResultLine)),
+
+    [{File, Ws}, {HrlFile, HrlWs}] = Warnings,
+    ?assertEqual("types_and_opaques.erl", filename:basename(File)),
+    ?assertEqual({{120,2}, beam_doc,
+                  {hidden_type_used_in_exported_fun,{hidden_type,0}}}, lists:nth(4, Ws)),
+
+    ?assertEqual("types_and_opaques.hrl", filename:basename(HrlFile)),
+    ?assertEqual({{1,2}, beam_doc,
+                  {hidden_type_used_in_exported_fun,{hidden_included_type,0}}}, lists:nth(1, HrlWs)),
+
+    {ok, ModName, [_]} =
+        default_compile_file(Conf, ModuleName, [return_warnings, nowarn_hidden_doc, nowarn_unused_type]),
+
+    ok.
+
+callback(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName, [{File,Warnings}]} =
+        default_compile_file(Conf, ModuleName, [return_warnings, report]),
+    Doc = #{<<"en">> => <<"Callback fn that always returns ok.">>},
+    ImpCallback = #{<<"en">> => <<"This is a test">>},
+    FunctionDoc = #{<<"en">> => <<"all_ok()\n\nCalls all_ok/0">>},
+    ChangeOrder = #{<<"en">> => <<"Test changing order">>},
+    {ok, {docs_v1, _,_, _, none, _,
+          [{{callback,nowarn,1},{39,2},[<<"nowarn(Arg)">>],hidden,#{}},
+           {{callback,warn,0},{36,2},[<<"warn()">>],hidden,#{}},
+           {{callback,bounded,1},_,[<<"bounded(X)">>],none,#{}},
+           {{callback,multi,1},_,[<<"multi(Argument)">>],
+            #{ <<"en">> := <<"A multiclause callback with slogan docs">> },#{}},
+           {{callback,multi_no_slogan,1},_,[<<"multi_no_slogan/1">>],none,#{}},
+           {{callback,ann,1},_,[<<"ann(X)">>],none,#{}},
+           {{callback,param,1},_,[<<"param(X)">>],none,#{}},
+           {{callback, change_order,0},_,[<<"change_order()">>], ChangeOrder,
+            #{equiv := <<"ok()">>}},
+           {{callback, all_ok,0},_,[<<"all_ok()">>], Doc, #{}},
+           {{function, main2,0},_,[<<"main2()">>], #{<<"en">> := <<"Second main">>},
+            #{equiv := <<"main()">>}},
+           {{function, main,0},_,[<<"main()">>], FunctionDoc, #{}},
+           {{function, all_ok,0},_, [<<"all_ok()">>],ImpCallback,
+            #{equiv := <<"ok/0">>}}
+          ]}} = code:get_doc(ModName),
+
+    ?assertEqual("callback.erl", filename:basename(File)),
+    io:format("Warnings: ~p~n", [Warnings]),
+    ?assertEqual(1, length(Warnings)),
+    ?assertMatch({{36,2},beam_doc,{hidden_callback,{warn,0}}}, lists:nth(1, Warnings)),
+
+    {ok, ModName, []} =
+        default_compile_file(Conf, ModuleName, [return_warnings, report, nowarn_hidden_doc]),
+
+    ok.
+
+private_types(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+
+    {ok, {docs_v1, _,_, _, none, _,
+          [
+           %% Types
+           RemoteTypeT, TupleT, RecordAT, RecordInlineT,
+           MapValue2T, MapKey2T, MapValueT, MapKeyT,
+           FunRet2T, FunRetT, FunT, Complex, BoundedRetT,
+           ArgT, BoundedArgT, Private, HiddenExportT, PrivateCBT,
+           PublicT, PrivateT,
+           %% Callbacks
+           CBar,
+           %% Functions
+           Bounded, HiddenTypeExposed, Hidden, Bar]}} = code:get_doc(ModName),
+
+    ?assertMatch({{type,remote_type_t,1}, _, _, none, #{exported := false}},RemoteTypeT),
+    ?assertMatch({{type,tuple_t,0}, _, _, none, #{exported := false}},TupleT),
+    ?assertMatch({{type,record_a_t,0}, _, _, none, #{exported := false}},RecordAT),
+    ?assertMatch({{type,record_inline_t,0}, _, _, none, #{exported := false}},RecordInlineT),
+    ?assertMatch({{type,map_value_2_t,0}, _, _, none, #{exported := false}},MapValue2T),
+    ?assertMatch({{type,map_key_2_t,0}, _, _, none, #{exported := false}},MapKey2T),
+    ?assertMatch({{type,map_value_t,0}, _, _, none, #{exported := false}},MapValueT),
+    ?assertMatch({{type,map_key_t,0}, _, _, none, #{exported := false}},MapKeyT),
+    ?assertMatch({{type,fun_ret_2_t,0}, _, _, none, #{exported := false}},FunRet2T),
+    ?assertMatch({{type,fun_ret_t,0}, _, _, none, #{exported := false}},FunRetT),
+    ?assertMatch({{type,fun_t,0}, _, _, none, #{exported := false}},FunT),
+    ?assertMatch({{type,complex,1}, _, _, none, #{exported := true}},Complex),
+    ?assertMatch({{type,bounded_ret_t,0}, _, _, none, #{exported := false}},BoundedRetT),
+    ?assertMatch({{type,arg_t,0}, _, _, none, #{exported := false}},ArgT),
+    ?assertMatch({{type,bounded_arg_t,0}, _, _, none, #{exported := false}},BoundedArgT),
+    ?assertMatch({{type,private,0}, {28,2}, [<<"private()">>], hidden, #{exported := false}},Private),
+    ?assertMatch({{type,hidden_export_t,0},_,[<<"hidden_export_t()">>],hidden,#{exported := true}},HiddenExportT),
+    ?assertMatch({{type,private_cb_t,0},_,_,none,#{exported := false}},PrivateCBT),
+    ?assertMatch({{type,public_t,0},_, [<<"public_t()">>], none,#{ exported := true}},PublicT),
+    ?assertMatch({{type,private_t,0},_, [<<"private_t()">>], none,#{ exported := false}},PrivateT),
+    ?assertMatch({{callback,bar,1},_,_,none,#{}},CBar),
+    ?assertMatch({{function,bounded,2},_,_,none,#{}},Bounded),
+    ?assertMatch({{function,hidden_type_exposed,0},{32,1},[<<"hidden_type_exposed()">>],none,#{}},HiddenTypeExposed),
+    ?assertMatch({{function,hidden,0},_,[<<"hidden()">>],hidden,#{}},Hidden),
+    ?assertMatch({{function,bar,0},_,[<<"bar()">>],none,#{}},Bar),
+
+    ok.
+
+
+export_all(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    ImpCallback = #{<<"en">> => <<"This is a test">>},
+    FunctionDoc = #{<<"en">> => <<"all_ok()\n\nCalls all_ok/0">>},
+    {ok, {docs_v1, _,_, _, none, _,
+          [{{function, main2,0},_,[<<"main2()">>], #{<<"en">> := <<"Second main">>},
+            #{equiv := <<"main()">>}},
+           {{function, main,0},_,[<<"main()">>], FunctionDoc, #{}},
+           {{function, all_ok,0},_, [<<"all_ok()">>],ImpCallback,
+            #{equiv := <<"ok/0">>}}
+          ]}} = code:get_doc(ModName),
+    ok.
+
+equiv(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    {ok, {docs_v1, _,_, _, none, _,
+          [{{function, main, 2},_,[<<"main(A, B)">>], none,
+            #{ }},
+            {{function, main, 1},_,[<<"main(A)">>], none,
+             #{ equiv := <<"main(A, 1)">> }}
+          ]}} = code:get_doc(ModName),
+    ok.
+
+spec(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    {ok, {docs_v1, _,_, _, none, _,
+          [{{type,no,0},_,[<<"no()">>],none,#{exported := false}},
+           {{type,yes,0},_,[<<"yes()">>],none,#{exported := false}},
+           {{callback,me,1},_,[<<"me/1">>],none,#{}},
+           {{function,baz,1},_,[<<"baz(X)">>],none,#{}},
+           {{function,foo,1},_,[<<"foo(X)">>],none,#{}}]}} = code:get_doc(ModName),
+    ok.
+
+user_defined_type(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    {ok, {docs_v1, _,_, _, none, _, []}} = code:get_doc(ModName),
+    ok.
+
+deprecated(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+    {ok, {docs_v1, _,_, _, none, _,
+          [{{type,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"the type deprecated:test(_) is deprecated; Deprecation reason">>}},
+           {{type,test,0},_,[<<"test()">>],none,#{deprecated := <<"the type deprecated:test() is deprecated; see the documentation for details">>}},
+           {{callback,test,0},_,[<<"test()">>],none,#{deprecated := <<"Meta reason">>}},
+           {{function,test,2},_,[<<"test(N, M)">>],none,#{deprecated := <<"Meta reason">>}},
+           {{function,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"deprecated:test/1 is deprecated; Deprecation reason">>}},
+           {{function,test,0},_,[<<"test()">>],none,#{deprecated := <<"deprecated:test/0 is deprecated; see the documentation for details">>}}]}} =
+        code:get_doc(ModName),
+
+    {ok, ModName} = default_compile_file(Conf, ModuleName, [{d,'TEST_WILDCARD'},
+                                                    {d, 'REASON', next_major_release}]),
+    {ok, {docs_v1, _,_, _, none, _,
+          [{{type,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"the type deprecated:test(_) is deprecated; see the documentation for details">>}},
+           {{type,test,0},_,[<<"test()">>],none,#{deprecated := <<"the type deprecated:test() is deprecated; see the documentation for details">>}},
+           {{callback,test,0},_,[<<"test()">>],none,#{deprecated := <<"Meta reason">>}},
+           {{function,test,2},_,[<<"test(N, M)">>],none,#{deprecated := <<"Meta reason">>}},
+           {{function,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"deprecated:test/1 is deprecated; will be removed in the next major release. See the documentation for details">>}},
+           {{function,test,0},_,[<<"test()">>],none,#{deprecated := <<"deprecated:test/0 is deprecated; see the documentation for details">>}}]}} =
+        code:get_doc(ModName),
+
+    {ok, ModName} = default_compile_file(Conf, ModuleName, [{d,'ALL_WILDCARD'},
+                                                    {d,'REASON',next_version},
+                                                    {d,'TREASON',eventually}]),
+    {ok, {docs_v1, _,_, _, none, _,
+          [{{type,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"the type deprecated:test(_) is deprecated; will be removed in a future release. See the documentation for details">>}},
+           {{type,test,0},_,[<<"test()">>],none,#{deprecated := <<"the type deprecated:test() is deprecated; see the documentation for details">>}},
+           {{callback,test,0},_,[<<"test()">>],none,#{deprecated := <<"Meta reason">>}},
+           {{function,test,2},_,[<<"test(N, M)">>],none,#{deprecated := <<"Meta reason">>}},
+           {{function,test,1},_,[<<"test(N)">>],none,#{deprecated := <<"deprecated:test/1 is deprecated; will be removed in the next version. See the documentation for details">>}},
+           {{function,test,0},_,[<<"test()">>],none,#{deprecated := <<"deprecated:test/0 is deprecated; see the documentation for details">>}}]}} =
+        code:get_doc(ModName),
+    ok.
+
+warn_missing_doc(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName, [{File,Warnings}, {HrlFile, HrlWarnings}]} =
+        default_compile_file(Conf, ModuleName, [return_warnings, warn_missing_doc, report]),
+
+    {ok, {docs_v1, _,_, _, none, _,
+          [{{type,test,1},_,[<<"test(N)">>],none,_},
+           {{type,test,0},_,[<<"test()">>],none,_},
+           {{callback,test,0},_,[<<"test()">>],none,_},
+           {{function,test,1},_,[<<"test(N)">>],none,_},
+           {{function,test,0},_,[<<"test()">>],none,_},
+           {{function,test,2},_,[<<"test(N, M)">>],none,_}]}
+    } = code:get_doc(ModName),
+
+    ?assertEqual("warn_missing_doc.erl", filename:basename(File)),
+    ?assertEqual(6, length(Warnings)),
+    ?assertMatch({1, beam_doc, missing_moduledoc}, lists:nth(1, Warnings)),
+    ?assertMatch({{6,2}, beam_doc, {missing_doc, {type,test,0}}}, lists:nth(2, Warnings)),
+    ?assertMatch({{7,2}, beam_doc, {missing_doc, {type,test,1}}}, lists:nth(3, Warnings)),
+    ?assertMatch({{9,2}, beam_doc, {missing_doc, {callback,test,0}}}, lists:nth(4, Warnings)),
+    ?assertMatch({{13,1}, beam_doc, {missing_doc, {function,test,0}}}, lists:nth(5, Warnings)),
+    ?assertMatch({{14,1}, beam_doc, {missing_doc, {function,test,1}}}, lists:nth(6, Warnings)),
+
+    ?assertEqual("warn_missing_doc.hrl", filename:basename(HrlFile)),
+    ?assertEqual(1, length(HrlWarnings)),
+    ?assertMatch({{2,1}, beam_doc, {missing_doc, {function,test,2}}}, lists:nth(1, HrlWarnings)),
+
+    ok.
+
+doc_with_file(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, Cwd} = file:get_cwd(),
+    try
+        ok = file:set_cwd(proplists:get_value(data_dir, Conf)),
+        {ok, ModName} = default_compile_file(Conf, ModuleName, [{i, "./folder"}]),
+        {ok, {docs_v1, ModuleAnno,_, _, #{<<"en">> := <<"# README\n\nThis is a test">>}, _,
+              [{{type,bar,1},_,[<<"bar(X)">>],none,#{exported := false}},
+               {{type,foo,1},_,[<<"foo(X)">>],none,#{exported := true}},
+               {{type,private_type_exported,0},_,[<<"private_type_exported()">>],
+                #{<<"en">> := <<"# TYPES\n\nTest">>}, #{exported := false}},
+               {{function,main2,1},Main2Anno,[<<"main2(I)">>],
+                #{<<"en">> := <<"# File\n\ntesting fetching docs from other folders">>}, #{}},
+               {{function,main,1},_,[<<"main(Var)">>],
+                #{<<"en">> := <<"# Fun\n\nTest importing function">>},#{}}]}} = code:get_doc(ModName),
+
+        ?assertEqual(1, erl_anno:line(ModuleAnno)),
+        ?assertEqual(1, erl_anno:line(Main2Anno)),
+        ?assertEqual("./folder/FILE", erl_anno:file(Main2Anno)),
+        ok
+    after
+        ok = file:set_cwd(Cwd)
+    end.
+
+doc_with_file_error(Conf) ->
+    ModuleName = ?get_name(),
+    {error,
+     [{_,
+       [{{6,2},epp,{moduledoc,file,"doesnotexist"}},
+        {{8,2},epp,{doc,file,"doesnotexist"}},
+        {{11,2},epp,{doc,file,"doesnotexist"}}]}] = Errors, []} = default_compile_file(Conf, ModuleName),
+    [[Mod:format_error(Error) || {_Loc, Mod, Error} <- Errs] || {_File, Errs} <- Errors],
+    {error, _, []} = default_compile_file(Conf, ModuleName, [report]),
+    ok.
+
+all_string_formats(Conf) ->
+    ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+
+    {ok, {docs_v1, _ModuleAnno,_, _, #{<<"en">> := <<"Moduledoc test module">>}, _,
+              [
+               {{function,six,0},_,_, #{<<"en">> := <<"all_string_formats-all_string_formats">>}, #{}},
+               {{function,five,0},_,_, #{<<"en">> := <<"all_string_formats-Doc module">>}, #{}},
+               {{function,four,0},_,_, #{<<"en">> := <<"Doc test mödule"/utf8>>}, #{}},
+               {{function,three,0},_,_, #{<<"en">> := <<"Doctestmodule">>}, #{}},
+               {{function,two,0},_,_, #{<<"en">> := <<"Doc test module">>}, #{}},
+               {{function,one,0},_,_, #{<<"en">> := <<"Doc test module">>}, #{}}
+              ]}} = code:get_doc(ModName),
+    ok.
+
+spec_switch_order(Conf) ->
+  ModuleName = ?get_name(),
+    {ok, ModName} = default_compile_file(Conf, ModuleName),
+
+    {ok, {docs_v1, _ModuleAnno,_, _, _, _,
+          [NotFalse, Other, Bar, Foo]}} = code:get_doc(ModName),
+  {{function,not_false,0}, {53,1}, [<<"not_false()">>], none,#{}} = NotFalse,
+  {{function,other,0},{37,2},[<<"other()">>],hidden,#{}} = Other,
+  {{function,bar,1},{31,2},[<<"bar(X)">>],hidden,#{}} = Bar,
+  {{function,foo,1}, {23, 2}, [<<"foo(Var)">>], #{ <<"en">> := <<"Foo does X">> }, #{}} = Foo.
+
+skip_doc(Conf) ->
+  ModuleName =?get_name(),
+  {ok, ModName} = default_compile_file(Conf, ModuleName, [no_docs]),
+
+  {ok,{docs_v1,0,erlang,<<"application/erlang+html">>,none,
+     #{generated := true,
+       otp_doc_vsn := {1,0,0}},
+       [{{function,main,0},{8,1},[<<"main/0">>],none,#{}},
+        {{function,foo,1},{16,1},[<<"foo/1">>],none,#{}}]}} = code:get_doc(ModName),
+
+  {ok, _ModName} = compile_file(Conf, ModuleName, [report, return_errors, no_docs]),
+  {error, missing} = code:get_doc(ModName),
+  ok.
+
+
+docs_from_ast(_Conf) ->
+    Code = """
+      -module(test).
+      -moduledoc "moduledoc".
+      -export([main/0]).
+      -doc "main".
+      main() -> ok.
+      """,
+
+    {ok, test, BeamCode} = compile:forms(scan_and_parse(Code),[debug_info]),
+    {ok, {test, [{documentation, Docs }]}} = beam_lib:chunks(BeamCode, [documentation]),
+
+    ?assertMatch(
+       #docs_v1{ module_doc = #{ <<"en">> := <<"moduledoc">> },
+                 anno = 2,
+                 docs = [{{function,main,0}, 4, _, #{ <<"en">> := <<"main">> }, _}]},
+       Docs),
+
+    check_no_doc_attributes(BeamCode),
+
+    {ok, test, BeamCodeWSource} = compile:forms(scan_and_parse(Code),[beam_docs, debug_info, {source, "test.erl"}]),
+    {ok, {test, [{documentation, DocsWSource }]}} = beam_lib:chunks(BeamCodeWSource, [documentation]),
+
+    ?assertMatch(
+       #docs_v1{ module_doc = #{ <<"en">> := <<"moduledoc">> },
+                 anno = 2,
+                 docs = [{{function,main,0}, 4,
+                          _, #{ <<"en">> := <<"main">> }, _}]},
+       DocsWSource),
+    check_no_doc_attributes(BeamCodeWSource),
+    ok.
+
+scan_and_parse(Code) ->
+    {ok, Toks, _} = erl_scan:string(Code),
+    parse(Toks).
+
+parse([]) -> [];
+parse(Toks) ->
+    {Form, [Dot | Rest]} = lists:splitwith(fun(E) -> element(1,E) =/= dot end, Toks),
+    {ok, F} = erl_parse:parse_form(Form ++ [Dot]),
+    [F | parse(Rest)].
+
+compile_file(Conf, ModuleName, ExtraOpts) ->
+    ErlModName = ModuleName ++ ".erl",
+    Filename = filename:join(proplists:get_value(data_dir, Conf), ErlModName),
+    io:format("Compiling: ~ts~n~p~n",[Filename, ExtraOpts]),
+    case compile:file(Filename, ExtraOpts) of
+        Res when element(1, Res) =:= ok ->
+            ModName = element(2, Res),
+            case lists:search(fun (X) -> X =:= no_docs end, ExtraOpts) of
+              false when length(ExtraOpts) > 0 ->
+                check_no_doc_attributes(code:which(ModName)),
+                Res;
+              _ ->
+                Res
+            end;
+        Else ->
+            Else
+    end.
+
+default_compile_file(Conf, ModuleName) ->
+  default_compile_file(Conf, ModuleName, []).
+default_compile_file(Conf, ModuleName, ExtraOpts) ->
+  compile_file(Conf, ModuleName, [report, return_errors, debug_info] ++ ExtraOpts).
+
+
+%% Verify that all doc and moduledoc attributes are stripped from debug_info
+check_no_doc_attributes(Mod) ->
+    {ok, {_ModName,
+          [{debug_info,
+            {debug_info_v1,erl_abstract_code,
+             {AST, Opts}}}]}} = beam_lib:chunks(Mod, [debug_info]),
+    false = lists:search(
+              fun(E) ->
+                      element(1,E) == attribute
+                          andalso
+                            (element(3,E) == doc orelse element(3,E) == moduledoc)
+              end, AST),
+    false = lists:member(no_docs, Opts),
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/FUN b/lib/compiler/test/beam_doc_SUITE_data/FUN
new file mode 100644
index 0000000000..ace17b0007
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/FUN
@@ -0,0 +1,3 @@
+# Fun
+
+Test importing function
diff --git a/lib/compiler/test/beam_doc_SUITE_data/README b/lib/compiler/test/beam_doc_SUITE_data/README
new file mode 100644
index 0000000000..935d425318
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/README
@@ -0,0 +1,3 @@
+# README
+
+This is a test
diff --git a/lib/compiler/test/beam_doc_SUITE_data/TYPES b/lib/compiler/test/beam_doc_SUITE_data/TYPES
new file mode 100644
index 0000000000..4b5f08ac26
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/TYPES
@@ -0,0 +1,3 @@
+# TYPES
+
+Test
diff --git a/lib/compiler/test/beam_doc_SUITE_data/all_string_formats.erl b/lib/compiler/test/beam_doc_SUITE_data/all_string_formats.erl
new file mode 100644
index 0000000000..c567fc046f
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/all_string_formats.erl
@@ -0,0 +1,20 @@
+-module(all_string_formats).
+
+-export([one/0,two/0,three/0,four/0,five/0,six/0]).
+
+-moduledoc """
+  Moduledoc test module
+  """.
+
+-doc ~S"Doc test module".
+one() -> ok.
+-doc ~B"Doc test module".
+two() -> ok.
+-doc <<"Doc","test","modul",$e>>.
+three() -> ok.
+-doc <<"Doc test mödule"/utf8>>.
+four() -> ok.
+-doc <<?MODULE_STRING, "-Doc module">>.
+five() -> ok.
+-doc ?MODULE_STRING "-" ?MODULE_STRING.
+six() -> ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/callback.erl b/lib/compiler/test/beam_doc_SUITE_data/callback.erl
new file mode 100644
index 0000000000..1f0bf1aae0
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/callback.erl
@@ -0,0 +1,69 @@
+-module(callback).
+
+%% -doc "
+%% This should be ignored
+%% ".
+%% -behaviour(gen_server).
+
+-export([all_ok/0, main/0, main2/0]).
+
+-doc "
+Callback fn that always returns ok.
+".
+-callback all_ok() -> ok.
+
+-doc "
+Test changing order
+".
+-doc #{equiv => ok()}.
+-callback change_order() -> Order :: boolean().
+
+-callback param(X) -> X.
+-callback ann(X :: integer()) -> Y :: integer().
+
+-callback multi_no_slogan(X :: integer()) -> X :: integer();
+                         (X :: atom()) -> X :: atom().
+
+
+-doc "multi(Argument)
+
+A multiclause callback with slogan docs".
+-callback multi(X :: integer()) -> X :: integer();
+               (X :: atom()) -> X :: atom().
+
+-callback bounded(X) -> integer() when X :: integer().
+
+-doc hidden.
+-callback warn() -> ok.
+
+-doc hidden.
+-compile({nowarn_hidden_doc, nowarn/1}).
+-callback nowarn(Arg :: atom()) -> ok.
+
+
+-doc #{equiv => ok/0}.
+-doc "
+This is a test
+".
+all_ok() ->
+    all_ok().
+
+-doc #{equiv => main()}.
+-spec main() -> ok.
+-doc "
+all_ok()
+
+Calls all_ok/0
+".
+main() ->
+    all_ok().
+
+-doc #{equiv => main()}.
+-doc "
+main2()
+
+Second main
+".
+-spec main2() -> ok.
+main2() ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/deprecated.erl b/lib/compiler/test/beam_doc_SUITE_data/deprecated.erl
new file mode 100644
index 0000000000..a2edfec1b0
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/deprecated.erl
@@ -0,0 +1,46 @@
+-module(deprecated).
+
+-export([test/0, test/1, test/2]).
+-export_type([test/0, test/1]).
+
+-ifndef(REASON).
+-define(REASON,"Deprecation reason").
+-endif.
+
+-ifndef(TREASON).
+-define(TREASON,"Deprecation reason").
+-endif.
+
+-doc #{ deprecated => "Meta reason" }.
+-callback test() -> ok.
+
+-ifdef(TEST_WILDCARD).
+-deprecated({test, '_', ?REASON}).
+-else.
+-ifdef(ALL_WILDCARD).
+-deprecated({'_', '_', ?REASON}).
+-else.
+-deprecated({test, 1, ?REASON}).
+-endif.
+-endif.
+-deprecated({test, 0}).
+
+-ifdef(TEST_WILDCARD).
+-deprecated_type({test, '_'}).
+-else.
+-ifdef(ALL_WILDCARD).
+-deprecated_type({'_', '_', ?TREASON}).
+-else.
+-deprecated_type({test, 1, ?TREASON}).
+-endif.
+-endif.
+-deprecated_type({test, 0}).
+-type test() :: ok.
+-type test(N) :: N.
+
+test() -> ok.
+test(N) -> N.
+
+-doc #{ deprecated => "Meta reason" }.
+test(N,M) -> N + M.
+     
diff --git a/lib/compiler/test/beam_doc_SUITE_data/doc_with_file.erl b/lib/compiler/test/beam_doc_SUITE_data/doc_with_file.erl
new file mode 100644
index 0000000000..ff4c0040a8
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/doc_with_file.erl
@@ -0,0 +1,25 @@
+-module(doc_with_file).
+
+-export([main/1, main2/1]).
+-export_type([foo/1]).
+
+-moduledoc {file, "README"}.
+
+-doc {file, "TYPES"}.
+-type private_type_exported() :: integer().
+
+-doc {file, "FUN"}.
+-spec main(Var) -> foo(Var).
+main(X) ->
+    X.
+
+-include("doc_with_file.hrl").
+-spec main2( I :: integer()) -> bar(I :: integer()).
+main2(X) when is_atom(X) ->
+    X;
+main2(X) ->
+    X.
+
+
+-type foo(X) :: { X, private_type_exported()}.
+-type bar(X) :: foo({X, private_type_exported()}).
diff --git a/lib/compiler/test/beam_doc_SUITE_data/doc_with_file_error.erl b/lib/compiler/test/beam_doc_SUITE_data/doc_with_file_error.erl
new file mode 100644
index 0000000000..baa7034cc6
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/doc_with_file_error.erl
@@ -0,0 +1,24 @@
+-module(doc_with_file_error).
+
+-export([main/1, main2/1]).
+-export_type([foo/1]).
+
+-moduledoc {file, "doesnotexist"}.
+
+-doc {file, "doesnotexist"}.
+-type private_type_exported() :: integer().
+
+-doc {file, "doesnotexist"}.
+-spec main(Var) -> foo(Var).
+main(X) ->
+    X.
+
+-doc(({file, "folder/doesnotexist"})).
+-spec main2( I :: integer()) -> bar(I :: integer()).
+main2(X) when is_atom(X) ->
+    X;
+main2(X) ->
+    X.
+
+-type foo(X) :: { X, private_type_exported()}.
+-type bar(X) :: foo({X, private_type_exported()}).
diff --git a/lib/compiler/test/beam_doc_SUITE_data/docformat.erl b/lib/compiler/test/beam_doc_SUITE_data/docformat.erl
new file mode 100644
index 0000000000..40b1bd3762
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/docformat.erl
@@ -0,0 +1,18 @@
+-module(docformat).
+
+-export([main/0]).
+
+
+-moduledoc #{since => "1.0"}.
+-moduledoc #{deprecated => "Use something else"}.
+-moduledoc #{format => "text/asciidoc"}.
+-moduledoc "
+Moduledoc test module
+".
+
+
+-doc "
+Doc test module
+".
+main() ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/docmodule_with_doc_attributes.erl b/lib/compiler/test/beam_doc_SUITE_data/docmodule_with_doc_attributes.erl
new file mode 100644
index 0000000000..7d1dbe1d4d
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/docmodule_with_doc_attributes.erl
@@ -0,0 +1,36 @@
+-module(docmodule_with_doc_attributes).
+
+-export([main/0, ok/0, no_docs/0, no_docs_multi/1, with_file_docs/0]).
+
+-moduledoc "
+Moduledoc test module
+".
+
+
+-doc "
+Doc test module
+".
+main() ->
+    ok().
+
+
+-doc #{authors => "Someone"}.
+ok() ->
+     no_docs().
+
+no_docs() ->
+    ok.
+
+-doc {file, "README"}.
+with_file_docs() ->
+    ok.
+
+no_docs_multi(a) ->
+    a;
+no_docs_multi(A) ->
+    private_multi(A).
+
+private_multi(a) ->
+    a;
+private_multi(A) ->
+    A.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/equiv.erl b/lib/compiler/test/beam_doc_SUITE_data/equiv.erl
new file mode 100644
index 0000000000..a111066356
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/equiv.erl
@@ -0,0 +1,10 @@
+-module(equiv).
+
+-export([main/1, main/2]).
+
+-doc #{ equiv => main(A, 1) }.
+main(A) ->
+    main(A, 1).
+
+main(A, B) ->
+    {A, B}.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/export_all.erl b/lib/compiler/test/beam_doc_SUITE_data/export_all.erl
new file mode 100644
index 0000000000..13d4e7d39c
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/export_all.erl
@@ -0,0 +1,31 @@
+-module(export_all).
+
+-compile(export_all).
+
+
+-doc #{equiv => ok/0}.
+-doc "
+This is a test
+".
+all_ok() ->
+    all_ok().
+
+-doc #{equiv => main()}.
+-spec main() -> ok.
+-doc "
+all_ok()
+
+Calls all_ok/0
+".
+main() ->
+    all_ok().
+
+-doc #{equiv => main()}.
+-doc "
+main2()
+
+Second main
+".
+-spec main2() -> ok.
+main2() ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/folder/FILE b/lib/compiler/test/beam_doc_SUITE_data/folder/FILE
new file mode 100644
index 0000000000..efb71e584a
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/folder/FILE
@@ -0,0 +1,3 @@
+# File
+
+testing fetching docs from other folders
diff --git a/lib/compiler/test/beam_doc_SUITE_data/folder/doc_with_file.hrl b/lib/compiler/test/beam_doc_SUITE_data/folder/doc_with_file.hrl
new file mode 100644
index 0000000000..a9d40b48df
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/folder/doc_with_file.hrl
@@ -0,0 +1 @@
+-doc({file, "FILE"}).
diff --git a/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc.erl b/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc.erl
new file mode 100644
index 0000000000..2d02658e24
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc.erl
@@ -0,0 +1,15 @@
+-module(hide_moduledoc).
+
+-export([main/0]).
+
+-moduledoc false.
+
+-doc "
+Doc test module
+".
+main() ->
+    ok().
+
+-doc #{since => "1.0"}.
+ok() ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc2.erl b/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc2.erl
new file mode 100644
index 0000000000..12bea08d52
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/hide_moduledoc2.erl
@@ -0,0 +1,25 @@
+-module(hide_moduledoc2).
+
+-export([main/0, handle_call/1]).
+
+-moduledoc hidden.
+
+-doc "
+Doc test module
+".
+-doc hidden.
+main() ->
+    ok().
+
+-doc #{since => "1.0"}.
+-doc hidden.
+ok() ->
+    ok.
+
+-doc false.
+-spec handle_call('which' | {'add',atom()} | {'delete',atom()}) ->
+        {'reply', 'ok' | [atom()]}.
+
+handle_call({add,_Address}=A) -> A;
+handle_call({delete,_Address}=D) -> D;
+handle_call(which) -> which.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/private_types.erl b/lib/compiler/test/beam_doc_SUITE_data/private_types.erl
new file mode 100644
index 0000000000..a490dc29f1
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/private_types.erl
@@ -0,0 +1,65 @@
+-module(private_types).
+
+-export([bar/0, hidden/0, hidden_type_exposed/0, bounded/2]).
+-export_type([public_t/0, hidden_export_t/0, complex/1]).
+
+-type private_t() :: integer(). %% In chunk because referred to by exported bar/0
+-type public_t() :: integer(). %% In chunk because exported
+-type private_cb_t() :: integer(). %% In chunk because referred to by callback
+-type local_t() :: integer(). %% Not in chunk because only referred by non-exported function
+
+-doc false.
+-type hidden_export_t() :: integer(). %% In chunk because exported
+
+-callback bar(private_cb_t()) -> ok.
+
+-spec bar() -> private_t().
+bar() -> baz().
+
+-spec baz() -> local_t().
+baz() -> 1.
+
+-type hidden_t() :: integer(). %% Not in chunk because only referred to by hidden function
+
+-doc false.
+-spec hidden() -> hidden_t().
+hidden() -> 1.
+
+-doc false.
+-type private() :: integer().
+
+-spec hidden_type_exposed() -> private().
+hidden_type_exposed() -> 1.
+
+-type bounded_arg_t() :: integer().
+-type arg_t() :: integer().
+-type bounded_ret_t() :: integer().
+-spec bounded(A :: arg_t(), B) -> C
+              when B :: bounded_arg_t(),
+                   C :: bounded_ret_t().
+bounded(A, B) -> A + B.
+
+-record(r,{ a :: record_a_t(), f :: record_f_t(), rec :: #r{} }). %% We have a recusive type to make sure we handle that
+-type complex(A) ::
+        [fun((fun_t()) -> fun_ret_t()) |
+         fun((...) -> fun_ret_2_t()) |
+         #{ map_key_t() := map_value_t(),
+            map_key_2_t() => map_value_2_t() } |
+         #r{ f :: record_inline_t() } |
+         {tuple_t()} |
+         maps:iterator_order(remote_type_t(A)) |
+         [] | 1 .. 2
+        ].
+
+-type fun_t() :: integer().
+-type fun_ret_t() :: integer().
+-type fun_ret_2_t() :: integer().
+-type map_key_t() :: integer().
+-type map_value_t() :: integer().
+-type map_key_2_t() :: integer().
+-type map_value_2_t() :: integer().
+-type record_inline_t() :: integer().
+-type record_a_t() :: integer().
+-type record_f_t() :: integer(). %% Should not be included as #r{ f } overrides it
+-type tuple_t() :: integer().
+-type remote_type_t(A) :: A.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/singleton_docformat.erl b/lib/compiler/test/beam_doc_SUITE_data/singleton_docformat.erl
new file mode 100644
index 0000000000..7aab513844
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/singleton_docformat.erl
@@ -0,0 +1,21 @@
+-module(singleton_docformat).
+
+-export([main/0]).
+
+-moduledoc #{format => <<"text/asciidoc">>,
+             since => "1.0",
+             deprecated => "Use something else"}.
+-moduledoc "
+Moduledoc test module
+".
+
+
+-doc #{ authors => [<<"Beep Bop">>] }.
+-doc #{ equiv => main/3 }.
+-doc "
+Doc test module
+
+More info here
+".
+main() ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/singleton_meta.erl b/lib/compiler/test/beam_doc_SUITE_data/singleton_meta.erl
new file mode 100644
index 0000000000..aa4bb6fb30
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/singleton_meta.erl
@@ -0,0 +1,17 @@
+-module(singleton_meta).
+
+-export([main/0, main1/0]).
+
+-doc #{ authors => [<<"Beep Bop">>] }.
+-doc #{ equiv => main/3 }.
+main() ->
+    main1().
+
+-doc (#{ equiv => main(_) }).
+-doc "
+main1()
+
+Returns always ok.
+".
+main1() ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/singletondoc.erl b/lib/compiler/test/beam_doc_SUITE_data/singletondoc.erl
new file mode 100644
index 0000000000..ed5b349a9d
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/singletondoc.erl
@@ -0,0 +1,19 @@
+-module(singletondoc).
+
+-export([main/0, foo/1]).
+
+-doc "
+Doc test module
+".
+main() ->
+    ok.
+
+-doc "
+foo(ok)
+
+Tests multi-clauses
+".
+foo(X) when is_atom(X) ->
+    X;
+foo(_) ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/singletonmoduledoc.erl b/lib/compiler/test/beam_doc_SUITE_data/singletonmoduledoc.erl
new file mode 100644
index 0000000000..cd81012d08
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/singletonmoduledoc.erl
@@ -0,0 +1,7 @@
+-module(singletonmoduledoc).
+
+-export([]).
+
+-moduledoc "
+Moduledoc test module
+".
diff --git a/lib/compiler/test/beam_doc_SUITE_data/skip_doc.erl b/lib/compiler/test/beam_doc_SUITE_data/skip_doc.erl
new file mode 100644
index 0000000000..afb0323104
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/skip_doc.erl
@@ -0,0 +1,19 @@
+-module(skip_doc).
+
+-export([main/0, foo/1]).
+
+-doc "
+Doc test module
+".
+main() ->
+    ok.
+
+-doc "
+foo(ok)
+
+Tests multi-clauses
+".
+foo(X) when is_atom(X) ->
+    X;
+foo(_) ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/slogan.erl b/lib/compiler/test/beam_doc_SUITE_data/slogan.erl
new file mode 100644
index 0000000000..45f5aca908
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/slogan.erl
@@ -0,0 +1,73 @@
+-module(slogan).
+
+-export([main/1,
+         bar/0,
+         no_slogan/1,
+         spec_slogan/1,
+         spec_slogan/2,
+         no_doc_slogan/1,
+         spec_no_doc_slogan/1,
+         spec_multiclause_slogan_ignored/1,
+         connect/2
+        ]).
+
+-doc "
+main(Foo)
+
+Returns ok.
+".
+-spec main(X :: integer()) -> ok.
+main(_X) ->
+    ok.
+
+-doc "
+foo()
+Not a slogan since foo =/= bar
+".
+bar() ->
+    ok.
+
+-doc "
+Not a slogan
+
+Tests slogans in multi-clause
+".
+-spec no_slogan(atom()) -> atom();
+               (term()) -> ok.
+no_slogan(X) when is_atom(X) ->
+    X;
+no_slogan(_X) ->
+    ok.
+
+-spec spec_slogan(Y :: integer()) -> integer() | ok.
+-doc "Not a slogan".
+spec_slogan(_X) -> ok.
+
+-spec spec_slogan(Y :: integer(), Z :: integer()) -> integer() | ok.
+-doc "Not a slogan".
+spec_slogan(_X, _Y) -> _X + _Y.
+
+no_doc_slogan(X) -> X.
+
+-spec spec_no_doc_slogan(Y) -> Y.
+spec_no_doc_slogan(X) ->
+    X.
+
+
+-spec spec_multiclause_slogan_ignored(Y) -> Y;
+                                     (Z) -> Z when Z :: integer().
+spec_multiclause_slogan_ignored(X) ->
+    X.
+
+
+-doc(#{equiv => connect/3}).
+-doc(#{since => <<"OTP R14B">>}).
+-spec connect(TCPSocket, TLSOptions) ->
+                     {ok, sslsocket} |
+                     {error, reason} |
+                     {option_not_a_key_value_tuple, any()} when
+      TCPSocket :: socket,
+      TLSOptions :: [tls_client_option].
+
+connect(Socket, SslOptions) ->
+    {Socket, SslOptions}.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/spec.erl b/lib/compiler/test/beam_doc_SUITE_data/spec.erl
new file mode 100644
index 0000000000..df0331b1ce
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/spec.erl
@@ -0,0 +1,24 @@
+-module(spec).
+
+-type yes() :: integer().
+-type no() :: atom().
+
+-export([foo/1, baz/1]).
+
+-record(name, {field = undefined :: atom()}).
+
+-callback me(X :: yes()) -> no();
+            (no()) -> yes();
+            (term()) -> #name{ }.
+
+-spec spec:foo(yes()) -> {yes(), yes()} | yes();
+              (no()) -> no().
+foo(X) ->
+    _ = #name{field = none},
+    X.
+
+
+-spec baz(Z) -> Z when Z :: yes();
+         (no()) -> no().
+baz(X) ->
+    X.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/spec_switch_order.erl b/lib/compiler/test/beam_doc_SUITE_data/spec_switch_order.erl
new file mode 100644
index 0000000000..9aa9262474
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/spec_switch_order.erl
@@ -0,0 +1,54 @@
+-module(spec_switch_order).
+
+-export([foo/1, bar/1, other/0, not_false/0]).
+
+-doc hidden.
+-spec foo(integer()) -> mytype().
+
+
+
+-doc "
+bar(Var)
+".
+-spec bar(integer()) -> another_type().
+
+
+
+
+-type mytype() :: ok.
+-type another_type() :: ok.
+
+
+
+-doc "
+foo(Var)
+
+Foo does X
+".
+foo(_X) ->
+    ok.
+
+-doc hidden.
+bar(_X) ->
+    ok.
+
+
+%% Ordering issue
+-doc false.
+-spec other() -> ok.
+%% docs #{ {other, 0} => {hidden, none, #{}}
+%% need to reset fields
+
+-spec not_false() -> ok.
+%% create entry with current fields
+%% reset state
+
+-doc #{}.
+other() ->
+    ok.
+%% fetch or create entry with for other
+%% update unset fields
+%% we need to differenciate between unset and set with default
+
+not_false() ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.erl b/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.erl
new file mode 100644
index 0000000000..4512f66a4d
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.erl
@@ -0,0 +1,147 @@
+-module(types_and_opaques).
+
+-export([foo/0, private_encoding_func/2,map_fun/0,ignore_type_from_hidden_fun/0]).
+
+-export_type([name/1,unnamed/0, mmaybe/1, callback_mode_result/0]).
+
+-export([uses_public/0]).
+-export_type([public/0]).
+
+-include("types_and_opaques.hrl").
+
+-doc "
+name(_)
+
+Represents the name of a person.
+".
+-type name(_Ignored) :: string().
+
+-doc #{since => "1.0"}.
+-doc #{equiv => non_neg_integer/0}.
+-type natural_number() :: non_neg_integer().
+
+-doc "
+Tests generics
+".
+-doc #{equiv => madeup()}.
+-type param(X) :: {X, integer(), Y :: string()}.
+
+-doc #{equiv => non_neg_integer()}.
+-doc "
+unnamed()
+
+Represents the name of a person that cannot be named.
+".
+-opaque unnamed() :: name(integer()).
+
+
+
+-export_type([natural_number/0, param/1]).
+
+
+
+-doc #{ authors => "Someone else" }.
+-doc "
+mmaybe(X) ::= nothing | X.
+
+Represents a maybe type.
+".
+-opaque mmaybe(X) :: nothing | X.
+
+-opaque non_exported() :: atom().
+
+-type not_exported_either() :: atom().
+
+-doc hidden.
+-doc #{ authors => "Someone else" }.
+-type hidden_false() :: atom().
+
+-doc false.
+-doc "
+Here is ok.
+".
+-type hidden() :: hidden_false().
+
+
+
+-export_type([hidden_false/0, hidden/0]).
+
+
+
+-type one() :: 1.
+-type two() :: one().
+-type three() :: two().
+-type four() :: three().
+
+-spec foo() -> three().
+foo() -> 1.
+
+
+-type encoding_func() :: fun((non_neg_integer()) -> boolean()).
+
+-spec private_encoding_func(Data, Options) -> AbsTerm when
+      Data :: term(),
+      Options :: Location | [Option],
+      Option :: {encoding, Encoding}
+              | {line, Line}
+              | {location, Location},
+      Encoding :: 'latin1' | 'unicode' | 'utf8' | 'none' | encoding_func(),
+      Line :: erl_anno:line(),
+      Location :: erl_anno:location(),
+      AbsTerm :: term().
+private_encoding_func(_, _) ->
+    ok.
+
+
+-type callback_mode_result() ::
+	callback_mode() | [callback_mode() | state_enter()].
+-type callback_mode() :: 'state_functions' | 'handle_event_function'.
+-type state_enter() :: 'state_enter'.
+
+
+-type mymap() :: #{ foo => my_private_type(),
+                    bar := my_other_private_type()}.
+
+-type my_private_type() :: integer().
+-type my_other_private_type() :: non_neg_integer().
+
+-spec map_fun() -> mymap().
+map_fun() ->
+    ok.
+
+
+-doc false.
+-spec ignore_type_from_hidden_fun() -> four().
+ignore_type_from_hidden_fun() ->
+    ok.
+
+%% Type below should be a warning, since it is refered
+%% by a public function or type and the inner type is hidden.
+-doc false.
+-type hidden_type() :: integer().
+%% Test suppression of hidden type warning.
+-doc false.
+-type hidden_nowarn_type() :: integer().
+-compile({nowarn_hidden_doc, [hidden_nowarn_type/0]}).
+
+-type intermediate() :: hidden_type() | hidden_included_type() | hidden_nowarn_type().
+-type public() :: intermediate().
+
+-spec uses_public() -> public().
+uses_public() ->
+    qux().
+
+-doc false.
+-doc "
+Hidden function with doc attribute
+".
+qux() ->
+    qux2().
+
+
+-doc "
+Hidden function with doc attribute
+".
+-doc false.
+qux2() ->
+    ok.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.hrl b/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.hrl
new file mode 100644
index 0000000000..eda452edab
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/types_and_opaques.hrl
@@ -0,0 +1,2 @@
+-doc false.
+-type hidden_included_type() :: integer().
diff --git a/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.erl b/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.erl
new file mode 100644
index 0000000000..6d5b745b39
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.erl
@@ -0,0 +1,3 @@
+-module(user_defined_type).
+
+-include("user_defined_type.hrl").
diff --git a/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.hrl b/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.hrl
new file mode 100644
index 0000000000..63356724c9
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/user_defined_type.hrl
@@ -0,0 +1,2 @@
+-type foo() :: integer().
+-type foo_dependent() :: foo().
diff --git a/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.erl b/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.erl
new file mode 100644
index 0000000000..d2b6c8f38f
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.erl
@@ -0,0 +1,14 @@
+-module(warn_missing_doc).
+
+-export([test/0, test/1, test/2]).
+-export_type([test/0, test/1]).
+
+-type test() :: ok.
+-type test(N) :: N.
+
+-callback test() -> ok.
+
+-include("warn_missing_doc.hrl").
+
+test() -> ok.
+test(N) -> N.
diff --git a/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.hrl b/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.hrl
new file mode 100644
index 0000000000..5b702b9078
--- /dev/null
+++ b/lib/compiler/test/beam_doc_SUITE_data/warn_missing_doc.hrl
@@ -0,0 +1,2 @@
+-doc #{ }.
+test(N,M) -> N + M.
diff --git a/lib/compiler/test/compile_SUITE.erl b/lib/compiler/test/compile_SUITE.erl
index 2c6f260618..8093b731c3 100644
--- a/lib/compiler/test/compile_SUITE.erl
+++ b/lib/compiler/test/compile_SUITE.erl
@@ -408,15 +408,22 @@ makedep(Config) when is_list(Config) ->
 
     %% Generate dependencies and compile normally at the same time.
     GeneratedHrl = filename:join(PrivDir, "generated.hrl"),
-    ok = file:write_file(GeneratedHrl, ""),
-    {ok,simple} = compile:file(Simple, [report,makedep_side_effect,
-                                        {makedep_output,Target},
-                                        {i,PrivDir}|IncludeOptions]),
-    {ok,Mf9} = file:read_file(Target),
-    BasicMf3 = iolist_to_binary([string:trim(BasicMf2), " ", filename:join(PrivDir, "generated.hrl"), "\n"]),
-    BasicMf3 = makedep_canonicalize_result(Mf9, DataDir),
-    error = compile:file(Simple, [report,makedep_side_effect,
-                                  {makedep_output,PrivDir}|IncludeOptions]),
+    GeneratedDoc = filename:join(proplists:get_value(data_dir, Config), "foo.md"),
+    try
+        ok = file:write_file(GeneratedHrl, ""),
+        ok = file:write_file(GeneratedDoc, ""),
+        {ok,simple} = compile:file(Simple, [report,makedep_side_effect,
+                                            {makedep_output,Target},
+                                            {i,PrivDir}|IncludeOptions]),
+        {ok,Mf9} = file:read_file(Target),
+        BasicMf3 = iolist_to_binary([string:trim(BasicMf2), " $(srcdir)/foo.md ", filename:join(PrivDir, "generated.hrl"), "\n"]),
+        BasicMf3 = makedep_canonicalize_result(Mf9, DataDir),
+        error = compile:file(Simple, [report,makedep_side_effect,
+                                      {makedep_output,PrivDir}|IncludeOptions])
+    after
+        ok = file:delete(GeneratedHrl),
+        ok = file:delete(GeneratedDoc)
+    end,
 
     %% Cover generation of long lines that must be split.
     CompileModule = filename:join(code:lib_dir(compiler), "src/compile.erl"),
@@ -432,7 +439,6 @@ makedep(Config) when is_list(Config) ->
     error = compile:file(Simple, [report,makedep,{makedep_output,a_bad_output_device}]),
 
     ok = file:delete(Target),
-    ok = file:delete(GeneratedHrl),
     ok = file:del_dir(filename:dirname(Target)),
     ok.
 
@@ -707,7 +713,6 @@ encrypted_abstr_1(Simple, Target) ->
     erpc:call(
       Node,
       fun() ->
-              {ok,OldCwd} = file:get_cwd(),
               ok = file:set_cwd(TargetDir),
 
               error = compile:file(Simple, [encrypt_debug_info,report]),
diff --git a/lib/compiler/test/compile_SUITE_data/simple-basic1.mk b/lib/compiler/test/compile_SUITE_data/simple-basic1.mk
index 4073fa82d0..53bec35f96 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-basic1.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-basic1.mk
@@ -1 +1 @@
-simple.beam: $(srcdir)/simple.erl
+simple.beam: $(srcdir)/simple.erl $(srcdir)/unicode-0.md
diff --git a/lib/compiler/test/compile_SUITE_data/simple-basic2.mk b/lib/compiler/test/compile_SUITE_data/simple-basic2.mk
index 761d1d9582..98406b8083 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-basic2.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-basic2.mk
@@ -1 +1 @@
-simple.beam: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl
+simple.beam: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl
diff --git a/lib/compiler/test/compile_SUITE_data/simple-missing.mk b/lib/compiler/test/compile_SUITE_data/simple-missing.mk
index b13d44ec36..38fccfd82e 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-missing.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-missing.mk
@@ -1 +1 @@
-simple.beam: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl generated.hrl
+simple.beam: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl generated.hrl
diff --git a/lib/compiler/test/compile_SUITE_data/simple-phony.mk b/lib/compiler/test/compile_SUITE_data/simple-phony.mk
index c84bcedd2a..fb049c1db7 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-phony.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-phony.mk
@@ -1,3 +1,5 @@
-simple.beam: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl
+simple.beam: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl
+
+$(srcdir)/unicode-0.md:
 
 $(srcdir)/include/simple.hrl:
diff --git a/lib/compiler/test/compile_SUITE_data/simple-target1.mk b/lib/compiler/test/compile_SUITE_data/simple-target1.mk
index dd9fa0d6e5..8b31c4c696 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-target1.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-target1.mk
@@ -1 +1 @@
-$target: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl
+$target: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl
diff --git a/lib/compiler/test/compile_SUITE_data/simple-target2.mk b/lib/compiler/test/compile_SUITE_data/simple-target2.mk
index a5fc6f461d..f41b9a066c 100644
--- a/lib/compiler/test/compile_SUITE_data/simple-target2.mk
+++ b/lib/compiler/test/compile_SUITE_data/simple-target2.mk
@@ -1 +1 @@
-$$target: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl
+$$target: $(srcdir)/simple.erl $(srcdir)/unicode-0.md $(srcdir)/include/simple.hrl
diff --git a/lib/compiler/test/compile_SUITE_data/simple.erl b/lib/compiler/test/compile_SUITE_data/simple.erl
index 9385d101e0..5129e6e204 100644
--- a/lib/compiler/test/compile_SUITE_data/simple.erl
+++ b/lib/compiler/test/compile_SUITE_data/simple.erl
@@ -28,6 +28,7 @@
 test() ->
     passed.
 
+-doc {file, "unicode-0.md"}.
 unicode() ->
     {"это",'спутник'}.
 
@@ -37,6 +38,9 @@ unicode() ->
 -ifdef(need_foo).
 -include("simple.hrl").
 
+-ifdef(include_generated).
+-doc {file, "foo.md"}.
+-endif.
 foo() ->
     {?included_value, ?foo_value}.
 
diff --git a/lib/compiler/test/compile_SUITE_data/unicode-0.md b/lib/compiler/test/compile_SUITE_data/unicode-0.md
new file mode 100644
index 0000000000..b95d9f74a1
--- /dev/null
+++ b/lib/compiler/test/compile_SUITE_data/unicode-0.md
@@ -0,0 +1 @@
+Small test docs
diff --git a/lib/erl_docgen/priv/bin/validate_links.escript b/lib/erl_docgen/priv/bin/validate_links.escript
index d1bccfa3fc..0ea9b3730c 100755
--- a/lib/erl_docgen/priv/bin/validate_links.escript
+++ b/lib/erl_docgen/priv/bin/validate_links.escript
@@ -210,13 +210,12 @@ parse_mod2app(Filename) ->
 
 validate_links({Filename, Links}, CachedFiles) ->
     %% io:format("~s ~p~n",[Links]),
-    lists:foreach(
-      fun({LinkType, TypeLinks}) ->
-              lists:foreach(
-                fun({Line,Link}) ->
-                        validate_link(Filename, LinkType, Line, Link, CachedFiles)
-                end, TypeLinks)
-      end, maps:to_list(maps:filter(fun(Key,_) -> not is_atom(Key) end,Links))).
+    maps:foreach(fun(LinkType, TypeLinks) when not is_atom(LinkType) ->
+                       lists:foreach(fun({Line,Link}) ->
+                                             validate_link(Filename, LinkType, Line, Link, CachedFiles)
+                                     end, TypeLinks);
+                    (_, _) -> ok
+                 end, Links).
 validate_link(Filename, LinkType, Line, [{"marker",Marker}], CachedFiles) ->
     validate_link(Filename, LinkType, Line, Marker, CachedFiles);
 validate_link(Filename, "seemfa", Line, Link, CachedFiles) ->
diff --git a/lib/stdlib/doc/src/beam_lib.xml b/lib/stdlib/doc/src/beam_lib.xml
index e743741ea8..0d2a6586cf 100644
--- a/lib/stdlib/doc/src/beam_lib.xml
+++ b/lib/stdlib/doc/src/beam_lib.xml
@@ -52,6 +52,7 @@
       <item><c>labeled_exports ("ExpT")</c></item>
       <item><c>labeled_locals ("LocT")</c></item>
       <item><c>locals ("LocT")</c></item>
+      <item><c>documentation ("Docs")</c></item>
     </list>
   </description>
 
@@ -200,7 +201,7 @@ io:fwrite("~s~n", [erl_prettypr:format(erl_syntax:form_list(AC))]).</code>
     <datatype>
       <name name="chunkid"/>
       <desc>
-        <p>"Attr" | "CInf" | "Dbgi" | "ExpT" | "ImpT" | "LocT" | "AtU8"</p>
+        <p><c>"Attr" | "CInf" | "Dbgi" | "ExpT" | "ImpT" | "LocT" | "AtU8" | "Docs"</c></p>
       </desc>
     </datatype>
     <datatype>
@@ -236,6 +237,15 @@ io:fwrite("~s~n", [erl_prettypr:format(erl_syntax:form_list(AC))]).</code>
           is automatically computed from the <c>debug_info</c> chunk.</p>
       </desc>
     </datatype>
+    <datatype>
+      <name name="docs"/>
+      <desc>
+        <p>
+          <seeguide marker="kernel:eep48_chapter#the--docs--format">
+          EEP-48 documentation format</seeguide>
+        </p>
+      </desc>
+    </datatype>
     <datatype>
       <name name="forms"/>
     </datatype>
diff --git a/lib/stdlib/src/beam_lib.erl b/lib/stdlib/src/beam_lib.erl
index 31be277643..8ee2a737c7 100644
--- a/lib/stdlib/src/beam_lib.erl
+++ b/lib/stdlib/src/beam_lib.erl
@@ -20,6 +20,8 @@
 -module(beam_lib).
 -behaviour(gen_server).
 
+-include_lib("kernel/include/eep48.hrl").
+
 %% Avoid warning for local function error/1 clashing with autoimported BIF.
 -compile({no_auto_import,[error/1]}).
 %% Avoid warning for local function error/2 clashing with autoimported BIF.
@@ -71,19 +73,21 @@
 -type label()     :: integer().
 
 -type chunkid()   :: nonempty_string(). % approximation of the strings below
-%% "Abst" | "Dbgi" | "Attr" | "CInf" | "ExpT" | "ImpT" | "LocT" | "Atom" | "AtU8".
+%% "Abst" | "Dbgi" | "Attr" | "CInf" | "ExpT" | "ImpT" | "LocT" | "Atom" | "AtU8" | "Docs"
 -type chunkname() :: 'abstract_code' | 'debug_info'
                    | 'attributes' | 'compile_info'
                    | 'exports' | 'labeled_exports'
                    | 'imports' | 'indexed_imports'
                    | 'locals' | 'labeled_locals'
-                   | 'atoms'.
+                   | 'atoms' | 'documentation'.
 -type chunkref()  :: chunkname() | chunkid().
 
 -type attrib_entry()   :: {Attribute :: atom(), [AttributeValue :: term()]}.
 -type compinfo_entry() :: {InfoKey :: atom(), term()}.
 -type labeled_entry()  :: {Function :: atom(), arity(), label()}.
 
+-type docs() :: #docs_v1{}.
+
 -type chunkdata() :: {chunkid(), dataB()}
                    | {'abstract_code', abst_code()}
                    | {'debug_info', debug_info()}
@@ -95,7 +99,8 @@
                    | {'indexed_imports', [{index(), module(), Function :: atom(), arity()}]}
                    | {'locals', [{atom(), arity()}]}
                    | {'labeled_locals', [labeled_entry()]}
-                   | {'atoms', [{integer(), atom()}]}.
+                   | {'atoms', [{integer(), atom()}]}
+                   | {'documentation', docs()}.
 
 %% Error reasons
 -type info_rsn()  :: {'chunk_too_big', file:filename(),
@@ -744,6 +749,18 @@ chunk_to_data(debug_info=Id, Chunk, File, _Cs, AtomTable, Mod) ->
                     {AtomTable, {Id, anno_from_term(Term)}}
 	    end
     end;
+chunk_to_data(documentation=Id, Chunk, File, _Cs, AtomTable, _Mod) ->
+    try
+        case binary_to_term(Chunk) of
+            #docs_v1{} = Term ->
+                {AtomTable, {Id, Term}};
+            _ ->
+                error({invalid_chunk, File, chunk_name_to_id(Id, File)})
+        end
+    catch
+	error:badarg ->
+	    error({invalid_chunk, File, chunk_name_to_id(Id, File)})
+    end;
 chunk_to_data(abstract_code=Id, Chunk, File, _Cs, AtomTable, Mod) ->
     %% Before Erlang/OTP 20.0.
     case Chunk of
@@ -793,6 +810,7 @@ chunk_name_to_id(attributes, _)      -> "Attr";
 chunk_name_to_id(abstract_code, _)   -> "Abst";
 chunk_name_to_id(debug_info, _)      -> "Dbgi";
 chunk_name_to_id(compile_info, _)    -> "CInf";
+chunk_name_to_id(documentation, _)   -> "Docs";
 chunk_name_to_id(Other, File) -> 
     error({unknown_chunk, File, Other}).
 
diff --git a/lib/stdlib/src/epp.erl b/lib/stdlib/src/epp.erl
index 2b531c8869..826a763893 100644
--- a/lib/stdlib/src/epp.erl
+++ b/lib/stdlib/src/epp.erl
@@ -27,9 +27,12 @@
 -export([default_encoding/0, encoding_to_string/1,
          read_encoding_from_binary/1, read_encoding_from_binary/2,
          set_encoding/1, set_encoding/2, read_encoding/1, read_encoding/2]).
+
 -export([interpret_file_attribute/1]).
 -export([normalize_typed_record_fields/1,restore_typed_record_fields/1]).
 
+-include_lib("kernel/include/file.hrl").
+
 %%------------------------------------------------------------------------
 
 -export_type([source_encoding/0]).
@@ -234,6 +237,10 @@ format_error({circular,M,A}) ->
     io_lib:format("circular macro '~ts/~p'", [M,A]);
 format_error({include,W,F}) ->
     io_lib:format("can't find include ~s \"~ts\"", [W,F]);
+format_error({Tag, invalid, Alternative}) when Tag =:= moduledoc; Tag =:= doc ->
+    io_lib:format("invalid ~s tag, only ~s allowed", [Tag, Alternative]);
+format_error({Tag, W, Filename}) when Tag =:= moduledoc; Tag =:= doc ->
+    io_lib:format("can't find ~s ~s \"~ts\"", [Tag, W, Filename]);
 format_error({illegal,How,What}) ->
     io_lib:format("~s '-~s'", [How,What]);
 format_error({illegal_function,Macro}) ->
@@ -932,6 +939,12 @@ scan_toks([{'-',_Lh},{atom,_Ld,warning}=Warn|Toks], From, St) ->
     scan_err_warn(Toks, Warn, From, leave_prefix(St));
 scan_toks([{'-',_Lh},{atom,_Li,include}=Inc|Toks], From, St) ->
     scan_include(Toks, Inc, From, St);
+scan_toks([{'-',_Lh},{atom,_Ld,D}=Doc | [{'(', _},{'{',_} | _] = Toks], From, St)
+  when D =:= doc; D =:= moduledoc ->
+    scan_filedoc(coalesce_strings(Toks), Doc, From, St);
+scan_toks([{'-',_Lh},{atom,_Ld,D}=Doc | [{'{',_} | _] = Toks], From, St)
+  when D =:= doc; D =:= moduledoc ->
+    scan_filedoc(coalesce_strings(Toks), Doc, From, St);
 scan_toks([{'-',_Lh},{atom,_Li,include_lib}=IncLib|Toks], From, St) ->
     scan_include_lib(Toks, IncLib, From, St);
 scan_toks([{'-',_Lh},{atom,_Li,ifdef}=IfDef|Toks], From, St) ->
@@ -978,14 +991,73 @@ scan_toks(Toks0, From, St) ->
 	    wait_req_scan(St)
     end.
 
+%% First we parse either ({file, "filename"}) or {file, "filename"} and
+%% return proper errors if syntax is incorrect. Only literal strings are allowed.
+scan_filedoc([{'(', _},{'{',_}, {atom, _,file},
+              {',', _}, {string, _, _} = DocFilename,
+              {'}', _},{')',_},{dot,_} = Dot], DocType, From, St) ->
+    scan_filedoc_content(DocFilename, Dot, DocType, From, St);
+scan_filedoc([{'(', _},{'{',_}, {atom, _,file} | _] = Toks, DocType, From, St) ->
+    T = find_mismatch(['(','{',atom,',',string,'}',')',dot], Toks, DocType),
+    epp_reply(From, {error,{loc(T),epp,{bad,DocType}}}),
+    wait_req_scan(St);
+scan_filedoc([{'(', _},{'{',_}, T | _], DocType, From, St) ->
+    epp_reply(From, {error,{loc(T),epp,{DocType, invalid, file}}}),
+    wait_req_scan(St);
+scan_filedoc([{'{',_}, {atom, _,file},
+              {',', _}, {string, _, _} = DocFilename,
+              {'}', _},{dot,_} = Dot], DocType, From, St) ->
+    scan_filedoc_content(DocFilename, Dot, DocType, From, St);
+scan_filedoc([{'{',_}, {atom, _,file} | _] = Toks, {atom,_,DocType}, From, St) ->
+    T = find_mismatch(['{',{atom, file},',',string,'}',dot], Toks, DocType),
+    epp_reply(From, {error,{loc(T),epp,{bad,DocType}}}),
+    wait_req_scan(St);
+scan_filedoc([{'{',_}, T | _], {atom,_,DocType}, From, St) ->
+    epp_reply(From, {error,{loc(T),epp,{DocType, invalid, file}}}),
+    wait_req_scan(St).
+
+%% Reads the content of the file and rewrites the AST as if
+%% the content had been written in-place.
+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
+        {ok, NewF, Pname} ->
+            case file:read_file_info(NewF) of
+                {ok, #file_info{ size = Sz }} ->
+                    {ok, Bin} = file:read(NewF, Sz),
+                    ok = file:close(NewF),
+                    StartLoc = start_loc(St#epp.location),
+                    %% Enter a new file for this doc entry
+                    enter_file_reply(From, Pname, erl_anno:new(StartLoc), StartLoc,
+                                     code, St#epp.deterministic),
+                    epp_reply(From, {ok,
+                                     [{'-',StartLoc}, {atom, StartLoc, Doc}]
+                                     ++ [{string, StartLoc, unicode:characters_to_list(Bin)}, {dot,StartLoc}]}),
+                    %% Restore the previous file
+                    enter_file_reply(From, CurrentFilename,
+                                     erl_anno:new(loc(Dot)), loc(Dot), code,
+                                     St#epp.deterministic),
+                    wait_req_scan(St);
+                {error, _} ->
+                    ok = file:close(NewF),
+                    epp_reply(From, {error,{DocLoc,epp,{Doc, file, DocFilename}}}),
+                    wait_req_scan(St)
+            end;
+        {error, _} ->
+            epp_reply(From, {error,{DocLoc,epp,{Doc, file, DocFilename}}}),
+            wait_req_scan(St)
+    end.
+
 %% Determine whether we have passed the prefix where a -feature
 %% directive is allowed.
 in_prefix({atom, _, Atom}) ->
     %% These directives are allowed inside the prefix
     lists:member(Atom, ['module', 'feature',
                         'if', 'else', 'elif', 'endif', 'ifdef', 'ifndef',
-                        'define', 'undef',
-                        'include', 'include_lib']);
+                        'define', 'undef', 'include', 'include_lib',
+                        'moduledoc', 'doc']);
 in_prefix(_T) ->
     false.
 
@@ -1937,6 +2009,8 @@ find_mismatch([var_or_atom|Tags], [{var,_A,_V}=T|Ts], _T0) ->
     find_mismatch(Tags, Ts, T);
 find_mismatch([var_or_atom|Tags], [{atom,_A,_N}=T|Ts], _T0) ->
     find_mismatch(Tags, Ts, T);
+find_mismatch([{Tag,Value}|Tags], [{Tag,_A,Value}=T|Ts], _T0) ->
+    find_mismatch(Tags, Ts, T);
 find_mismatch(_, Ts, T0) ->
     no_match(Ts, T0).
 
diff --git a/lib/stdlib/src/erl_lint.erl b/lib/stdlib/src/erl_lint.erl
index 766aade3a0..4c57859c74 100644
--- a/lib/stdlib/src/erl_lint.erl
+++ b/lib/stdlib/src/erl_lint.erl
@@ -174,7 +174,9 @@ value_option(Flag, Default, On, OnVal, Off, OffVal, Opts) ->
                bvt = none :: 'none' | [any()],  %Variables in binary pattern
                gexpr_context = guard            %Context of guard expression
                    :: gexpr_context(),
-               load_nif=false :: boolean()      %true if calls erlang:load_nif/2
+               load_nif=false :: boolean(),      %true if calls erlang:load_nif/2
+               doc_defined = {false, none} :: {boolean(), term()},
+               moduledoc_defined = {false, none} :: {boolean(), term()}
               }).
 
 -type lint_state() :: #lint{}.
@@ -250,6 +252,8 @@ format_error(multiple_on_loads) ->
     "more than one on_load attribute";
 format_error({bad_on_load_arity,{F,A}}) ->
     io_lib:format("function ~tw/~w has wrong arity (must be 0)", [F,A]);
+format_error({Tag, duplicate_doc_attribute, Ann}) ->
+    io_lib:format("redefining documentation attribute (~p) previously set at line ~p", [Tag, Ann]);
 format_error({undefined_on_load,{F,A}}) ->
     io_lib:format("function ~tw/~w undefined", [F,A]);
 format_error(nif_inline) ->
@@ -889,23 +893,73 @@ attribute_state({attribute,Aa,behaviour,Behaviour}, St) ->
     St#lint{behaviour=St#lint.behaviour ++ [{Aa,Behaviour}]};
 attribute_state({attribute,Aa,behavior,Behaviour}, St) ->
     St#lint{behaviour=St#lint.behaviour ++ [{Aa,Behaviour}]};
-attribute_state({attribute,A,type,{TypeName,TypeDef,Args}}, St) ->
-    type_def(type, A, TypeName, TypeDef, Args, St);
-attribute_state({attribute,A,opaque,{TypeName,TypeDef,Args}}, St) ->
-    type_def(opaque, A, TypeName, TypeDef, Args, St);
+attribute_state({attribute,A,type,{TypeName,TypeDef,Args}}=AST, St) ->
+    St1 = untrack_doc(AST, St),
+    type_def(type, A, TypeName, TypeDef, Args, St1);
+attribute_state({attribute,A,opaque,{TypeName,TypeDef,Args}}=AST, St) ->
+    St1 = untrack_doc(AST, St),
+    type_def(opaque, A, TypeName, TypeDef, Args, St1);
 attribute_state({attribute,A,spec,{Fun,Types}}, St) ->
     spec_decl(A, Fun, Types, St);
-attribute_state({attribute,A,callback,{Fun,Types}}, St) ->
-    callback_decl(A, Fun, Types, St);
+attribute_state({attribute,A,callback,{Fun,Types}}=AST, St) ->
+    St1  =untrack_doc(AST, St),
+    callback_decl(A, Fun, Types, St1);
 attribute_state({attribute,A,optional_callbacks,Es}, St) ->
     optional_callbacks(A, Es, St);
 attribute_state({attribute,A,on_load,Val}, St) ->
     on_load(A, Val, St);
+attribute_state({attribute, _A, DocAttr, Doc}=AST, St)
+  when is_list(Doc) andalso (DocAttr =:= moduledoc orelse DocAttr =:= doc) ->
+    track_doc(AST, St);
 attribute_state({attribute,_A,_Other,_Val}, St) -> % Ignore others
     St;
 attribute_state(Form, St) ->
     function_state(Form, St#lint{state=function}).
 
+
+%% -doc "
+%% Tracks whether we have read a documentation attribute string multiple times.
+%% Terminal elements that reset the state of the documentation attribute tracking
+%% are:
+
+%% - function,
+%% - opaque,
+%% - type
+%% - callback
+
+%% These terminal elements are also the only ones where one should place
+%% documentation attributes.
+%% ".
+track_doc({attribute, A, Tag, Doc}=_AST, #lint{}=St)
+  when is_list(Doc) andalso (Tag =:= moduledoc orelse Tag =:= doc) ->
+    case get_doc_attr(Tag, St) of
+        {true, Ann} -> add_error(A, {Tag, duplicate_doc_attribute, erl_anno:line(Ann)}, St);
+        {false, _} -> update_doc_attr(Tag, A, St)
+    end;
+track_doc(_AST, St) ->
+    St.
+
+%%
+%% Helper functions to track documentation attributes
+%%
+get_doc_attr(moduledoc, #lint{moduledoc_defined = Moduledoc}) -> Moduledoc;
+get_doc_attr(doc, #lint{doc_defined = Doc}) -> Doc.
+
+update_doc_attr(moduledoc, A, #lint{}=St) ->
+    St#lint{moduledoc_defined = {true, A}};
+update_doc_attr(doc, A, #lint{}=St) ->
+    St#lint{doc_defined = {true, A}}.
+
+%% -doc "
+%% Reset the tracking of a documentation attribute.
+
+%% That is, assume that a terminal object was reached, thus we need to reset
+%% the state so that the linter understands that we have not seen any other
+%% documentation attribute.
+%% ".
+untrack_doc(_AST, St) ->
+    St#lint{doc_defined = {false, none}}.
+
 %% function_state(Form, State) ->
 %%      State'
 %%  Allow for record, type and opaque type definitions and spec
@@ -914,18 +968,25 @@ attribute_state(Form, St) ->
 
 function_state({attribute,A,record,{Name,Fields}}, St) ->
     record_def(A, Name, Fields, St);
-function_state({attribute,A,type,{TypeName,TypeDef,Args}}, St) ->
-    type_def(type, A, TypeName, TypeDef, Args, St);
-function_state({attribute,A,opaque,{TypeName,TypeDef,Args}}, St) ->
-    type_def(opaque, A, TypeName, TypeDef, Args, St);
+function_state({attribute,A,type,{TypeName,TypeDef,Args}}=AST, St) ->
+    St1 = untrack_doc(AST, St),
+    type_def(type, A, TypeName, TypeDef, Args, St1);
+function_state({attribute,A,opaque,{TypeName,TypeDef,Args}}=AST, St) ->
+    St1 = untrack_doc(AST, St),
+    type_def(opaque, A, TypeName, TypeDef, Args, St1);
 function_state({attribute,A,spec,{Fun,Types}}, St) ->
     spec_decl(A, Fun, Types, St);
+function_state({attribute,_A,doc,_Val}=AST, St) ->
+    track_doc(AST, St);
+function_state({attribute,_A,moduledoc,_Val}=AST, St) ->
+    track_doc(AST, St);
 function_state({attribute,_A,dialyzer,_Val}, St) ->
     St;
 function_state({attribute,Aa,Attr,_Val}, St) ->
     add_error(Aa, {attribute,Attr}, St);
-function_state({function,Anno,N,A,Cs}, St) ->
-    function(Anno, N, A, Cs, St);
+function_state({function,Anno,N,A,Cs}=AST, St) ->
+    St1 = untrack_doc(AST, St),
+    function(Anno, N, A, Cs, St1);
 function_state({eof,Location}, St) -> eof(Location, St).
 
 %% eof(LastLocation, State) ->
diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl
index 7f21253a55..c1c2ce434e 100644
--- a/lib/stdlib/src/erl_parse.yrl
+++ b/lib/stdlib/src/erl_parse.yrl
@@ -1418,6 +1418,47 @@ build_attribute({atom,Aa,file}, Val) ->
 	    {attribute,Aa,file,{Name,Line}};
         [Other|_] -> error_bad_decl(Other, file)
     end;
+build_attribute({atom,Aa,Attr}, Val) when Attr =:= doc; Attr =:= moduledoc ->
+    case Val of
+        [{atom,_,Value}] when is_boolean(Value) ->
+	    {attribute,Aa,Attr,Value};
+        [{atom,_,hidden=Value}]  ->
+	    {attribute,Aa,Attr,Value};
+	[{string,_,Value}] ->
+	    {attribute,Aa,Attr,Value};
+        [{bin,_, _} = Bin] ->
+            case term(Bin) of
+                Value when is_binary(Value) ->
+                    {attribute,Aa,Attr,Value};
+                _Else ->
+                    error_bad_decl(Bin, doc)
+            end;
+	[{map,_,Pairs} = Expr] ->
+            Value =
+                try
+                    maps:from_list(
+                      lists:map(
+                        fun({map_field_assoc,_,K,V}) ->
+                                case normalise(K) of
+                                    equiv when Attr =:= doc, element(1, V) =:= call ->
+                                        {equiv, V};
+                                    NormalK ->
+                                        {NormalK, normalise(attribute_farity(V))}
+                                end;
+                           (E) ->
+                                throw({badarg, E})
+                        end, Pairs))
+                catch {badarg,E} ->
+                        ret_abstr_err(E, "bad attribute");
+                      _:_ ->
+                        ret_abstr_err(Expr, "bad attribute")
+                end,
+            {attribute,Aa,Attr,Value};
+        [{tuple,_,[{atom,_,file},{string,_,Value}]}] ->
+            {attribute,Aa,Attr,{file,Value}};
+	[Other|_] ->
+            error_bad_decl(Other, doc)
+    end;
 build_attribute({atom,Aa,Attr}, Val) ->
     case Val of
 	[Expr0] ->
diff --git a/lib/stdlib/test/beam_lib_SUITE.erl b/lib/stdlib/test/beam_lib_SUITE.erl
index ef3a615699..11c0253ccb 100644
--- a/lib/stdlib/test/beam_lib_SUITE.erl
+++ b/lib/stdlib/test/beam_lib_SUITE.erl
@@ -453,7 +453,7 @@ strip_add_chunks(Conf) when is_list(Conf) ->
     compare_chunks(B1, NB1, NBId1),
 
     %% Keep all the extra chunks
-    ExtraChunks = ["Abst", "Dbgi", "Attr", "CInf", "LocT", "Atom"],
+    ExtraChunks = ["Abst", "Dbgi", "Attr", "CInf", "Docs", "LocT", "Atom"],
     {ok, {simple, AB1}} = beam_lib:strip(B1, ExtraChunks),
     ABId1 = chunk_ids(AB1),
     true = length(BId1) == length(ABId1),
diff --git a/lib/stdlib/test/epp_SUITE.erl b/lib/stdlib/test/epp_SUITE.erl
index 90b1712d14..b440f83be5 100644
--- a/lib/stdlib/test/epp_SUITE.erl
+++ b/lib/stdlib/test/epp_SUITE.erl
@@ -30,7 +30,8 @@
 	 test_error/1, test_warning/1, otp_14285/1,
 	 test_if/1,source_name/1,otp_16978/1,otp_16824/1,scan_file/1,file_macro/1,
          deterministic_include/1, nondeterministic_include/1,
-         gh_8268/1
+         gh_8268/1,
+         moduledoc_include/1
         ]).
 
 -export([epp_parse_erl_form/2]).
@@ -73,7 +73,8 @@ all() ->
      encoding, extends, function_macro, test_error, test_warning,
      otp_14285, test_if, source_name, otp_16978, otp_16824, scan_file, file_macro,
      deterministic_include, nondeterministic_include,
-     gh_8268].
+     gh_8268,
+     moduledoc_include].
 
 groups() ->
     [{upcase_mac, [], [upcase_mac_1, upcase_mac_2]},
@@ -127,6 +127,48 @@ file_macro(Config) when is_list(Config) ->
     "Other source" = FileA = FileB,
     ok.
 
+moduledoc_include(Config) when is_list(Config) ->
+    PrivDir = proplists:get_value(priv_dir, Config),
+    ModuleFileContent = <<"-module(moduledoc).
+
+                           -moduledoc {file, \"README.md\"}.
+
+                           -export([]).
+                          ">>,
+    DocFileContent = <<"# README
+
+                        This file is a test
+                       ">>,
+    CreateFile = fun (Dir, File, Content) ->
+                     Dirname = filename:join([PrivDir, Dir]),
+                     ok = create_dir(Dirname),
+                     Filename = filename:join([Dirname, File]),
+                     ok = file:write_file(Filename, Content),
+                     Filename
+                 end,
+
+    %% positive test: checks that all works as expected
+    ModuleName = CreateFile("module_attr", "moduledoc.erl", ModuleFileContent),
+    DocName = CreateFile("module_attr", "README.md", DocFileContent),
+    {ok, List} = epp:parse_file(ModuleName, []),
+    {attribute, _, moduledoc, ModuleDoc} = lists:keyfind(moduledoc, 3, List),
+    ?assertEqual({ok, unicode:characters_to_binary(ModuleDoc)}, file:read_file(DocName)),
+
+    %% negative test: checks that we produce an expected error
+    ModuleErrContent = binary:replace(ModuleFileContent, <<"README">>, <<"NotExistingFile">>),
+    ModuleErrName = CreateFile("module_attr", "moduledoc_err.erl", ModuleErrContent),
+    {ok, ListErr} = epp:parse_file(ModuleErrName, []),
+    {error,{_,epp,{moduledoc,file, "NotExistingFile.md"}}} = lists:keyfind(error, 1, ListErr),
+
+    ok.
+
+create_dir(Dir) ->
+    case file:make_dir(Dir) of
+        ok -> ok;
+        {error, eexist} -> ok;
+        _ -> error
+    end.
+
 deterministic_include(Config) when is_list(Config) ->
     DataDir = proplists:get_value(data_dir, Config),
     File = filename:join(DataDir, "deterministic_include.erl"),
@@ -930,11 +972,12 @@ scan_file(Config) when is_list(Config) ->
     [FileForm1, ModuleForm, ExportForm,
      FileForm2, FileForm3, FunctionForm,
      {eof,_}] = Toks,
-    [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm1,
+    [{'-',_}, {atom,_,file}, {'(',_} | _ ]   = FileForm1,
     [{'-',_}, {atom,_,module}, {'(',_} | _ ] = ModuleForm,
     [{'-',_}, {atom,_,export}, {'(',_} | _ ] = ExportForm,
-    [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm2,
-    [{'-',_}, {atom,_,file}, {'(',_} | _ ] = FileForm3,
+    [{'-',_}, {atom,_,file}, {'(',_} | _ ]   = FileForm2,
+    [{'-',_}, {atom,_,file}, {'(',_} | _ ]   = FileForm3,
+    [{atom,_,ok}, {'(',_} | _]               = FunctionForm,
     ok.
 
 macs(Epp) ->
diff --git a/lib/stdlib/test/erl_lint_SUITE.erl b/lib/stdlib/test/erl_lint_SUITE.erl
index 64cf5643f7..c54c1bd00d 100644
--- a/lib/stdlib/test/erl_lint_SUITE.erl
+++ b/lib/stdlib/test/erl_lint_SUITE.erl
@@ -36,6 +36,7 @@
 -export([all/0, suite/0, groups/0]).
 
 -export([singleton_type_var_errors/1,
+         documentation_attributes/1,
          unused_vars_warn_basic/1,
          unused_vars_warn_lc/1,
          unused_vars_warn_rec/1,
@@ -116,6 +117,7 @@ all() ->
      eep49,
      redefined_builtin_type,
      singleton_type_var_errors,
+     documentation_attributes,
      match_float_zero].
 
 groups() -> 
@@ -909,6 +911,108 @@ unused_import(Config) when is_list(Config) ->
     [] = run(Config, Ts),
     ok.
 
+documentation_attributes(Config) when is_list(Config) ->
+    Ts = [{error_moduledoc,
+          <<"-moduledoc \"\"\"
+             Error
+             \"\"\".
+             -import(lists, []).
+
+             -moduledoc \"\"\"
+             Duplicate entry
+             \"\"\".
+             main() -> error.
+            ">>,
+          [],
+          {errors,[{{6,15},erl_lint,{moduledoc,duplicate_doc_attribute,1}}], []}},
+
+
+          {error_doc_import,
+          <<"-doc \"\"\"
+             Error
+             \"\"\".
+             -import(lists, []).
+
+             -doc \"\"\"
+             Duplicate entry
+             \"\"\".
+             main() -> error.
+            ">>,
+          [],
+          {errors,[{{6,15},erl_lint,{doc,duplicate_doc_attribute,1}}], []}},
+
+          {error_doc_export,
+           <<"-doc \"\"\"
+              Error
+              \"\"\".
+              -export([]).
+
+              -doc \"\"\"
+              Duplicate entry
+              \"\"\".
+              main() -> error.
+            ">>,
+          [],
+          {errors,[{{6,16},erl_lint,{doc,duplicate_doc_attribute,1}}], []}},
+
+          {error_doc_export_type,
+           <<"-doc \"\"\"
+              Error
+              \"\"\".
+              -export_type([]).
+
+              -doc \"\"\"
+              Duplicate entry
+              \"\"\".
+              main() -> error.
+            ">>,
+          [],
+          {errors,[{{6,16},erl_lint,{doc,duplicate_doc_attribute,1}}], []}},
+
+          {error_doc_include,
+           <<"-doc \"\"\"
+              Error
+              \"\"\".
+              -include_lib(\"common_test/include/ct.hrl\").
+
+              -doc \"\"\"
+              Duplicate entry
+              \"\"\".
+              main() -> error.
+            ">>,
+          [],
+          {errors,[{{6,16},erl_lint,{doc,duplicate_doc_attribute,1}}], []}},
+
+          {error_doc_behaviour,
+           <<"-doc \"\"\"
+              Error
+              \"\"\".
+              -behaviour(gen_server).
+
+              -doc \"\"\"
+              Duplicate entry
+              \"\"\".
+              main() -> error.
+            ">>,
+          [],
+          {error,[{{6,16},erl_lint,{doc,duplicate_doc_attribute,1}}],
+           [{{4,16},erl_lint,{undefined_behaviour_func,{handle_call,3},gen_server}},
+            {{4,16},erl_lint,{undefined_behaviour_func,{handle_cast,2},gen_server}},
+            {{4,16},erl_lint,{undefined_behaviour_func,{init,1},gen_server}}]}},
+
+          {ok_doc_in_wrong_position,
+           <<"-doc \"\"\"
+              Bad position, that gets attached to function.
+              We do not report this as an error.
+              \"\"\".
+              -include_lib(\"common_test/include/ct.hrl\").
+
+              main() -> ok.
+            ">>, [], []}
+         ],
+    [] = run(Config, Ts),
+    ok.
+
 %% Test singleton type variables
 singleton_type_var_errors(Config) when is_list(Config) ->
     Ts = [{singleton_error1,
diff --git a/make/otp.mk.in b/make/otp.mk.in
index 1ed554ee76..88a7d14b33 100644
--- a/make/otp.mk.in
+++ b/make/otp.mk.in
@@ -104,7 +104,7 @@ endif
 ifdef BOOTSTRAP
   ERL_COMPILE_FLAGS += +slim
 else
-  ERL_COMPILE_FLAGS += +debug_info
+  ERL_COMPILE_FLAGS += +debug_info +no_docs
 endif
 ifeq ($(ERL_DETERMINISTIC),yes)
   ERL_COMPILE_FLAGS += +deterministic
diff --git a/system/doc/reference_manual/Makefile b/system/doc/reference_manual/Makefile
index c51496ca5d..932be45e8d 100644
--- a/system/doc/reference_manual/Makefile
+++ b/system/doc/reference_manual/Makefile
@@ -94,6 +94,9 @@ clean clean_docs:
 	rm -f $(TOP_PDF_FILE) $(TOP_PDF_FILE:%.pdf=%.fo)
 	rm -f errs core *~ 
 
+$(XMLDIR)/%.xml: %.md $(ERL_TOP)/make/emd2exml
+	$(ERL_TOP)/make/emd2exml $< $@
+
 # ----------------------------------------------------
 # Release Target
 # ---------------------------------------------------- 
diff --git a/system/doc/reference_manual/documentation.md b/system/doc/reference_manual/documentation.md
new file mode 100644
index 0000000000..7eb351204e
--- /dev/null
+++ b/system/doc/reference_manual/documentation.md
@@ -0,0 +1,414 @@
+# Documentation
+
+Documentation in Erlang is done through the `-moduledoc` and `-doc` [attributes][]. For example:
+
+    -module(math).
+    -moduledoc """
+    A module for basic arithmetic.
+    """.
+    
+    -export([add/2]).
+    
+    -doc "Adds two numbers together."
+    add(One, Two) -> One + Two.
+
+The `-moduledoc` attribute has to be located before the first `-doc` attribute or
+function declaration. It documents the overall purpose of the module.
+
+The `-doc` attribute always precedes the [function][] or [attribute][attributes] it documents.
+The attributes that can be documented are [user-defined types][] (`-type` and `-opaque`) and
+[behaviour module attributes][] (`-callback`).
+
+By default the format used for documentation attributes is [Markdown][wikipedia]
+but that can be changed by setting [module documentation metadata](#moduledoc-metadata).
+
+A good starting point to writing Markdown is [Basic writing and formatting syntax][github].
+
+For details on what is allowed to be part of the `-moduledoc` and `-doc` attributes, see
+[Documentation Attributes][doc_attrs].
+
+`-doc` attributes have been available since Erlang/OTP 27.
+
+[attributes]: system/reference_manual:modules#module-attributes
+[function]: system/reference_manual:functions
+[user-defined types]: system/reference_manual:typespec#type-declarations-of-user-defined-types
+[behaviour module attributes]: system/reference_manual:modules#behaviour-module-attribute
+[Earmark]: https://github.com/robertdober/earmark_parser
+[wikipedia]: https://en.wikipedia.org/wiki/Markdown
+[github]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax
+[doc_attrs]: system/reference_manual:modules#documentation-attributes
+
+## Documentation metadata
+
+It is possible to add metadata to the documentation entry. You do this by adding
+a `-moduledoc` or `-doc` attribute with a map as argument. For example:
+
+    -module(math).
+    -moduledoc """
+    A module for basic arithmetic.
+    """.
+    -moduledoc #{ since => "1.0" }.
+    
+    -export([add/2]).
+    
+    -doc "Adds two number together."
+    -doc(#{ since => "1.0" }).
+    add(One, Two) -> One + Two.
+
+The metadata is used by documentation tools to provide extra information to
+the user. There can be multiple metadata documentation entries, in which case
+the maps will be merged with the latest taking precedence if there are
+duplicate keys. Example:
+
+    -doc "Adds two number together."
+    -doc #{ since => "1.0", author => "Joe" }.
+    -doc #{ since => "2.0" }.
+    add(One, Two) -> One + Two.
+
+This will result in a metadata entry of `#{ since => "2.0", author => "Joe" }`.
+
+The keys and values in the metadata map can be any type, but it is recommended
+that only [atoms][] are used for keys and [strings][] for the values.
+
+[atoms]: data_types#atom
+[strings]: data_types#string
+
+## External documentation files
+
+The `-moduledoc` and `-doc` can also be placed in external files. To do so use
+`-doc {file, "path/to/doc.md"}` to point to the documentation. The path used is
+relative to the file where the `-doc` attribute is located. For example:
+
+    %% doc/add.md
+    Adds two numbers together
+
+and
+
+    %% src/math.erl
+    -doc({file, "../doc/add.md"}).
+    add(One, Two) -> One + Two.
+
+## Documenting a module
+
+The module description should include details on how to use the API
+and examples of the different functions working together. Here is a
+good place to use images and other diagrams to better show the usage
+of the module. Instead of writing a long text in the `moduledoc`
+attribute, it could be better to break it out into an external page.
+
+The `moduledoc` attribute should start with a short paragraph
+describing the module and then go into greater details. For example:
+
+    -module(math).
+    -moduledoc """
+       A module for basic arithmetic.
+
+       This module can be used to add and subtract values. For example:
+
+       ```
+       1> math:subtract(math:add(2, 3), 1).
+       4
+       ```
+       """.
+
+### Moduledoc metadata
+
+There are three reserved metadata keys for `-moduledoc`:
+
+- `since` - Shows in which version of the application the module was added.
+- `deprecated` - Shows a text in the documentation explaining that it is deprecated
+  and what to use instead.
+- `format` - The format to use for all documentation in this module.
+  The default is `text/markdown`.
+  It should be written using the [mime type][] of the format.
+
+Example:
+
+    -moduledoc {file, "../doc/math.asciidoc"}.
+    -moduledoc #{ since => "0.1", format => "text/asciidoc" }.
+    -moduledoc #{ deprecated => "Use the stdlib math module instead." }.
+
+[mime type]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
+
+## Documenting functions, user-defined types, and callbacks
+
+Functions, types, and callbacks can be documented using the `-doc` attribute.
+Each entry should start with a short paragraph describing the purpose of entity,
+and then go into greater detail in needed.
+
+It is not recommended to include images or diagrams in this documentation as
+it is used by IDEs and [c:h/1][] to show the documentation to the user.
+
+For example:
+
+    -doc """
+    A number that can be used by the math module.
+    
+    We use a special number here so that we know
+    that this number comes from this module.
+    """.
+    -opaque number() :: {math, erlang:number()}.
+    
+    -doc """
+    Adds two number together.
+    
+    ### Example:
+    
+    ```
+    1> math:add(math:number(1), math:number(2)).
+    {number, 3}
+    ```
+    """.
+    -spec add(number(), number()) -> number().
+    add({number, One}, {number, Two}) -> {number, One + Two}.
+
+[c:h/1]: seemfa/stdlib:c#h/1
+
+### Doc metadata
+
+There are four reserved metadata keys for `-doc`:
+
+- `since => unicode:chardata()` - Shows which version of the application the module
+  was added.
+- `deprecated => unicode:chardata()` - Shows a text in the documentation explaining
+  that it is deprecated and what to use instead. The compiler will automatically
+  insert this key if there is a `-deprecated` attribute marking a function as deprecated.
+- `equiv => unicode:chardata()` - Notes that this function is equivalent to another function
+  in this module. The equivalence can be described using either `Func/Arity` or `Func(Args)`.
+  For example:
+
+        -doc #{ equiv => add/3 }.
+        add(One, Two) -> add(One, Two, []).
+        add(One, Two, Options) -> ...
+    
+    or
+    
+        -doc #{ equiv => add(One, Two, []) }.
+        -spec add(One :: number(), Two :: number()) -> number().
+        add(One, Two) -> add(One, Two, []).
+        add(One, Two, Options) -> ...
+    
+    The entry into the [EEP-48][] doc chunk metadata is the value converted to a string.
+
+- `exported => boolean()` - A [boolean/0][] signifying if the entry is `exported`
+  or not. For any `-type` attribute this value is automatically set by the compiler
+  and should not be set by the user.
+
+[boolean/0]: seetype/erts:erlang#boolean
+
+### Doc slogans
+
+The doc slogan is a short text shown to describe the function and its arguments.
+By default it is determined by looking at the names of the arguments in the `-spec` or
+function. For example:
+
+    add(One, Two) -> One + Two.
+    
+    -spec sub(One :: integer(), Two :: integer()) -> integer().
+    sub(X, Y) -> X - Y.
+
+will have a slogan of `add(One, Two)` and `sub(One, Two)`.
+
+For types or callbacks, the slogan is derived from the type or callback specification.
+For example:
+
+    -type number(Value) :: {number, Value}.
+    %% slogan will be `number(Value)`
+    
+    -opaque number() :: {number, number()}.
+    %% slogan will be `number()`
+    
+    -callback increment(In :: number()) -> Out.
+    %% slogan will be `increment(In)`
+    
+    -callback increment(In) -> Out when
+       In :: number().
+    %% slogan will be `increment(In)`
+
+If it is not possible to "easily" figure out a nice slogan from the code, the
+MFA syntax is used instead. For example: `add/2`, `number/1`, `increment/1`
+
+It is possible to supply a custom slogan by placing it as the first line of
+the `-doc` attribute. The provided slogan must be in the form of a function
+declaration up until the `->`. For example:
+
+    -doc """
+    add(One, Two)
+    
+    Adds two numbers.
+    """.
+    add(A, B) -> A + B.
+
+Will create the slogan `add(One, Two)`. The slogan will be removed from the
+documentation string, so in the example above only the text `"Adds two numbers"`
+will be part of the documentation. This works for functions, types, and callbacks.
+
+## Links in Markdown
+
+When writing documentation in Markdown, links are automatically found in any
+inline code segment that looks like an MFA. For example:
+
+    -doc "See `sub/2` for more details".
+
+will create a link to the `sub/2` function in the current module if it exists.
+One can also use `` `sub/2` `` as the link target. For example:
+
+    -doc "See [subtract](`sub/2`) for more details".
+    -doc "See [`sub/2`] for more details".
+    -doc """
+    See [subtract] for more details
+
+    [subtract]: `sub/2`
+    """.
+    -doc """
+    See [subtract][1] for more details
+    
+    [1]: `sub/2`
+    """.
+
+The above examples result in the same link being created.
+
+The link can also other entities:
+
+- `remote functions` - Use `module:function/arity` syntax.
+  
+  Example:
+  
+      -doc "See `math:sub/2` for more details".
+  
+- `modules` - Write the module with a `m` prefix. Use anchors to
+  jump to a specific place in the module.
+  
+  Example:
+  
+      -doc "See `m:math` for more details".
+      -doc "See `m:math#anchor` for more details".
+
+- `types` - Use the same syntax as for local/remote function but add a `t` prefix.
+  
+  Example:
+  
+      -doc "See `t:number/0` for more details".
+      -doc "See `t:math:number/0` for more details".
+
+- `callbacks` - Use the same syntax as for local/remote function but add a `c` prefix.
+  
+  Example:
+  
+      -doc "See `c:increment/0` for more details".
+      -doc "See `c:math:increment/0` for more details".
+
+- `extra pages` - For extra pages in the current application use a normal link,
+  for example "`[release notes](notes.md)`".
+  For extra pages in another application use the `e` prefix and state which
+  application the page belongs to. One can also use anchors to jump to a specific
+  place in the page.
+  
+  Example:
+    
+      -doc "See `e:stdlib:unicode_usage` for more details".
+      -doc "See `e:stdlib:unicode_usage#notes-about-raw-filenames` for more details".
+
+## What is visible versus hidden?
+
+An Erlang [application][] normally consists of various public and private modules. That is,
+modules that should be used by other applications and modules that should not. By default
+all modules in an application are visible, but by setting `-moduledoc false.`
+specific modules can be hidden from being listed as part of the available API.
+
+An Erlang [module][] consists of public and private functions and type attributes.
+By default, all exported functions, exported types and callbacks are considered
+visible and part of the modules public API. In addition, any non-exported
+type that is referred to by any other visible type attribute is also visible,
+but not considered to be part of the public API. For example:
+
+    -export([example/0]).
+    
+    -type private() :: one.
+    -spec example() -> private().
+    example() -> one.
+
+in the above code, the function `example/0` is exported and it referenced the
+un-exported type `private/0`. Therefore both `example/0` and `private/0` will
+be marked as visible. The `private/0` type will have the metadata field `exported`
+set to `false` to show that it is not part of the public API.
+
+If you want to make a visible entity hidden you need to set the `-doc` attribute to
+`false`. Let's revisit out previous example:
+
+    -export([example/0]).
+    
+    -type private() :: one.
+    -spec example() -> private().
+    -doc false.
+    example() -> one.
+
+The function `example/0` is exported but explicitly marked as hidden; therefore
+both `example/0` and `private/0` will be hidden.
+
+Any documentation added to an automatically hidden entity
+(non-exported function or type) is ignored and will generate a
+warning. Such functions can be documented using comments.
+
+[application]: seeerl/kernel:application
+[module]: modules
+
+## Compiling and getting documentation
+
+The Erlang compiler has support for compiling the documentation into [EEP-48][]
+documentation chunks by passing the [beam_docs][] flag to [compile:file/1][], or
+`+beam_docs` to [erlc][].
+
+The documentation can then be retrieved using [code:get_doc/1][], or viewed using the
+shell built-in command [h()][c:h/1]. For example:
+
+    1> h(math).
+    
+          math
+    
+      A module for basic arithmetic.
+    
+    2> h(math, add).
+    
+          add(One, Two)
+    
+      Adds two numbers together.
+
+[EEP-48]: kernel:eep48_chapter
+[compile:file/1]: seemfa/compiler:compile#file/1
+[beam_docs]: seeerl/compiler:compile#beam_docs
+[erlc]: seecom/erts:erlc
+[code:get_doc/1]: seemfa/kernel:code#get_doc/1
+
+## Using ExDoc to generate HTML/ePub documentation
+
+[ExDoc][] has built-in support to generate documentation from Markdown. The simplest
+way to use it is by using the [rebar3_ex_doc][] plugin. To setup a rebar3 project to
+use [ExDoc][] to generate documentation add the following to your `rebar3.config`.
+
+    %% Enable the plugin
+    {plugins, [rebar3_ex_doc]}.
+    
+    %% Configure the compiler to emit documentation
+    {profiles, [{docs, [{erl_opts, [beam_docs]}]}]}.
+    
+    {ex_doc, [
+     {extras, ["README.md"]},
+     {main, "README.md"},
+     {source_url, "https://github.com/namespace/your_app"}
+    ]}.
+
+When configured you can run `rebar3 ex_doc` and the documentation will be generated to
+`doc/index.html`. For more details and options see the [rebar3_ex_doc][] documentation.
+
+You can also download the [release escript bundle][ex_doc_escript] from github and
+run it from the command line. The documentation for using the escript is
+found by running `ex_doc --help`.
+
+If you are writing documentation that will be using [ExDoc][] to generate HTML/ePub
+it is highly recommended to read its documentation.
+
+[ExDoc]: https://hexdocs.pm/ex_doc/
+[rebar3_ex_doc]: https://hexdocs.pm/rebar3_ex_doc
+[ex_doc_escript]: https://github.com/elixir/ex_doc/releases/latest
+[Earmark]: https://hexdocs.pm/earmark_parser
diff --git a/system/doc/reference_manual/modules.xml b/system/doc/reference_manual/modules.xml
index 360345d4ad..955374ecea 100644
--- a/system/doc/reference_manual/modules.xml
+++ b/system/doc/reference_manual/modules.xml
@@ -104,6 +104,18 @@ fact(0) ->           %  |
             functions from. <c>Functions</c> is a list similar as for
             <c>export</c>.</p>
         </item>
+        <tag><c>-moduledoc(Documentation).</c> or <c>-moduledoc Documentation.</c></tag>
+        <item>
+            <p>The user documentation for this module. The allowed values for
+              <c>Documentation</c> are the same as for
+              <seeguide marker="#documentation-attributes"><c>-doc</c></seeguide>.
+            </p>
+            <p>See the
+              <seeguide marker="documentation">Documentation</seeguide>
+              guide in the Erlang Reference Manual for more details about how
+              to use <c>-moduledoc</c>.
+            </p>
+        </item>
         <tag><c>-compile(Options).</c></tag>
         <item>
           <p>Compiler options. <c>Options</c> is a single option
@@ -247,6 +259,54 @@ behaviour_info(callbacks) -> Callbacks.</pre>
 	    which is not to be further updated.
 	</p>
     </section>
+
+    <section>
+        <title>Documentation attributes</title>
+        <p>The module attribute <c>-doc(Documentation)</c> is used to provide user
+          documentation for a function/type/callback: </p>
+        <pre>
+-doc("Example documentation").
+example() -> ok.
+        </pre>
+        <p>The attribute should be placed just before the entity it documents.The parenthesis are
+        optional around <c>Documentation</c>. The allowed values for <c>Documentation</c> are:</p>
+        <taglist>
+            <tag><seeguide marker="data_types#string">literal string</seeguide> or <seeguide marker="expressions#unicode-segments">utf-8 encoded binary string</seeguide></tag>
+            <item><p>The string documenting the entity. Any literal string is allowed,
+              so both <seeguide marker="data_types#tqstring">triple quoted strings</seeguide>
+              and <seeguide marker="data_types#sigil">sigils</seeguide> that translate to literal
+              strings can be used. The following examples are equivalent:</p>
+              <code>
+-doc("Example \"docs\"").
+-doc(&lt;&lt;"Example \"docs\""/utf8>>).
+-doc ~S/Example "docs"/.
+-doc """
+   Example "docs"
+   """
+-doc ~B|Example "docs"|.
+              </code>
+              <p>For clarity it is recommended to use either normal <c>"strings"</c> or
+                triple quoted strings for documentation attributes.</p>
+            </item>
+            <tag><c>{file, </c><seetype marker="kernel:file#filename"><c>file:filename()</c></seetype><c>}</c></tag>
+            <item>Read the contents of filename and use that as the documentation string.</item>
+            <tag><c>false</c></tag>
+            <item>Set the current entity as hidden, that is, it should not be listed as an
+              available function and has no documentation.</item>
+            <tag><c>Metadata :: </c><seetype marker="erts:erlang#map"><c>map()</c></seetype></tag>
+            <item>
+                <p>
+                    Metadata about the current entity. Some of the keys in the
+                    metadata have a special meaning. See <seeguide marker="system/reference_manual:documentation#moduledoc-metadata">Moduledoc metadata</seeguide> and <seeguide marker="system/reference_manual:documentation#doc-metadata">Doc metadata</seeguide> for more details.
+                </p>
+            </item>
+        </taglist>
+        <p>It is possible to have multiple Metadata doc attributes per entity, but only a single
+          documentation string entry is allowed.</p>
+        <p>See the <seeguide marker="documentation">Documentation</seeguide>
+          guide in the Erlang Reference Manual for more details.
+	</p>
+    </section>
   </section>
 
   <section>
@@ -364,12 +424,6 @@ behaviour_info(callbacks) -> Callbacks.</pre>
 	  all NIF functions in the module.</p>
 	  </item>
 
-        <tag><c>native</c></tag>
-	  <item>
-	  <p>Return <c>true</c> if the module has native compiled code.
-          Return <c>false</c> otherwise. In a system compiled without HiPE
-          support, the result is always <c>false</c></p>
-	  </item>
       </taglist>
     </section>
   </section>
diff --git a/system/doc/reference_manual/part.xml b/system/doc/reference_manual/part.xml
index ec2e3e0306..abee9fba9e 100644
--- a/system/doc/reference_manual/part.xml
+++ b/system/doc/reference_manual/part.xml
@@ -34,6 +34,7 @@
   <xi:include href="patterns.xml"/>
   <xi:include href="modules.xml"/>
   <xi:include href="functions.xml"/>
+  <xi:include href="documentation.xml"/>
   <xi:include href="typespec.xml"/>
   <xi:include href="opaques.xml"/>
   <xi:include href="expressions.xml"/>
diff --git a/system/doc/reference_manual/xmlfiles.mk b/system/doc/reference_manual/xmlfiles.mk
index 8e2af09699..740b070ded 100644
--- a/system/doc/reference_manual/xmlfiles.mk
+++ b/system/doc/reference_manual/xmlfiles.mk
@@ -23,6 +23,7 @@ REF_MAN_CHAPTER_FILES = \
 	patterns.xml \
 	modules.xml \
 	functions.xml \
+	documentation.xml \
 	expressions.xml \
 	macros.xml \
 	records.xml \
-- 
2.35.3

openSUSE Build Service is sponsored by