Conversation
runtime.Fetch was taught in expr-lang#952 to resolve a field on the concrete type held by an embedded interface, but the sibling builtin get() was left on the old standard-promotion-only lookup. get() is documented to differ from runtime.Fetch only in returning nil instead of panicking, so the same field access returned a value through member access (a.field) yet nil through get(a, "field"). Share the traversal by exporting runtime.FetchFromEmbeddedInterfaces and calling it from get()'s struct branch, mirroring Fetch. Adds a regression test under test/issues/952.
sanmaxdev
left a comment
There was a problem hiding this comment.
The shared lookup keeps get() consistent with member access for fields behind embedded interfaces, while preserving nil for missing fields. The focused package tests and full suite pass.
Checked with:
go test ./vm/runtime ./builtin ./test/issues/952go test ./...git diff --check upstream/master...HEAD
|
Thanks for looking at this. I would rather not lean on "the suite passes" from either of us, so here is what can be checked without taking anyone's word, plus the one design question this PR actually raises. State on $ grep -n 'etchFromEmbeddedInterfaces' vm/runtime/runtime.go
92: if result, found := fetchFromEmbeddedInterfaces(v, fieldName); found {
163: func fetchFromEmbeddedInterfaces(v reflect.Value, fieldName string) (any, bool) {
183: if result, found := fetchFromEmbeddedInterfaces(fv, fieldName); found {
$ grep -c 'etchFromEmbeddedInterfaces' builtin/lib.go
0
The part that makes this a bug rather than a design choice is in 619 // Main difference from runtime.Fetch
620 // is that we return `nil` instead of panic.
621 return nil, nilThe file states the invariant, and the file breaks it. The two are supposed to agree on which fields exist and disagree only on what happens when one does not. The reason they drifted is mechanical rather than anyone's oversight: the traversal is unexported, and That export is the real review question, and it is yours to decide rather than mine:
I took the first because it is the smallest diff. If you prefer the third I am happy to redo it that way. Two things I am not claiming: $ gh pr checks 977 --repo expr-lang/expr
no checks reported on the 'fix/get-embedded-interface-field' branchThere is no CI evidence on this branch, and I have no Go toolchain on the machine I am on right now, so I have not re-run the suite against current |
The builtin
get(obj, "field")doesn't resolve fields on the concrete type behind an embedded interface, while member access(...).fielddoes. So for a value whose static type is unknown at compile time,(cond ? x : y).Valuereturns the value butget((cond ? x : y), "Value")returns nil — inconsistent access for the same field.PR #952 added
fetchFromEmbeddedInterfacestoruntime.Fetchbut left its siblingget()behind; #935 had previously kept the two in lockstep. The fix mirrors #952's logic intoget().Verified both ways with
go test: old returns nil, fixed returns the value; a missing field still returns nil (no panic), zero regression across the suite.