In a nested policy notation, why can a call inside an inner block silently configure the enclosing node instead?
answer
- an inner block does not close the outer
- receivers stack; lookup walks outward
- nearest receiver answers first
- no error: the call is legal
- shadowing when both offer the name
basics
~20 sEnclosing receivers stay in scope when an inner block opens, so a name the inner node cannot answer is looked up outward and answered by the enclosing node. The call is legal, nothing is misspelled, and the wrong node is configured.
solid answer
~50 sOpening an inner block does not close the outer one. The blocks nest lexically, so inside a rule block the role node is still an available receiver, sitting one step further out in the lookup chain. When a bare name appears, resolution tries the nearest receiver first and walks outward until something offers that member. If the rule scope has no `priority` but the role scope does, `priority = 5` written inside the rule block compiles and sets the **role's** priority. Nothing here is a mistake the type checker can see: it is a legal member access on a receiver that is genuinely in scope. The symmetric case is worse — when both scopes offer the same name, the inner one shadows the outer and the author gets the opposite of what they believed with no warning at all.
code
pseudocode · 7 linesrole("auditor") { // receiver: the role node
priority = 10 // sets the role's priority
rule("read-ledger") { // receiver: the rule node
effect = ALLOW // sets the rule's effect
priority = 5 // rule has no priority -> sets the ROLE's
}
}go deeper
Remember that opening an inner block does not close the outer one: both nodes are available targets, so a line's indentation is not proof of which node it configures.
Explain the lookup walking outward from the nearest receiver, and distinguish the two outcomes: a leak when only the outer scope has the member, shadowing when both do.
Diagnose it from the symptom — a valid tree with values on the wrong node — and name the change that introduced it, usually a member added to an outer scope or a block moved between levels.
Decide the standard: which scope surfaces may grow, whether crossing must be explicit, and what review rule keeps two independently authored levels from colliding on obvious names.
## The lookup chain, not a single receiver A block evaluated against an implicit receiver does not *replace* the enclosing receiver; it **stacks on top of it**. While the inner block runs, at least two nodes are available as targets for an unqualified name: the node the inner call created, and the node the enclosing block is configuring. In a three-level notation there are three, and so on outward. Name resolution then does the obvious thing: it tries the nearest receiver, and if that receiver has no member of that name it keeps looking outward. Two distinct outcomes follow, and it is worth keeping them apart: - **Outward leak.** The inner scope has no such member; the outer one does; the call lands on the outer node. - **Shadowing.** Both scopes have the member; the nearer one wins and the outer one is unreachable without naming it. ```pseudocode role("auditor") { priority = 10 rule("read-ledger") { effect = ALLOW priority = 5 // rule scope has no priority -> sets the ROLE's } } ``` The author read the nesting as containment and expected the last line to be about the rule. Resolution read the nesting as an enclosing scope and answered from the role. ## Why nothing complains | What a reviewer expects to catch it | Why it does not | |---|---| | The compiler or type checker | The call is a well-typed member access on a receiver that is in scope | | A spell checker or linter on names | The name is spelled correctly — it just belongs to the other scope | | Tests of the notation itself | The notation builds a valid tree; only the *values* are on the wrong node | | A reviewer reading the file | The line is indented under the rule, which is exactly the misleading signal | That combination — legal, well-typed, correctly spelled, visually plausible — is why this is the failure this construct is known for, and why interviewers ask about it rather than about how the nesting works. ## How it shows up in practice 1. A member is added to an **outer** scope type long after the notation shipped. Every inner block in every file suddenly gains a new name it can reach outward to. Nothing that compiled stops compiling; new authoring mistakes simply become possible. 2. Two scope types are written by different people and independently choose an obvious name — `enabled`, `priority`, `description`, `timeout`. Now shadowing is in play at every nesting site. 3. A block is copied from one level to another during a refactor. Lines that resolved to the node they were written under now resolve outward, and the tree is quietly built wrong. ## What actually helps - **Disjoint member names per level.** If no name exists at two levels, an outward lookup finds nothing and the mistake becomes a compile error rather than a wrong value. This is discipline, not enforcement, and it decays as the notation grows. - **Distinct scope types with narrow surfaces.** The fewer members a scope type has, the less there is to reach. - **A scope-marker discipline the language can check**, where the receiver types are grouped into a family and only the nearest member of that family is implicitly available. That is the enforced form of the same intent. - **Naming the outer scope explicitly** where reaching it is genuinely wanted: give the outer block's node a name at the call site and write it, so the crossing is visible in review. ## The direction to state carefully It is easy to say this backwards. The default is **not** that an inner block hides the outer scope; the default is that the outer scope remains available and is consulted when the inner one has no answer. Hiding is something a notation must *opt into*. Equally, when both scopes offer a name, the nearer one is the one that wins in notations that resolve against the nearest receiver first — the outer one does not take precedence for having been introduced earlier. A candidate who inverts either of those two statements has the model exactly wrong, and will design the notation's guard rails in the wrong direction as a result.
- Two enclosing scopes both expose a member of the same name. Which one does an unqualified write hit, and what can the author do about it?In a notation that resolves against the nearest receiver first, the inner scope shadows the outer one and wins silently. The author's options are to name the outer node explicitly at the call site and write through that name, to rename one of the two members, or to adopt a scope-marker discipline that refuses the implicit outer access altogether.
- Why doesn't adding a member to an outer scope type break any existing file?Because it only widens what an inner block can reach outward. Every line that resolved before still resolves the same way, so nothing that compiled stops compiling. What changes is the set of new mistakes that are now expressible, which is why widening an outer scope's surface deserves the same review as changing the notation itself.
- Is the outward lookup ever the behaviour you want?Yes — it is how a nested block reads shared context from the level above, such as a namespace or a default set once at the top. The problem is not that outward access exists but that it is implicit, so wanted and unwanted crossings look identical in the source. Making the wanted ones explicit is the usual fix.
saying these in an interview costs you the question
- Assumes a call inside a block can only hit the nearest receiver
- Blames a typo, when both names exist and the call is legal
- Says the type checker will always catch a misplaced call
- Thinks the outer node leaves scope once an inner block opens
- Claims the outer member wins when two scopes share a name
- Believes the inner node is not yet built when its block runs