From 7b58015f079bd726aad4a1d6263520143d89897a Mon Sep 17 00:00:00 2001 From: happi Date: Tue, 19 May 2026 13:51:14 +0200 Subject: [PATCH 1/3] Add EEP 80 draft for in-app export visibility --- eeps/eep-0080.md | 343 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 eeps/eep-0080.md diff --git a/eeps/eep-0080.md b/eeps/eep-0080.md new file mode 100644 index 0000000..0d3a2aa --- /dev/null +++ b/eeps/eep-0080.md @@ -0,0 +1,343 @@ + Author: Erik Stenman + Status: Draft + Type: Standards Track + Created: 19-May-2026 + Erlang-Version: OTP-30.0 + Post-History: + +**** +EEP XXX: BEAM-Level In-App Export Visibility +---- + +Abstract +======== + +This EEP proposes application-scoped function visibility for Erlang by +introducing two module attributes, `-app(App).` and +`-app_export([Name/Arity, ...]).`, with enforcement in the BEAM runtime. +Functions listed in `-app_export/1` remain ordinary exported functions, but +remote calls to them are accepted only from modules declaring the same +`-app/1` value. + +The feature adds package-private-style visibility without adding new Erlang +syntax and without changing the behavior of existing modules. Code that does +not use the new attributes behaves as it does today. The proposal is intended +to make internal APIs explicit, prevent accidental coupling between OTP +applications or libraries, and give tools a shared source of visibility +metadata. + +Motivation +========== + +Erlang's module boundary is binary: a function is either exported or private +to its defining module. Libraries often need a middle ground where functions +are callable by sibling modules inside the same application but are not part of +the external API. Today that distinction is represented with naming +conventions, documentation, wrapper modules, or static checks. Those +approaches document intent, but they do not provide a uniform runtime boundary. + +A VM-level mechanism gives Erlang code a precise way to say that an exported +function is internal to an application. It prevents accidental dependencies, +improves refactoring safety, exposes intent in BEAM metadata, and gives tools +such as xref, Dialyzer, documentation generators, and language servers a common +representation to inspect. + +Specification +============= + +New Module Attributes +--------------------- + +Two module attributes are introduced: + + -app(App). + +where `App` is an atom naming the application visibility domain of the module, +and: + + -app_export(Functions). + +where `Functions` is a list of function names and arities using the existing +`Name/Arity` attribute syntax, for example: + + -app_export([internal_helper/1, shared_util/2]). + +A module without `-app/1` behaves exactly as it does today. An exported +function not listed in `-app_export/1` behaves exactly as it does today. + +Runtime Enforcement +------------------- + +When an external call targets a function marked by the callee module as +app-restricted, the BEAM runtime checks the caller module's application +visibility domain. + +The call succeeds if the caller module declares the same `-app(App)` value as +the callee module. Otherwise the VM raises an exception of the form: + + error:{badappcall, #{ + caller_mfa => {CallerModule, CallerFunction, CallerArity}, + caller_app => CallerApp, + target_mfa => {TargetModule, TargetFunction, TargetArity}, + target_app => TargetApp + }} + +where `CallerApp` is either an atom or `undefined`, and `TargetApp` is the atom +from the callee module's `-app/1` attribute. + +The check applies to remote calls, `apply/3`, and remote fun invocation. Local +calls inside the defining module are unaffected. BIFs and NIFs are unaffected +unless explicitly represented as app-restricted exports by an implementation. +That use is not recommended. + +Distributed calls are enforced on the callee node using the callee node's code +and metadata. This proposal does not change Erlang distribution trust +semantics. + +BEAM File and Loader Behavior +----------------------------- + +No new BEAM chunk is required. The attributes are stored in existing BEAM +attribute metadata. The loader reads `app` and `app_export` attributes during +module loading. + +For each loaded module, the runtime stores the module's application visibility +domain. For each exported function whose `{Name, Arity}` pair is listed in +`-app_export/1`, the runtime marks the export entry as app-restricted and +associates it with the module's application visibility domain. + +If `-app_export/1` is present without `-app/1`, the functions are treated as +unrestricted at runtime. The compiler should warn for this case. + +Hot code loading naturally follows the active code index. When new code is +loaded, its module metadata determines the visibility behavior for calls into +that code version. + +Compiler and Tooling Behavior +----------------------------- + +The compiler should accept the new attributes and include them in BEAM +attribute metadata. It should warn when `-app_export/1` is present without +`-app/1`, or when `-app_export/1` lists a function that is not exported by the +module. + +`code:module_info(attributes)` should include `app` and `app_export` in the +same way it reports other retained module attributes. + +Static analysis tools may use the attributes to report cross-application calls +to app-restricted functions before runtime. Such checks are advisory; runtime +enforcement is defined by the BEAM. + +Examples +======== + +A module can expose a public function while restricting another exported +function to callers in the same application domain: + + %% foo_a.erl + -module(foo_a). + -app(foo). + -export([open/0, secret/0]). + -app_export([secret/0]). + + open() -> open. + secret() -> top_secret. + +A module with the same application domain can call the restricted function: + + %% foo_b.erl + -module(foo_b). + -app(foo). + -export([try/0]). + + try() -> {ok, foo_a:open(), foo_a:secret()}. + +A module with a different application domain cannot call it: + + %% bar_c.erl + -module(bar_c). + -app(bar). + -export([try/0]). + + try() -> foo_a:secret(). + +The call from `bar_c:try/0` raises `badappcall`. A caller without `-app/1` +would also be rejected when calling `foo_a:secret/0`. + +Rationale +========= + +Attributes are used instead of new syntax because the feature describes module +metadata and function visibility, not a new expression form. This keeps the +language surface stable and reuses BEAM metadata that existing tools already +understand. + +The restriction is enforced by the VM because compile-time checks alone cannot +cover all call paths. Calls through `apply/3`, remote funs, dynamically loaded +modules, and mixed-version systems all need a single runtime rule. + +The rule is application-scoped rather than module-list-scoped. A module list +can express very fine-grained visibility, but it also creates maintenance cost +when internal modules are split, renamed, or reorganized. Application scope is +coarser, but it maps to how many Erlang libraries already distinguish public +API from internal modules. + +The default is unrestricted. Existing code does not opt in accidentally, and +adding only `-app/1` has no behavioral effect. A module author must list a +function in `-app_export/1` to restrict it. + +The proposal deliberately does not define `-app(App)` as identical to an OTP +application name. Using the OTP application name is recommended, but the +attribute defines a visibility domain. This lets the mechanism remain useful +for code that is not packaged as an OTP application while preserving the common +OTP use case. + +Backwards Compatibility +======================= + +Existing code without `-app/1` and `-app_export/1` is unchanged. Adding +`-app/1` alone is also unchanged. + +Adding `-app_export/1` to an existing exported function tightens visibility and +can break callers outside the declared application domain. This is intentional +and should be treated as a public API compatibility change by libraries that +use it. + +Older VMs that do not implement this EEP will ignore the attributes and will +not enforce app-restricted visibility. Code that depends on enforcement must +therefore require a VM version that implements this EEP. + +Security Considerations +======================= + +This feature is an encapsulation mechanism, not a sandbox. A node operator +with shell access, the ability to load arbitrary code, or a modified VM can +bypass it. Erlang distribution trust boundaries are unchanged. + +The error value includes caller and target metadata to make failures +debuggable. Implementations should avoid including more process or code server +state than is needed to explain the rejected call. + +Performance Impact +================== + +Unrestricted exports should pay at most one predictable flag check in external +call paths. Restricted exports require an additional comparison of the caller +and callee application atoms. The allocation of the detailed `badappcall` term +occurs only on the error path. + +Implementations should keep the hot path branch predictable and avoid heap +allocation unless enforcement fails. + +Indicative prototype measurements on x86 JIT, comparing a patched VM against +vanilla upstream OTP at commit `311fecb87f` with `+S 1:1`, showed public calls +within measurement noise and allowed restricted calls adding a few nanoseconds +per operation: + + direct public, same app: 6.89 ns/op -> 7.25 ns/op + direct restricted, same app: 7.02 ns/op -> 12.01 ns/op + apply public, same app: 6.89 ns/op -> 7.21 ns/op + apply restricted, same app: 7.07 ns/op -> 13.35 ns/op + external fun restricted, same app: 6.82 ns/op -> 13.34 ns/op + direct public, cross app: 7.17 ns/op -> 6.81 ns/op + +These figures are non-normative and come from a microbenchmark. The blocked +cross-application path is not comparable to a normal call because it constructs +and catches an exception. + +Implementation Notes +==================== + +A prototype has been used to validate the basic design, but the prototype is +not linked from this initial EEP draft. A clean public implementation fork can +be linked once it is ready for review. + +The implementation touches these areas: + +* the export table, to record app-restricted exports; +* the module data structure, to record the module application atom; +* the BEAM loader, to read `app` and `app_export` attributes; +* external call dispatch, `apply/3`, and remote fun handling, to enforce the + restriction; +* compiler linting, to warn about malformed or ineffective declarations; and +* common test coverage for same-app success, cross-app failure, unrestricted + exports, dynamic calls, and tool-facing metadata. + +A final implementation must include JIT support, reference manual updates, and +complete OTP test coverage before the EEP can become Final. + +Alternatives Considered +======================= + +Parse transforms can reject some invalid calls, but they are opt-in per module +and do not reliably cover `apply/3`, remote funs, generated code, or modules +compiled without the transform. + +Documentation-only mechanisms, including hidden documentation, help consumers +avoid internal functions but do not prevent accidental runtime use. + +Static analysis only, as explored by [EEP 67][], can catch many direct calls, +but it cannot define the behavior of dynamic calls or mixed-code systems by +itself. + +Explicit module allow lists, as explored by [EEP 5][], provide finer control +but require every internal caller to be named. This makes routine +reorganization expensive and can turn visibility metadata into a second module +dependency graph. + +Related Work +============ + +[EEP 5][] proposed `-export_to` for finer-grained visibility. It allowed a +module to specify which other modules could call selected functions. This EEP +uses an application-level visibility domain instead of explicit module lists. + +[EEP 67][] proposed `-internal_export` for marking functions internal to an OTP +application. It focused on static analysis through tools such as xref and +Dialyzer. That EEP was rejected after the OTP team concluded that +documentation attributes and static tooling were sufficient for identifying +internal APIs. This EEP revisits the same broad problem, but proposes runtime +enforcement as the distinguishing semantic change. + +[OTP PR 7407][] implemented EEP 67 and was closed after EEP 67 was rejected. +The discussion is useful prior art for the distinction between metadata-only +visibility and enforced visibility. + +Open Questions +============== + +The proposal needs community feedback on whether the attribute should be named +`-app/1`, given the possible confusion with OTP application metadata, or +whether a more explicit name such as `-visibility_app/1` would be preferable. + +The exact exception shape should also be reviewed. The current proposal uses +`badappcall` with a map containing caller and target metadata. An alternative +is to reuse `undef` or `badarg`, but those errors lose the reason why the call +was rejected. + +A final design must specify the JIT implementation strategy in enough detail to +show that the ordinary external call path remains fast. + +References +========== + +[EEP 5]: eep-0005.md + "EEP 5, More Versatile Encapsulation with export_to, O'Keefe" + +[EEP 67]: eep-0067.md + "EEP 67, Internal exports, Mindek" + +[OTP PR 7407]: https://github.com/erlang/otp/pull/7407 + "Implement EEP 67: Internal exports" + +Copyright +========= + +This document is placed in the public domain or under the CC0-1.0-Universal +license, whichever is more permissive. + +Local Variables: +mode: indented-text +coding: utf-8 +fill-column: 70 +End: From 2ac94b57fd955085a1572b81b95744839aa9fd4d Mon Sep 17 00:00:00 2001 From: happi Date: Tue, 19 May 2026 15:42:17 +0200 Subject: [PATCH 2/3] Link EEP 80 prototype implementation --- eeps/eep-0080.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/eeps/eep-0080.md b/eeps/eep-0080.md index 0d3a2aa..f7738c1 100644 --- a/eeps/eep-0080.md +++ b/eeps/eep-0080.md @@ -248,9 +248,9 @@ and catches an exception. Implementation Notes ==================== -A prototype has been used to validate the basic design, but the prototype is -not linked from this initial EEP draft. A clean public implementation fork can -be linked once it is ready for review. +A prototype implementation is available in the `eep80-poc` branch of +[otp-app-export][]. It is intended to help review the design and VM +implications. It is not yet proposed as a merge-ready Erlang/OTP pull request. The implementation touches these areas: @@ -330,6 +330,9 @@ References [OTP PR 7407]: https://github.com/erlang/otp/pull/7407 "Implement EEP 67: Internal exports" +[otp-app-export]: https://github.com/happi/otp-app-export/tree/eep80-poc + "Prototype implementation" + Copyright ========= From 0b889cafb6a93c4d9d28efe483506b67dbae519c Mon Sep 17 00:00:00 2001 From: happi Date: Tue, 19 May 2026 19:07:12 +0200 Subject: [PATCH 3/3] Rename EEP 80 visibility model to scopes --- eeps/eep-0080.md | 199 +++++++++++++++++++++++++---------------------- 1 file changed, 106 insertions(+), 93 deletions(-) diff --git a/eeps/eep-0080.md b/eeps/eep-0080.md index f7738c1..ece3f35 100644 --- a/eeps/eep-0080.md +++ b/eeps/eep-0080.md @@ -6,41 +6,40 @@ Post-History: **** -EEP XXX: BEAM-Level In-App Export Visibility +EEP XXX: BEAM-Level Scoped Export Visibility ---- Abstract ======== -This EEP proposes application-scoped function visibility for Erlang by -introducing two module attributes, `-app(App).` and -`-app_export([Name/Arity, ...]).`, with enforcement in the BEAM runtime. -Functions listed in `-app_export/1` remain ordinary exported functions, but +This EEP proposes scoped function visibility for Erlang by introducing two +module attributes, `-scope(Scope).` and +`-scope_export([Name/Arity, ...]).`, with enforcement in the BEAM runtime. +Functions listed in `-scope_export/1` remain ordinary exported functions, but remote calls to them are accepted only from modules declaring the same -`-app/1` value. +`-scope/1` value. The feature adds package-private-style visibility without adding new Erlang syntax and without changing the behavior of existing modules. Code that does not use the new attributes behaves as it does today. The proposal is intended -to make internal APIs explicit, prevent accidental coupling between OTP -applications or libraries, and give tools a shared source of visibility -metadata. +to make internal APIs explicit, prevent accidental coupling between libraries +or subsystems, and give tools a shared source of visibility metadata. Motivation ========== Erlang's module boundary is binary: a function is either exported or private to its defining module. Libraries often need a middle ground where functions -are callable by sibling modules inside the same application but are not part of -the external API. Today that distinction is represented with naming +are callable by sibling modules inside the same visibility scope but are not +part of the external API. Today that distinction is represented with naming conventions, documentation, wrapper modules, or static checks. Those approaches document intent, but they do not provide a uniform runtime boundary. A VM-level mechanism gives Erlang code a precise way to say that an exported -function is internal to an application. It prevents accidental dependencies, -improves refactoring safety, exposes intent in BEAM metadata, and gives tools -such as xref, Dialyzer, documentation generators, and language servers a common -representation to inspect. +function is internal to a declared visibility scope. It prevents accidental +dependencies, improves refactoring safety, exposes intent in BEAM metadata, and +gives tools such as xref, Dialyzer, documentation generators, and language +servers a common representation to inspect. Specification ============= @@ -50,44 +49,42 @@ New Module Attributes Two module attributes are introduced: - -app(App). + -scope(Scope). -where `App` is an atom naming the application visibility domain of the module, -and: +where `Scope` is an atom naming the visibility scope of the module, and: - -app_export(Functions). + -scope_export(Functions). where `Functions` is a list of function names and arities using the existing `Name/Arity` attribute syntax, for example: - -app_export([internal_helper/1, shared_util/2]). + -scope_export([internal_helper/1, shared_util/2]). -A module without `-app/1` behaves exactly as it does today. An exported -function not listed in `-app_export/1` behaves exactly as it does today. +A module without `-scope/1` behaves exactly as it does today. An exported +function not listed in `-scope_export/1` behaves exactly as it does today. Runtime Enforcement ------------------- When an external call targets a function marked by the callee module as -app-restricted, the BEAM runtime checks the caller module's application -visibility domain. +scope-restricted, the BEAM runtime checks the caller module's visibility scope. -The call succeeds if the caller module declares the same `-app(App)` value as -the callee module. Otherwise the VM raises an exception of the form: +The call succeeds if the caller module declares the same `-scope(Scope)` value +as the callee module. Otherwise the VM raises an exception of the form: - error:{badappcall, #{ + error:{badscopecall, #{ caller_mfa => {CallerModule, CallerFunction, CallerArity}, - caller_app => CallerApp, + caller_scope => CallerScope, target_mfa => {TargetModule, TargetFunction, TargetArity}, - target_app => TargetApp + target_scope => TargetScope }} -where `CallerApp` is either an atom or `undefined`, and `TargetApp` is the atom -from the callee module's `-app/1` attribute. +where `CallerScope` is either an atom or `undefined`, and `TargetScope` is the +atom from the callee module's `-scope/1` attribute. The check applies to remote calls, `apply/3`, and remote fun invocation. Local calls inside the defining module are unaffected. BIFs and NIFs are unaffected -unless explicitly represented as app-restricted exports by an implementation. +unless explicitly represented as scope-restricted exports by an implementation. That use is not recommended. Distributed calls are enforced on the callee node using the callee node's code @@ -98,16 +95,16 @@ BEAM File and Loader Behavior ----------------------------- No new BEAM chunk is required. The attributes are stored in existing BEAM -attribute metadata. The loader reads `app` and `app_export` attributes during -module loading. +attribute metadata. The loader reads `scope` and `scope_export` attributes +during module loading. -For each loaded module, the runtime stores the module's application visibility -domain. For each exported function whose `{Name, Arity}` pair is listed in -`-app_export/1`, the runtime marks the export entry as app-restricted and -associates it with the module's application visibility domain. +For each loaded module, the runtime stores the module's visibility scope. For +each exported function whose `{Name, Arity}` pair is listed in +`-scope_export/1`, the runtime marks the export entry as scope-restricted and +associates it with the module's visibility scope. -If `-app_export/1` is present without `-app/1`, the functions are treated as -unrestricted at runtime. The compiler should warn for this case. +If `-scope_export/1` is present without `-scope/1`, the functions are treated +as unrestricted at runtime. The compiler should warn for this case. Hot code loading naturally follows the active code index. When new code is loaded, its module metadata determines the visibility behavior for calls into @@ -117,52 +114,52 @@ Compiler and Tooling Behavior ----------------------------- The compiler should accept the new attributes and include them in BEAM -attribute metadata. It should warn when `-app_export/1` is present without -`-app/1`, or when `-app_export/1` lists a function that is not exported by the -module. +attribute metadata. It should warn when `-scope_export/1` is present without +`-scope/1`, or when `-scope_export/1` lists a function that is not exported by +the module. -`code:module_info(attributes)` should include `app` and `app_export` in the +`code:module_info(attributes)` should include `scope` and `scope_export` in the same way it reports other retained module attributes. -Static analysis tools may use the attributes to report cross-application calls -to app-restricted functions before runtime. Such checks are advisory; runtime +Static analysis tools may use the attributes to report cross-scope calls to +scope-restricted functions before runtime. Such checks are advisory; runtime enforcement is defined by the BEAM. Examples ======== A module can expose a public function while restricting another exported -function to callers in the same application domain: +function to callers in the same visibility scope: %% foo_a.erl -module(foo_a). - -app(foo). + -scope(foo). -export([open/0, secret/0]). - -app_export([secret/0]). + -scope_export([secret/0]). open() -> open. secret() -> top_secret. -A module with the same application domain can call the restricted function: +A module with the same visibility scope can call the restricted function: %% foo_b.erl -module(foo_b). - -app(foo). + -scope(foo). -export([try/0]). try() -> {ok, foo_a:open(), foo_a:secret()}. -A module with a different application domain cannot call it: +A module with a different visibility scope cannot call it: %% bar_c.erl -module(bar_c). - -app(bar). + -scope(bar). -export([try/0]). try() -> foo_a:secret(). -The call from `bar_c:try/0` raises `badappcall`. A caller without `-app/1` -would also be rejected when calling `foo_a:secret/0`. +The call from `bar_c:try/0` raises `badscopecall`. A caller without +`-scope/1` would also be rejected when calling `foo_a:secret/0`. Rationale ========= @@ -176,35 +173,35 @@ The restriction is enforced by the VM because compile-time checks alone cannot cover all call paths. Calls through `apply/3`, remote funs, dynamically loaded modules, and mixed-version systems all need a single runtime rule. -The rule is application-scoped rather than module-list-scoped. A module list -can express very fine-grained visibility, but it also creates maintenance cost -when internal modules are split, renamed, or reorganized. Application scope is -coarser, but it maps to how many Erlang libraries already distinguish public +The rule is scope-based rather than module-list-scoped. A module list can +express very fine-grained visibility, but it also creates maintenance cost when +internal modules are split, renamed, or reorganized. A named visibility scope +is coarser, but it maps to how many Erlang libraries already distinguish public API from internal modules. The default is unrestricted. Existing code does not opt in accidentally, and -adding only `-app/1` has no behavioral effect. A module author must list a -function in `-app_export/1` to restrict it. +adding only `-scope/1` has no behavioral effect. A module author must list a +function in `-scope_export/1` to restrict it. -The proposal deliberately does not define `-app(App)` as identical to an OTP -application name. Using the OTP application name is recommended, but the -attribute defines a visibility domain. This lets the mechanism remain useful -for code that is not packaged as an OTP application while preserving the common -OTP use case. +The proposal deliberately does not derive visibility from OTP applications, +`.app` files, source tree layout, or code paths. The attribute defines an +explicit visibility scope. This lets the mechanism remain useful for code that +is not packaged as an OTP application, for generated code, and for other BEAM +languages that may have different packaging conventions. Backwards Compatibility ======================= -Existing code without `-app/1` and `-app_export/1` is unchanged. Adding -`-app/1` alone is also unchanged. +Existing code without `-scope/1` and `-scope_export/1` is unchanged. Adding +`-scope/1` alone is also unchanged. -Adding `-app_export/1` to an existing exported function tightens visibility and -can break callers outside the declared application domain. This is intentional -and should be treated as a public API compatibility change by libraries that -use it. +Adding `-scope_export/1` to an existing exported function tightens visibility +and can break callers outside the declared visibility scope. This is +intentional and should be treated as a public API compatibility change by +libraries that use it. Older VMs that do not implement this EEP will ignore the attributes and will -not enforce app-restricted visibility. Code that depends on enforcement must +not enforce scope-restricted visibility. Code that depends on enforcement must therefore require a VM version that implements this EEP. Security Considerations @@ -223,7 +220,7 @@ Performance Impact Unrestricted exports should pay at most one predictable flag check in external call paths. Restricted exports require an additional comparison of the caller -and callee application atoms. The allocation of the detailed `badappcall` term +and callee scope atoms. The allocation of the detailed `badscopecall` term occurs only on the error path. Implementations should keep the hot path branch predictable and avoid heap @@ -234,16 +231,16 @@ vanilla upstream OTP at commit `311fecb87f` with `+S 1:1`, showed public calls within measurement noise and allowed restricted calls adding a few nanoseconds per operation: - direct public, same app: 6.89 ns/op -> 7.25 ns/op - direct restricted, same app: 7.02 ns/op -> 12.01 ns/op - apply public, same app: 6.89 ns/op -> 7.21 ns/op - apply restricted, same app: 7.07 ns/op -> 13.35 ns/op - external fun restricted, same app: 6.82 ns/op -> 13.34 ns/op - direct public, cross app: 7.17 ns/op -> 6.81 ns/op + direct public, same scope: 6.89 ns/op -> 7.25 ns/op + direct restricted, same scope: 7.02 ns/op -> 12.01 ns/op + apply public, same scope: 6.89 ns/op -> 7.21 ns/op + apply restricted, same scope: 7.07 ns/op -> 13.35 ns/op + external fun restricted, same scope: 6.82 ns/op -> 13.34 ns/op + direct public, cross scope: 7.17 ns/op -> 6.81 ns/op These figures are non-normative and come from a microbenchmark. The blocked -cross-application path is not comparable to a normal call because it constructs -and catches an exception. +cross-scope path is not comparable to a normal call because it constructs and +catches an exception. Implementation Notes ==================== @@ -251,17 +248,20 @@ Implementation Notes A prototype implementation is available in the `eep80-poc` branch of [otp-app-export][]. It is intended to help review the design and VM implications. It is not yet proposed as a merge-ready Erlang/OTP pull request. +The prototype still uses the earlier attribute names `-app/1` and +`-app_export/1`, and the earlier exception name `badappcall`. Those names will +be updated after the proposal terminology settles. The implementation touches these areas: -* the export table, to record app-restricted exports; -* the module data structure, to record the module application atom; -* the BEAM loader, to read `app` and `app_export` attributes; +* the export table, to record scope-restricted exports; +* the module data structure, to record the module visibility scope; +* the BEAM loader, to read `scope` and `scope_export` attributes; * external call dispatch, `apply/3`, and remote fun handling, to enforce the restriction; * compiler linting, to warn about malformed or ineffective declarations; and -* common test coverage for same-app success, cross-app failure, unrestricted - exports, dynamic calls, and tool-facing metadata. +* common test coverage for same-scope success, cross-scope failure, + unrestricted exports, dynamic calls, and tool-facing metadata. A final implementation must include JIT support, reference manual updates, and complete OTP test coverage before the EEP can become Final. @@ -285,12 +285,25 @@ but require every internal caller to be named. This makes routine reorganization expensive and can turn visibility metadata into a second module dependency graph. +A separate "use scope" attribute could distinguish the scope a module belongs +to from additional scopes whose restricted exports it is allowed to call. For +example: + + -scope(cowboy). + -use_scope(cowlib). + +This is more expressive than the single-scope model, but it changes the feature +from membership in one visibility scope to a capability-like system. Such a +system would need to define whether access is granted by the caller, the +callee, or both. This EEP keeps the core rule to one declared scope per module +and leaves friend or use-scope access as a possible extension. + Related Work ============ [EEP 5][] proposed `-export_to` for finer-grained visibility. It allowed a module to specify which other modules could call selected functions. This EEP -uses an application-level visibility domain instead of explicit module lists. +uses a named visibility scope instead of explicit module lists. [EEP 67][] proposed `-internal_export` for marking functions internal to an OTP application. It focused on static analysis through tools such as xref and @@ -307,13 +320,13 @@ Open Questions ============== The proposal needs community feedback on whether the attribute should be named -`-app/1`, given the possible confusion with OTP application metadata, or -whether a more explicit name such as `-visibility_app/1` would be preferable. +`-scope/1`, or whether a more explicit name such as `-visibility_scope/1` would +be preferable. The exact exception shape should also be reviewed. The current proposal uses -`badappcall` with a map containing caller and target metadata. An alternative -is to reuse `undef` or `badarg`, but those errors lose the reason why the call -was rejected. +`badscopecall` with a map containing caller and target metadata. An +alternative is to reuse `undef` or `badarg`, but those errors lose the reason +why the call was rejected. A final design must specify the JIT implementation strategy in enough detail to show that the ordinary external call path remains fast.