rhombus-prototype icon indicating copy to clipboard operation
rhombus-prototype copied to clipboard

Inconsistent visibility of exported members of a macro-generated namespace

Open distractedlambda opened this issue 1 month ago • 6 comments

#lang rhombus

module A ~lang rhombus/and_meta:
  export defn.macro 'create_ns $ns_name:
                       $bind: $val
                       ...':
    'namespace $ns_name:
       export def $bind: $val
       ...'

  create_ns X:
    answer: 42

  X.answer // fine

module B ~lang rhombus:
  import parent!A open

  create_ns Y:
    answer: 42

  Y.answer // answer: identifier not provided by Y

I would have expected both Y.answer to be allowed if X.answer is. Interestingly, bringing the export into its own declaration is sufficient to solve the problem (it makes both allowed).

distractedlambda avatar Nov 07 '25 22:11 distractedlambda

This is subtle, but I think it's working as it should.

The subtlety is what export <def> means: it exports only identifiers that have scopes reachable from the export identifier itself. When you use create_ns in the same module, that export can see identifiers defined in the top level of the same module (and, because the export ends up in the generated namespace, within that namespace within the module). When you use create_ns in a different module or in a nested scope like block, it can't.

The problem is solved by using export: $bind; ... separately, because then export doesn't have to see each identifier itself, and it's up to $bind (more precisely: the identifier substituted for $bind) alone. The problem also would be solved by using ~scope_like as in export ~scope_like $bind def $bind: $val.

mflatt avatar Nov 08 '25 13:11 mflatt

Interesting; this seems unfortunate. Unfortunate in that create_ns is written in an "obvious" way and does nothing to opt in to interaction with hygiene (e.g. Syntax.make), but it nonetheless has surprising behavior arising from subtleties of hygiene. And, even if that surprising behavior fits with the current mechanisms of hygiene, it doesn't feel like it fits within its "moral framework" (I would not expect a macro to behave differently when used outside its defining module, unless I was doing something manual and unusual with hygiene in its implementation).

distractedlambda avatar Nov 08 '25 14:11 distractedlambda

Using export before a definition is opting into an interaction with hygiene, because that kind of export has to use a scope to find identifiers that are not explicitly mentioned. It's the same with all_defined. Both of those have a ~scope_like option. Even though ~scope_like is optional, you might use its existence as a reminder that something inherently non-hygienic is going on. :)

mflatt avatar Nov 08 '25 15:11 mflatt

I'm not seeing anything different to do with hygiene-bending forms like a definition-prefixing export. Does it now seem clearer that export is hygiene-bending, and so export-generating macros will inevitably have to take that into account, or do you see a path to a more hygienic export?

mflatt avatar Nov 24 '25 20:11 mflatt

Is there something we could do that would make the definition-prefixing export "break" (i.e. not actually export the binding) in both cases, rather than working in one and failing in another?

Also, would it be possible and reasonable to have the prefix form of export report an error if it would export nothing?

distractedlambda avatar Nov 24 '25 22:11 distractedlambda

Is there something we could do that would make the definition-prefixing export "break" (i.e. not actually export the binding) in both cases, rather than working in one and failing in another?

The export definition form could just always report a syntax error, but I don't think that's what you have in mind. :) Otherwise, I don't see how this points to a way around the inherent hygiene-bending issue.

Also, would it be possible and reasonable to have the prefix form of export report an error if it would export nothing?

Possible, but I am skeptical that it would be a good idea. Having zero definitions be an error seems like it would compose less well. Concretely, create_ns Y: () = values() would be an error even though it explicitly has zero definitions, while create_ns Y:«» would be allowed (unless create_ns goes out of its way to disallow the later).

mflatt avatar Dec 07 '25 14:12 mflatt

Rereading this, I think I'm convinced "that export is hygiene-bending, and so export-generating macros will inevitably have to take that into account". I'm good with closing this issue.

distractedlambda avatar Dec 14 '25 18:12 distractedlambda