#[jedem::export] on a bare fn or a mod, not only an impl - #7
Merged
Conversation
zmaril
force-pushed
the
feat/export-bare-fn
branch
from
August 18, 2026 16:37
4845b16 to
e6472f4
Compare
zmaril
force-pushed
the
feat/export-bare-fn
branch
from
August 18, 2026 20:44
e6472f4 to
227d330
Compare
Item 6. A crate exporting free functions had to invent a type for them to hang
off -- `pub struct Jawohl;` in jawohl's case, conveying nothing and existing
only because the macro demanded an impl block. Every consumer was writing one.
The attribute now accepts three forms, and all three capture the same things
because they share one lowering path:
#[jedem::export] pub fn greet(name: &str) -> String { ... }
#[jedem::export] pub mod arithmetic { pub fn add(a: i64, b: i64) -> i64 { ... } }
#[jedem::export] impl Greeter { pub fn greet(name: &str) -> String { ... } }
The design problem was making `surface! { api: [...] }` read the same for all
three. An impl exposes an associated `Type::JEDEM_INTERFACE`, and a first
attempt tried to have `surface!` resolve between differently-named constants --
which Rust macros cannot do, since there is no "try this path, else that one".
The fix uses a property of the language rather than fighting it: Rust keeps
modules and functions in SEPARATE NAMESPACES, so a module named after a function
can carry that function's descriptor without shadowing it. A bare `fn greet` now
emits a hidden `mod greet` holding `JEDEM_INTERFACE`, a `mod` gets the constant
injected into its own body, and an impl keeps its associated constant. All three
are then reached identically as `Path::JEDEM_INTERFACE`, so the surface list is
uniform and no resolution is needed.
The op-lowering logic that was inline in the impl expansion is extracted to a
shared `lower_fn`, so the three forms cannot drift apart in what they capture --
doc comments, name pins, borrowed parameters, inferred fallibility.
Private functions in an exported module are skipped rather than exported, and a
`mod foo;` declaration without a body is a clear error rather than silence.
Six tests, including one asserting all three forms generate correct call paths
in the same surface: a bare fn calls `core::shout`, a mod `core::arithmetic::add`,
a type `core::Hello::greet`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsDxLrGdx6nPaXkVEWkNvS
zmaril
force-pushed
the
feat/export-bare-fn
branch
from
August 18, 2026 20:45
227d330 to
2688c37
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #6. Item 6.
A crate exporting free functions had to invent a type for them to hang off —
pub struct Jawohl;in jawohl's case, conveying nothing and existing onlybecause the macro demanded an
impl. Every consumer was writing one.The interesting part: making
surface!read uniformlyAn
implexposes an associatedType::JEDEM_INTERFACE. A barefnhas no typeto hang one on. My first attempt had
surface!resolve betweendifferently-named constants — which Rust macros cannot do: there is no "try
this path, else that one."
The fix uses a property of the language rather than fighting it. Rust keeps
modules and functions in separate namespaces, so a module named after a
function can carry that function's descriptor without shadowing it:
fn greetemits a hiddenmod greetholdingJEDEM_INTERFACEmodgets the constant injected into its own bodyimplkeeps its associated constantAll three are then reached identically as
Path::JEDEM_INTERFACE, so thesurface list is uniform and no resolution is needed at all.
One lowering path, three forms
The op-lowering that was inline in the impl expansion is extracted to a shared
lower_fn, so the forms cannot drift apart in what they capture — doc comments,name pins, borrowed parameters, inferred fallibility.
Private functions in an exported module are skipped rather than exported, and
mod foo;without a body is a clear error rather than silence.Tests
Six, including one that puts all three forms in a single surface and asserts the
generated call paths: a bare fn calls
core::shout, a modcore::arithmetic::add, a typecore::Hello::greet.Next:
Box<dyn Error>, then#[derive(Enum)].🤖 Generated with Claude Code
https://claude.ai/code/session_01HsDxLrGdx6nPaXkVEWkNvS