Skip to content

A record reached only through another module's exports has invisible fields #75

Description

@simontreanor

Found while writing an ordinary program with Pyfun 0.5.0.

A record value that reaches a module through another module's exports is
usable but faceless: it can be stored, passed around, and handed back, yet
its fields cannot be accessed unless the module that declared the record is
imported directly. The error gives no hint that an import is the fix.

# inner.pyfun
type Config = { cSize: int }

let make n = Config { cSize = n }
# middle.pyfun
import Inner

type Wrap = { wConf: Inner.Config }

let mk n = Wrap { wConf = Inner.make n }

let size w = w.wConf.cSize
# outer.pyfun
import Middle

let x = Middle.mk 5
print x.wConf.cSize
$ pyfun check outer.pyfun
error: unknown record field `cSize`
 --> 4:7
  |
4 | print x.wConf.cSize
  |       ^^^^^^^^^^^^^

The tell

Everything around the access works. x.wConf resolves — Wrap came with
Middle's exports — and the Config inside it is a perfectly good value:
print (Middle.size x) checks and runs. Only reading a field of it fails,
and import Inner (a module outer.pyfun otherwise has no business naming)
makes the same line check. So the type crossed two module boundaries intact;
its field table crossed only one.

Why it happens

ModuleExports.records carries the records a module declares, not the
records its exports mention — Middle exports Wrap whose field is typed
Inner.Config, but Config's RecordInfo is not in the package. On the
consumer side, decls.records / field_owner are seeded from this module's
declarations plus directly imported modules only (src/types/mod.rs:2355,
and the doc on records at src/types/mod.rs:1789), so when
record_of_field (src/types/mod.rs:7155) looks up cSize it finds zero
owners and reports "unknown record field".

This is the same family as #72 (an extern type cannot be named across a
boundary): ModuleExports carries less than the schemes and records it
exports refer to. The scheme side of this was solved for #26 by
alpha-renaming imported schemes into the consumer's space; the record (and
extern-type) tables never got the equivalent closure.

The natural fix is for a module's exports to include the records (and opaque
types) that its exported schemes and record fields reference, transitively —
Wrap cannot honestly be exported without Config. Short of that, the error
could at least say where the field lives: Middle's interface knows wConf : Inner.Config, so "the record Config is declared in Inner, which is not
imported here" is computable and turns a dead end into a one-line fix.

Where it bit

Super Scrabble's variant work: Rules.View gained a field vRules: Variant.Ruleset, and web.pyfun — which imports Rules and renders from
the View — could not read v.vRules.rRackSize until it imported Variant
directly, though it never names the module for any other reason. A paper-cut
with a one-line workaround, but the diagnosis from "unknown record field" was
all guesswork.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions