Inconsistent visibility of exported members of a macro-generated namespace
#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).
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.
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).
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. :)
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?
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?
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
exportreport 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).
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.