-
Notifications
You must be signed in to change notification settings - Fork 61
Fix multiple object comprehension bugs #358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
k, | ||
new Val.Obj.Member(e.plus, Visibility.Normal) { | ||
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = { | ||
lazy val newScope: ValScope = s.extend(newBindings, self, sup) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two changes here:
- use
self
instead ofnewSelf
, which allows for bindings' references toself
to be properly updated when re-evaluated. - Pass in
sup
to allowsuper
references to resolve properly (in both local variables and in the values of the resulting object).
lazy val newScope: ValScope = s.extend(newBindings, self, sup) | ||
lazy val newBindings = visitBindings(binds, (self, sup) => newScope) | ||
visitExpr(e.value)( | ||
s.extend(newBindings, self, null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On closer look, I see that this is still passing null
for newSuper
here and that probably indicates that there's still a latent bug. Let me see if I can devise a test case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that Edit: now done in b74a493 |
I tested with eval(
"""
|local funcs = {
| [a](x): x * 2
| for a in ["f1", "f2", "f3"]
|};
|funcs.f1(10)
|""".stripMargin) ==> ujson.Num(20) gets:
|
case x => | ||
Error.fail( | ||
s"Field name must be string or null, not ${x.prettyName}", | ||
e.pos |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field name must be string or null, got ${x.prettyName}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied this wording from
sjsonnet/sjsonnet/src/sjsonnet/Evaluator.scala
Lines 683 to 687 in 84ac320
case x => | |
Error.fail( | |
s"Field name must be string or null, not ${x.prettyName}", | |
pos | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I factored this out in bd4e1b2 but, curiously, this appears to cause a test failure in the JDK 17 build: https://github.com/databricks/sjsonnet/actions/runs/14891478275/job/41824335648 :
java.lang.StackOverflowError
java.lang.String.rangeCheck(String.java:304)
java.lang.String.<init>(String.java:300)
jdk.internal.org.objectweb.asm.ClassReader.readUtf(ClassReader.java:3717)
jdk.internal.org.objectweb.asm.ClassReader.readUtf(ClassReader.java:3685)
jdk.internal.org.objectweb.asm.ClassReader.readUTF8(ClassReader.java:3666)
jdk.internal.org.objectweb.asm.ClassReader.readConst(ClassReader.java:3841)
java.lang.invoke.MethodHandles$Lookup$ClassFile.newInstance(MethodHandles.java:2258)
java.lang.invoke.MethodHandles$Lookup.makeHiddenClassDefiner(MethodHandles.java:2359)
java.lang.invoke.MethodHandles$Lookup.defineHiddenClass(MethodHandles.java:2127)
java.lang.invoke.InnerClassLambdaMetafactory.generateInnerClass(InnerClassLambdaMetafactory.java:407)
java.lang.invoke.InnerClassLambdaMetafactory.spinInnerClass(InnerClassLambdaMetafactory.java:315)
java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:228)
java.lang.invoke.LambdaMetafactory.altMetafactory(LambdaMetafactory.java:536)
java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:147)
java.lang.invoke.CallSite.makeSite(CallSite.java:315)
java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:281)
java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:271)
os.Path.relativeTo(Path.scala:584)
sjsonnet.OsPath.relativeToString(OsPath.scala:6)
sjsonnet.Error$Frame.<init>(Error.scala:51)
sjsonnet.Error$.fail(Error.scala:71)
sjsonnet.Materializer.apply0(Materializer.scala:72)
sjsonnet.Materializer.apply0(Materializer.scala:53)
sjsonnet.Materializer.apply0(Materializer.scala:53)
[...]
I think the problem is that we have code within the Materializer
which catches StackOverflowError
and then tries to call Error.fail
and that, in turn, hits a secondary StackOverflow
because the stack is already so deep:
sjsonnet/sjsonnet/src/sjsonnet/Materializer.scala
Lines 70 to 73 in 84ac320
} catch { | |
case _: StackOverflowError => | |
Error.fail("Stackoverflow while materializing, possibly due to recursive value", v.pos) | |
} |
I tried adding an @inline
to the new helper to see if that would somehow hack around this (f5e40c2) but it didn't help.
This preexisting design seems very fragile.
If we want to more fundamentally fix this and remove the core fragility, I think we probably need to allow the StackOverflowError
to bubble higher rather than catching it so deep, but that introduces some new challenges around making sure that we preserve the v
which triggered the error. I'm thinking that we might want to re-throw a wrapped exception containing a pointer to v
and then catch it and transform it to an Error.fail
at a higher level of the callstack (e.g. the outermost apply0
call). That's a larger refactoring / change, though, and I don't want to co-mingle it here.
Given this, I'm going to back out of the error message DRY and will re-attempt it in a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I think this might just be nondeterministically broken because the current reverted commit fails but its checkout tree is identical to a commit which passed two days ago.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this is a test ordering issue: the current test for the stack overflow handling fails if it runs prior to other tests: it was implicitly relying on other tests to perform certain loading and initialization steps.
We should fix this properly (e.g. by hardening the actual implementation, as this is a real bug), but as a temporary "unblock the test flakiness" hack I'm trying to force a particular loading step: 5cd4ed8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@inline
is just a hint; we can't rely on it.
For StackOverflow, I raised #284 once, but it may hurt some performance; there is some TCO in other implementations, eg : google/jsonnet#1142
val by = if (newBindingsF == null) 2 else newBindingsF.length + 2 | ||
val b = Arrays.copyOf(bindings, bindings.length + by) | ||
def extend(newBindings: Array[Lazy], newSelf: Val.Obj, newSuper: Val.Obj): ValScope = { | ||
val b = Arrays.copyOf(bindings, bindings.length + newBindings.length + 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bindings.length and newbindings.length can be extracted to a local variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to leave this as-is for now, given that the other methods (including extendSimple
) have similar repetition of .length
. My understanding is that JIT should already be optimizing this nicely.
new Val.Obj.Member(e.plus, Visibility.Normal) { | ||
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = { | ||
lazy val newScope: ValScope = s.extend(newBindings, self, sup) | ||
lazy val newBindings = visitBindings(binds, newScope) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is a little weird, it references each others.
It would be nice to add some comments here.
That As you noted in #337, the problem is that we're currently discarding any knowledge of RHS args (denoting that it's a function). This turned out to be fairly straightforward to fix: I just needed to modify the parser to create an |
Thanks @JoshRosen |
@JoshRosen Thank you , helps a lot |
This PR fixes multiple bugs in object comprehensions (fixes #357, fixes #331, plus another bug).
Mis-handling of
self
andsuper
in object comprehension locals when inheritedObject comprehensions may define local variables and those variables may reference
self
orsuper
.Consider the reproduction reported in #357:
This is supposed to output
but sjsonnet outputs
This bug occurs because of how bindings were managed: there's some tricky circular reference and variable rebinding logic (which I believe was added to handle references between locals) and that was rebinding the
self
reference to the comprehension result itself: that is wrong because theself
reference may change if an object is inherited or extended.While digging into this, I also discovered a related bug where
super
wasn't being properly re-bound in object comprehension locals or values:was failing with
Silent dropping of fields if key is not a string
The object comprehension
fails in regular jsonnet with an error
but in sjsonnet it was silently returning
{}
. This regression was introduced in #226 by adding a defaultcase _ =>
(presumably to fix a compiler or IDE warning).Support function definition via object comprehensions
This fixes #331 by adding parser support for defining functions using method syntax, e.g.
Fixes
This PR fixes the above bugs by changing logic within
Evaluator.visitObjComp
. It is best reviewed commit-by-commit or using a whitespace-free diff.