A nested policy notation must reject calls that reach an outer scope from an inner block — how is that enforced?
answer
- hide by family, not by level
- un-implicit, not unreachable
- name the outer node to cross
- every scope type carries the marker
- no checker: disjoint member names
basics
~20 sGroup the notation's scope types into one marked family. Inside a block, only the nearest receiver of that family stays implicitly available; an outer one must be named explicitly. Where the language cannot check that, disjoint member names per level are the fallback.
solid answer
~50 sThe enforced form is a **scope marker**: every scope type in one notation is tagged as belonging to the same family, and the language then refuses an implicit call that would resolve past the nearest receiver carrying that marker. The outer scope is not made unreachable — it is made **un-implicit**. An author who genuinely needs it binds the outer node to a name at the call site and writes through that name, so the crossing is explicit in the source and visible in review. Two details decide whether the discipline works: the marker must cover *every* scope type in the notation, since a level left unmarked stays implicitly reachable, and each notation needs its own family so that unrelated nested contexts are governed independently. Where a language offers no such check, the fallback is design: give each level disjoint member names, so an outward lookup finds nothing to hit.
code
pseudocode · 11 linesscope-marker PolicyNotation
marked-by PolicyNotation type RoleScope { priority, rule(...) }
marked-by PolicyNotation type RuleScope { effect, condition(...) }
role("auditor") { outerRole ->
rule("read-ledger") {
priority = 5 // rejected: outer marked scope not implicit here
outerRole.priority = 5 // allowed: the outer scope is named
}
}go deeper
Know that a well-built nested notation can forbid a line in an inner block from touching an outer level, and that reaching the outer level then means naming it.
State the rule precisely: only the nearest receiver of the marked family is implicit, and the marker must cover every scope type in the notation to leave no hole.
Compare enforcement with the fallbacks — disjoint member names, narrow scope types, explicit targets, close-of-scope validation — and say which prevent the mistake versus report it late.
Own the trade: adding a marker to a shipped notation breaks source that crossed implicitly, so decide the migration and the rule for shared top-level context before mandating it.
## What the marker actually does The default resolution rule is generous: an unqualified name is offered to the nearest receiver and then to each enclosing one in turn. A **scope marker** narrows that rule for a chosen set of types. The set is a *family*: every scope type in one notation carries the same marker. The rule becomes: > Inside a block whose receiver carries marker **M**, only that receiver is implicitly available among the receivers carrying **M**. Any enclosing receiver carrying **M** must be named to be used. Two consequences are worth stating precisely, because both are commonly stated backwards: - The outer scope is **not removed**. It is removed from *implicit* lookup. A handle to it still works. - Receivers *without* the marker are untouched. The marker restricts one family; it is not a general ban on outward resolution. ```pseudocode role("auditor") { outerRole -> rule("read-ledger") { priority = 5 // rejected: the outer marked scope is not implicit here outerRole.priority = 5 // allowed: the outer scope is named } } ``` The first line was the silent bug. Under the marker it is a compile error with a message naming the scope. The second line is the same intent stated on purpose, and a reviewer can see it. ## Getting the family right | Choice | Effect | |---|---| | One marker on every scope type of the notation | Correct: each level hides the levels outside it | | A different marker per nesting level | Useless: levels are in different families, so outer levels stay implicit | | One level left unmarked | A hole: that level remains implicitly reachable from every block inside it | | Two notations that may nest, sharing one marker | The outer notation's scope is hidden inside the inner one's blocks too | The second row is the mistake most teams make first, because "more markers" *sounds* stricter. It is the opposite: hiding only ever applies between receivers in the **same** family, so splitting the family removes the hiding. ## When the language cannot check it Not every language can express this constraint, and the mechanism degrades in a predictable order: 1. **Disjoint member names per level.** If no member name exists at two levels, an outward lookup finds nothing and the misplaced call fails to compile on its own. Cheap and effective, but it is a convention the notation's authors must keep as the surface grows. 2. **Narrow scope types.** A scope type that exposes three members offers three chances to be hit from inside. Fewer, more specific members shrink the target. 3. **Explicit targets.** Hand the node to the block as an ordinary parameter and require every line to name it. The notation loses its brevity and gains complete clarity about which node each line touches. 4. **Validation at the close of the scope.** This catches only the subset that produces an invalid node — a role with a value that a role should not carry at all. It runs later than the mistake, and the error points at the node rather than at the line, so the message is worse. The ordering matters in an interview answer: the first three prevent the mistake; the fourth only reports some of it afterwards. ## What it costs - **Legitimate crossings get noisier.** Reading a namespace or a default set at the top level now requires naming that level. Many notations accept this happily; a few find it so frequent that they expose the shared value as a member of every scope instead. - **The marker is part of the public surface.** Adding it to an existing notation breaks source that relied on the implicit crossing, which is a migration the notation's authors own. - **It does not touch ordering.** The marker decides *which receiver* a call hits. It says nothing about *when* a value is computed, so a block that reads a node it is still filling is unaffected by it. That last point is the one to volunteer: a candidate who offers the marker as the fix for every problem in a nested notation has not separated the two axes — which target, and at what point in the block's execution.
- Why does giving each nesting level its own marker fail to block anything?Because hiding applies only between receivers carrying the same marker. With one marker per level, an inner block's receiver and its enclosing receiver are in different families, so the enclosing one is never hidden and every outward call stays legal. The marker must name the notation, not the level.
- The team wants a value set at the top level to be readable from every inner block. How does that fit with the marker?Two honest options. Bind the outer node to a name at the call site and write through it, which keeps the crossing explicit; or decide the value is genuinely shared context and expose it as a member of every scope type, so no crossing is involved. Weakening the marker to allow one convenience re-opens it for everything.
- Does the marker help with a block that reads the node it is still filling?No. The marker governs which receiver an unqualified name resolves to; the half-built read is about when a statement runs relative to the others in the same block. They are independent axes, and conflating them leads to a notation that is strict about targets and still order-dependent.
Nested rooms with open doorways: from the inner room you can call out to the outer one without moving. A scope marker closes the inner door — you may still address the outer room, but you have to name it first.
saying these in an interview costs you the question
- Says the marker makes the outer scope unreachable, not merely un-implicit
- Gives each nesting level its own marker and calls it stricter
- Believes naming conventions alone stop a cross-scope call
- Assumes every language can reject such a call at compile time
- Offers close-of-scope validation as an equivalent to blocking the call
- Uses the marker to explain an ordering problem inside one block