Skip to content

Raise on a field of a null object, interpreted and compiled - #42

Open
Tutez64 wants to merge 2 commits into
MeguminBOT:mainfrom
Tutez64:bugfix/null-field-access
Open

Tutez64 wants to merge 2 commits into
MeguminBOT:mainfrom
Tutez64:bugfix/null-field-access

Conversation

@Tutez64

@Tutez64 Tutez64 commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Reading a field of a null object answered null instead of raising, in both modes:

var h:HostBase = null;
h.kept;     // null, interpreted and compiled
h.scaled;   // null, getter never reached
h.kept = 1; // raised interpreted, did nothing compiled
  • Interpreter. resolveField walks a.b.c segment by segment and treats a null got as a path still being resolved. After a variable, a call or a field that held null, the next segment was looked up as the type h.kept, found nothing, and the chain answered null. It now tracks whether got holds a value; a null value followed by a field raises EInvalidAccess, or answers null for ?.. Paths that are still types or packages (haxe.ds.Option.None) are unchanged.
  • cppia. A host field read by name went through Reflect.getProperty, which answers null for a null object, and a write through Reflect.setProperty, which does nothing. Both now go through a small runtime helper, hxscript.runtime.Fields, which raises the interpreter's Invalid access to field <f> before delegating to Reflect. Reads on a live object are unchanged.

Three corpus cases (host field read, host property read, host field write on null). Before this change the read cases answered null in every column. With it, eval-interp, hxcpp-interp, hxcpp-cppia and hxcpp-cppia-jit all answer Invalid access to field kept / scaled. Corpus totals: 348 ok / 5 threw on the hxcpp columns; eval-interp also has the pre-existing killed on "an abstract Map through its alias" (Instance prototype not found: haxe.ds.Map). test/run.sh eval cpp: 421 / 421 / 440 passed. HashLink not run.

🤖 Generated with Claude Code

@MeguminBOT MeguminBOT left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproduced: a read through null answers null in both modes, and a compiled write does
nothing. Raising is what Haxe and hscript do, so the direction is right. The trouble is that
scripts can rely on today's behaviour, most visibly as a null check one level too deep:

if (level.player.sprite != null) ... // level.player is still null here

That runs today and throws with this PR. On eval, h.kept == null, if (h.kept != null),
h.a.b, a null static's field and f().x all go from null to an error. Nothing that works
today may break, so could you put the raise behind a switch that is off by default, a Config flag
like strictAccess or a define, so a host opts in?

Two smaller things while you're there:

  • The error is EInvalidAccess, whose hint says the field "exists but is not visible from a
    script ... A private member is only reachable from inside the type that declares it". For a
    null object that sends people the wrong way; the null case wants its own message.
  • HashLink's compiled code isn't covered: the hl-bytecode column answers 0 for both reads and
    Null access for the write, before and after. Fine to leave for later, but worth saying in the
    description, which says "interpreted and compiled".

Tutez64 and others added 2 commits October 2, 2026 18:21
The interpreter read a segment after a null value as the next part of a type path, found no such type, and answered null. Compiled code read host fields through Reflect.getProperty, which answers null for a null object, and wrote through Reflect.setProperty, which does nothing. Both now raise Invalid access to field, as the interpreter already did for a write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scripts rely on a read through null answering null, often as a check one level too deep, so the raise is off by default and a host opts in. The null case gets its own error, ENullAccess, worded "Null access to field f" with a hint about the null object, instead of EInvalidAccess, whose hint is about private members. The interpreter's existing raises on a call or a write through null use it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Tutez64
Tutez64 force-pushed the bugfix/null-field-access branch from d019982 to 91c0d72 Compare October 2, 2026 16:39
@Tutez64

Tutez64 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Done in 91c0d72 (on top of the original commit, and the branch is rebased on main):

  • The raise is behind Config.strictNullAccess, off by default. Off, reads answer null and compiled writes do nothing, as before. The flag is read at the access in both modes rather than when compiling.
  • The null case has its own ENullAccess: Null access to field f, with a hint about the null object. The interpreter's existing raises on a call or write through null now use it as well, so their message changes, not whether they raise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants