What can go wrong when a call inside a configuration block reads the node that same block is still filling?
answer
- the receiver is an accumulator
- a read sees only the lines above
- looks declarative, runs sequentially
- valid tree, wrong value
- derive after the block closes
basics
~20 sThe receiver is a mutable accumulator, not a finished value, so a read mid-block sees only what the statements above it have added. The notation silently acquires ordering rules that its nested shape does not advertise.
solid answer
~50 sWhile a block runs, its receiver is half-built: the statements above the read have run, the ones below have not. A line that computes something from the node — a count of its children, a default derived from what has been declared so far — therefore depends on where it sits, even though the notation looks declarative and readers assume order does not matter. Nothing reports this; the tree is valid and the value is merely wrong. The fixes are about *when*, not about *which receiver*: compute the derived value after the block returns, express it as a function evaluated at the close of the scope, or design the building scope so that it only accepts writes and offers nothing to read. The last of those is the strongest, because it makes the order-dependent line impossible to write.
code
pseudocode · 6 linesrole("auditor") {
rule("a") { effect = ALLOW }
defaultPriority = ruleCount() // answers 1: the rules below have not run
rule("b") { effect = DENY }
rule("c") { effect = DENY }
}go deeper
Remember that a configuration block runs top to bottom like any other code, so a line that reads the thing being configured sees only what the lines above it did.
Explain the accumulator model: the receiver is mutable for the life of the block, which is why a derived value depends on its line's position even though the notation looks declarative.
Diagnose the symptom — a valid tree with a derived value that changes when entries are reordered — and propose deriving after the block returns rather than patching the line's position.
Decide whether the notation exposes a readable building scope at all; forbidding reads removes a class of order-dependence but constrains what authors can express inline.
## Why the node is mutable at all A scope-opening call creates a node, runs the block against it, and attaches the result. For the whole duration of the block, that node is an **accumulator**: members are being set and children are being added, one statement at a time. Nothing about the nesting changes that — depth in the text says where nodes belong, not when statements run. So a line inside the block occupies a position in a sequence, and a line that *reads* the node observes the accumulator in whatever state the lines above it left it. ```pseudocode role("auditor") { rule("a") { effect = ALLOW } defaultPriority = ruleCount() // reads 1: the rules below have not run rule("b") { effect = DENY } rule("c") { effect = DENY } } ``` `ruleCount()` answers **1**, because exactly one rule statement precedes it. Move the line to the bottom and it answers 3; move it to the top and it answers 0. All three versions build a valid role with three rules, and all three set a different default. ## Why this is worse than an ordinary ordering bug - The notation **reads as a declaration**, and readers of declarative-looking text assume the order of independent entries is free. - The mistake produces a **valid tree**. No validation fails, nothing is missing, and a schema check of the result passes. - The line that is wrong is not the line that moved. Adding a fourth rule *above* an existing read changes that read's answer, and the diff that broke it touches neither the read nor the value. - Reformatting, sorting entries alphabetically, or extracting a group of lines into a helper can all change the result. ## The four repairs, strongest first 1. **Give the building scope nothing to read.** If the scope type exposes only writes and child-opening calls, the order-dependent line cannot be written. The accumulator stays internal and the author never sees a half-built value. 2. **Compute after the block returns.** The scope-opening call already has a natural place for this: between the block finishing and the node being attached, the node is complete. Derived defaults belong there. 3. **Store a function, evaluate at the close.** Where the author must express the derivation themselves, let them supply it as a value to be evaluated when the scope closes rather than at the point it is written. The line's position then stops mattering. 4. **Snapshot at the terminal step.** Keep a mutable building type and a separate finished type, and let anything that reads work only on the finished one. This is also what stops a caller from holding the accumulator after the block has closed and mutating a node that is already attached. ## The second failure in the same family A related read is one that looks *sideways* rather than at the node itself: a rule referring to a sibling rule declared later in the same block. Same cause, same absence of a complaint — the sibling does not exist yet, so the reference resolves to nothing or to a stale value. The repair is also the same shape: collect references as unresolved names while the block runs and resolve them once the enclosing scope has closed, which is the point at which every sibling exists. | Symptom | What the author assumed | What was true | |---|---|---| | Derived default is too small | The block's entries are all present | Only the lines above the read had run | | Reference to a later sibling is empty | Nesting implies simultaneity | Statements ran top to bottom | | Result changes after a reformat | Order of independent entries is free | The read made order significant | ## What to say in an interview Name the mechanism first — the receiver is an accumulator, and a read sees a prefix of the block — then separate this axis from the other one explicitly: scope markers and lookup rules decide **which node** a line touches, and none of them affect **when** a line runs. A candidate who reaches for a scope marker here has merged two independent problems. The strongest answer finishes on the design move rather than the workaround: a building scope that cannot be read is a notation in which this bug has no expression at all.
- A rule refers to a sibling rule declared later in the same block. Why does that fail, and what is the repair?The sibling has not been created yet when the reference runs, so it resolves to nothing or to a stale value. The repair is to record the reference as an unresolved name while the block runs and resolve every such name once the enclosing scope has closed, since that is the first moment at which all siblings exist.
- Would a scope-marker discipline prevent the half-built read?No. A marker decides which receiver an unqualified name resolves to, and the half-built read resolves to exactly the right receiver — it just runs too early. These are independent axes, and fixing the target does nothing for the timing. The timing fix is to derive the value after the block returns.
- What stops a caller from keeping the receiver after the block has closed?Separating the building type from the finished one. The block is handed a mutable builder scope; the call converts it into a finished, immutable node before attaching. A reference captured from inside the block then points at a builder that is no longer consulted, rather than at live state inside an attached node.
saying these in an interview costs you the question
- Treats the building scope as the finished, immutable value
- Assumes a nested, declarative-looking notation is order-independent
- Expects a read to see children added later in the same block
- Reorders one line and calls the notation order-independent again
- Reaches for a scope marker to fix a timing problem
- Validates the node's invariants before its block has finished