Visibility — scopes, private, hidden
Level:
nelumbo.lang. Available afterimport nelumbo.lang, or transitively throughnelumbo.logicor any package.
Nelumbo has two mechanisms for controlling what names are visible where: scope blocks using { } and visibility modifiers (private, hidden, visible). This page describes both.
Scope blocks — { }
A block introduces a lexical scope. Any declaration inside the block — imports, types, patterns, variables, rules, facts, tests — is local to the block. Once the closing } is reached, those names are no longer in scope.
{
Aa :: Object
private Aa ::= XXX
Aa x
x = XXX ? [(x=XXX)][..]
}
{
Bb :: Object
private Bb ::= XXX
Bb x
x = XXX ? [(x=XXX)][..]
}
The two blocks above (from scoping.nl) each define a type, a literal, a variable, and a test. None of them collide, even though both use the name XXX and both use x. The scopes are independent; the private modifier on each ::= keeps the XXX literal local to its block.
What scopes are good for
- Isolating a DSL example in a file that already imports common modules
- Wrapping a pattern transformation expansion (see
../guides/language-transformations.md— the::> { ... }block is a scope) - Making two versions of the same construct coexist in a single file for comparison
Custom scope types
Any type that specialises Namespace introduces a scope of its own — no explicit {Namespace} prefix is needed (and none is accepted). Declaring a functor private within such a type confines it to that namespace, exactly as private does inside a { ... } block.
{
Aa :: Object
private Aa ::= XXX
Aa x
x = XXX ? [(x=XXX)][..]
}
From scoping.nl. The private keeps XXX local to the block; outside the block the literal does not exist.
Visibility modifiers
Two modifiers change what a declaration does beyond its scope:
| Modifier | Effect |
|---|---|
| (none) — the default | Exported from the module; imports see it |
private |
Visible only within the module (or scope) where it is declared |
hidden |
On a variable: filled in where the source leaves an operand out — see below |
<hidden T> and <visible T> holes in a pattern say whether that operand may be left out; see the section on hidden below.
private — module-internal
A private declaration is not exported. It is a detail of the module's implementation.
private Boolean ::= eq(Literal, Literal) @org.modelingvalue.nelumbo.logic.Equal
From logic.nl. The eq predicate is the native literal-equality primitive; it is used by the rule l1 = l2 <=> eq(l1, l2) inside logic.nl but is not something callers should reach for directly — they should use =. (The general-case = operator on Object is bound to a separate, public native NIs.)
Other private patterns in the stdlib include add, mult, string_concat, string_length, integer_string. All of them are native-backed primitives that the module wraps in friendlier operators.
hidden — implicit operands
A hidden variable is filled in by the parser where the source leaves an operand out. When an expression starts with an operator that needs a left operand, the parser uses the hidden variable in scope as that operand. The hidden.nl example starts with the simplest case:
hidden Integer a
>10 & <20 & =15 ? [(a=15)][(a=10),(a=20),..]
>10 & <20 & =15 reads as a>10 & a<20 & a=15. This works for any operator with a left operand, also inside a rule body, where *100+y reads as x*100+y (see below).
- A hidden variable is declared like any variable, with
hiddenin front:hidden Integer a. - One hidden variable per scope. A second one in the same scope, even of another type, is an error. A hidden variable of an outer scope is also used inside nested
{ }blocks, so give each use its own block, ashidden.nldoes. - The result names the hidden variable's value without its name: the test above prints
a>10&a<20&a=15 ? [(15)][(10),(20),..]. In an expected result, still write the name ((a=15)). A bare value is only accepted for a function query, whose result variable is a hidden variable the query introduces itself. - A printed node shows the variable the parser filled in (
a>10).
<hidden T> and <visible T> holes
A pattern can say whether its left operand may be implicit:
| Hole | Accepts |
|---|---|
<T> |
a written operand, or the hidden variable when the operand is left out |
<hidden T> |
only the hidden variable, filled in because the operand is left out |
<visible T> |
only a written operand |
<visible T> still accepts the hidden variable when you write its name. That lets one operator have a shorthand with a meaning of its own:
Integer ::= hidden Integer $$ Integer #35
Integer ::= visible Integer $ Integer #35
hidden Integer x
Integer y
x$y == *100+y
$$y == x$y
$$11=2211 ? [(x=22)][..]
x$11=2211 ? [(x=22)][..]
x$y is x*100+y, with *100+y using the implicit x. $$y exists only in the shorthand form and means x$y. The other two combinations are syntax errors: x$$11 ($$ takes no written operand) and $11 ($ needs one).
Implicit objects
A hidden hole in front of a name gives a name that reads as a property of an implicit object:
Test :: Object
String ::= Test.name #35,
hidden Testname #35
Test ::= T1, T2
fact T1.name = "T1", T2.name = "T2"
hidden Test t
name == t.name
name="T1" ? [(t=T1),..][..]
name on its own is the name of the hidden t, so name="T1" asks which Test is called "T1". transformation.nl uses this for derived attributes. Its attr transformation adds <hidden OT>AN next to <OT>.AN, so a rule about one object can name that object's other attributes without naming the object:
hidden Person p
address := street + " " + str(number) if number >= 100
Here address, street and number are all attributes of the same implicit p.
Interaction with imports
Imports honour visibility. A module sees:
- Every non-
private, non-hiddendeclaration in the imported module - Nothing else
Transitive imports behave transitively: if module A imports B, and B imports C, then A sees C's exported declarations (via B re-exporting its imports). The standard library relies on this: nelumbo.integers imports nelumbo.logic, so any module that imports integers also has access to the logic primitives without an explicit import.
Interaction with scopes
Visibility modifiers apply relative to the declaration's scope. A private pattern inside a { } block is private to that block; outside, it does not exist at all (because of the scope), so the modifier is equivalent to plain scoping in that case.
private at module top level is what matters most — that is the module's boundary between implementation and API.
See also
grammar.md— where these declarations fit syntacticallyhidden.nl— implicit operands, hidden/visible holes, implicit objectstransformation.nl— implicit objects in derived attributesscoping.nl— working scope example