Skip to content

#[jedem::export] on a bare fn or a mod, not only an impl - #7

Merged
zmaril merged 1 commit into
mainfrom
feat/export-bare-fn
Aug 18, 2026
Merged

#[jedem::export] on a bare fn or a mod, not only an impl#7
zmaril merged 1 commit into
mainfrom
feat/export-bare-fn

Conversation

@zmaril

@zmaril zmaril commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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 only
because the macro demanded an impl. Every consumer was writing one.

#[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 interesting part: making surface! read uniformly

An impl exposes an associated Type::JEDEM_INTERFACE. A bare fn has no type
to hang one on. My first attempt had surface! resolve between
differently-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:

  • a bare fn greet emits a hidden mod greet holding JEDEM_INTERFACE
  • a mod gets the constant injected into its own body
  • 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 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 mod
core::arithmetic::add, a type core::Hello::greet.

Next: Box<dyn Error>, then #[derive(Enum)].

🤖 Generated with Claude Code

https://claude.ai/code/session_01HsDxLrGdx6nPaXkVEWkNvS

@zmaril
zmaril force-pushed the feat/export-bare-fn branch from 4845b16 to e6472f4 Compare August 18, 2026 16:37
@zmaril
zmaril force-pushed the feat/export-bare-fn branch from e6472f4 to 227d330 Compare August 18, 2026 20:44
Base automatically changed from feat/cargo-jedem-run to main August 18, 2026 20:45
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
zmaril force-pushed the feat/export-bare-fn branch from 227d330 to 2688c37 Compare August 18, 2026 20:45
@zmaril
zmaril merged commit e9f5e42 into main Aug 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant