skip to content

In a language design where an array of a subtype is an array of its supertype, what must happen on every store?

level: middleimportance: must knowfreq 58%

answer

  1. the compiler gave up something at the call
  2. the array remembers more than the reference
  3. element type fixed at allocation
  4. test each write, not each read
  5. blameless line raises the failure

basics

~20 s

Each store is tested at run time against the element type the array was actually created with, and a value that does not fit is rejected right there, at the write - not at the call that let the widened array through.

solid answer

~40 s

Accepting the widening means the compiler can no longer prove stores are sound, so the guarantee moves to run time. The array carries its own element type, fixed when it was allocated, and every store compares the incoming value against that type rather than against the static element type of the reference used to reach it. A mismatch aborts the store instead of corrupting the buffer. Reads need no such check: every element already satisfies the wider element type, so widening cannot make a read lie. The cost of the bargain is that the error is raised at a store that is locally blameless, while the real mistake - widening a mutable container - happened somewhere else entirely.

code

pseudocode · 9 lines
pseudocode
// buffer records: elementType = PremiumSeat
block = newArray of PremiumSeat, size 100

ref: Array<Seat> = block              // accepted by the widening rule

store ref[7] = makeStandardSeat()
// run time: does StandardSeat fit elementType PremiumSeat?  no -> store refused

value = ref[7]                        // reads are never checked

go deeper

for a junior

Recall that the array itself remembers the element type it was created with, and that writing something outside that type is rejected while the program runs rather than while it compiles.

for a middle

Explain which of the candidate types the check compares against and why the static type of the reference is useless here, plus why reads are exempt.

for a senior

Describe how you diagnose it in production: the stack points at an innocent store, so you work back through who holds the buffer and where it was widened, and you expect the failure to be data-dependent.

for a principal

Weigh the design: a run-time guarantee bought call-site convenience at the price of a build-time one, and decide whether your own container APIs should accept that trade at all.

## Why a run-time check is the only place left Once a language decides that an array of a subtype may be used where an array of its supertype is expected, it has accepted a call the type checker can no longer justify. The routine on the receiving end is typed against the wider element and may legitimately store any value of that element type; the buffer it was handed may be narrower. There is no local information at the store that says otherwise, and the check cannot be moved back to the call because the array may be passed on any number of times before anybody writes. So the guarantee is relocated: the language keeps the buffer honest by testing every write while the program runs. ## What the array carries The key fact is that the array is not just a block of slots - it also carries the **element type it was created with**. That type is decided at allocation and never changes. A widened reference changes what the *compiler* believes about the array; it changes nothing about what the array itself records. ## What the check compares | Candidate | Used by the check? | Why | |---|---|---| | Static element type of the reference | no | that is precisely the type that may have been widened | | Declared parameter type of the storing routine | no | the same widened type, one step earlier | | Element type recorded in the array | yes | fixed at allocation, unaffected by any widening | | Type of whatever is already in the slot | no | slots may be empty, and elements may themselves be subtypes | The test is therefore: *is the value being stored acceptable as the array's own element type?* If yes, the store proceeds; if no, it is refused. ## Where the failure lands, and why that hurts The check makes the program **safe**, not **correct**. Three consequences follow: 1. **The error is raised at a blameless line.** The storing routine did exactly what its signature permitted. The defect is at the site that widened a mutable container, which may be in another module and does not appear in the failure at all. 2. **The failure is data-dependent.** If the widened array is only ever handed values that happen to fit, the program runs for years. The same code fails the first time a genuinely wider value arrives. 3. **The diagnostic must be read backwards.** The useful question is never "why did this store fail?" but "who widened the buffer this reference points at?" - so the investigation starts from who holds a reference to it. ## What the check does not cover - **Reads.** Every element of a narrower array already satisfies the wider element type, so widening cannot make a read return something ill-typed. Checking reads would cost throughput and catch nothing. - **The element's own type arguments.** The check can only test what the array records about its elements. Where a language discards the element's own type arguments before run time, a store check can confirm the outer kind of value and not the arguments inside it. - **The design mistake.** Nothing about the check discourages widening a mutable container; it only converts silent corruption into a loud failure. ## The bargain, stated plainly The design trades a compile-time guarantee for a run-time one in exchange for convenience at call sites - notably, the ability to write one routine over arrays of a general element and pass it anything narrower before the language had a way to write that routine generically. A language that instead treats such containers as usable only at their own element type rejects the call outright, needs no per-store test, and forces the author to either take a read-only view or parameterise the routine over the element type. Both designs are coherent; only one of them can tell you at build time that the store is wrong.

  • Why is there no matching check on reads?
    Because widening only ever loosens the element type. Every element of the narrower array is already acceptable as the wider element, so a read through the widened reference cannot produce a value that fails its static type. There is nothing for a read check to catch, and it would tax the most frequent operation.
  • Why can the failure not point at the line that really caused the problem?
    The causing line is the widening, which the language accepted as legal and left no record of. By the time a store fails, the array may have been passed through several routines. Only the reference chain - who holds this buffer and at which element type - identifies the culprit.
  • What happens to this check when the elements themselves carry type arguments?
    The array can only test what it records about its elements. Where a language keeps element type arguments at run time the test is exact; where it discards them, the store check confirms the outer type of the value and cannot confirm the arguments inside it, so a mismatch there escapes to a later read.

saying these in an interview costs you the question

  • Says the check tests the reference's declared element type
  • Claims both reads and writes are checked
  • Believes the check happens once when the array is passed
  • Says the failing store is the line that contains the bug
  • Thinks the check makes the widening correct, not just safe