-
Notifications
You must be signed in to change notification settings - Fork 62
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
Changes from 8 commits
05ed13c
280b679
2b76aeb
64edef1
ee94db1
7a281cb
856c193
3239399
1c51f11
55bcb26
fd038d8
b74a493
fd3e9f4
cb5593e
bd4e1b2
f5e40c2
4867920
63cad73
7df617e
e06e7a7
5cd4ed8
0b2fce2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -841,42 +841,40 @@ class Evaluator( | |||||||||||||||||||
| def visitObjComp(e: ObjBody.ObjComp, sup: Val.Obj)(implicit scope: ValScope): Val.Obj = { | ||||||||||||||||||||
| val binds = e.preLocals ++ e.postLocals | ||||||||||||||||||||
| val compScope: ValScope = scope // .clearSuper | ||||||||||||||||||||
|
|
||||||||||||||||||||
| lazy val newSelf: Val.Obj = { | ||||||||||||||||||||
| val builder = new java.util.LinkedHashMap[String, Val.Obj.Member] | ||||||||||||||||||||
| for (s <- visitComp(e.first :: e.rest, Array(compScope))) { | ||||||||||||||||||||
| lazy val newScope: ValScope = s.extend(newBindings, newSelf, null) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| lazy val newBindings = visitBindings(binds, (self, sup) => newScope) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| visitExpr(e.key)(s) match { | ||||||||||||||||||||
| case Val.Str(_, k) => | ||||||||||||||||||||
| val prev_length = builder.size() | ||||||||||||||||||||
| builder.put( | ||||||||||||||||||||
| k, | ||||||||||||||||||||
| new Val.Obj.Member(e.plus, Visibility.Normal) { | ||||||||||||||||||||
| def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = | ||||||||||||||||||||
| visitExpr(e.value)( | ||||||||||||||||||||
| s.extend(newBindings, self, null) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| val builder = new java.util.LinkedHashMap[String, Val.Obj.Member] | ||||||||||||||||||||
| for (s <- visitComp(e.first :: e.rest, Array(compScope))) { | ||||||||||||||||||||
| visitExpr(e.key)(s) match { | ||||||||||||||||||||
| case Val.Str(_, k) => | ||||||||||||||||||||
| val prev_length = builder.size() | ||||||||||||||||||||
| builder.put( | ||||||||||||||||||||
| 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 commentThe reason will be displayed to describe this comment to others. Learn more. There are two changes here:
|
||||||||||||||||||||
| lazy val newBindings = visitBindings(binds, (self, sup) => newScope) | ||||||||||||||||||||
| visitExpr(e.value)( | ||||||||||||||||||||
| s.extend(newBindings, self, null) | ||||||||||||||||||||
|
||||||||||||||||||||
| ) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| if (prev_length == builder.size() && settings.noDuplicateKeysInComprehension) { | ||||||||||||||||||||
| Error.fail(s"Duplicate key ${k} in evaluated object comprehension.", e.pos); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| case Val.Null(_) => // do nothing | ||||||||||||||||||||
| case _ => | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| val valueCache = if (sup == null) { | ||||||||||||||||||||
| Val.Obj.getEmptyValueCacheForObjWithoutSuper(builder.size()) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| new java.util.HashMap[Any, Val]() | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| if (prev_length == builder.size() && settings.noDuplicateKeysInComprehension) { | ||||||||||||||||||||
| Error.fail(s"Duplicate key ${k} in evaluated object comprehension.", e.pos); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| case Val.Null(_) => // do nothing | ||||||||||||||||||||
| case x => | ||||||||||||||||||||
| Error.fail( | ||||||||||||||||||||
| s"Field name must be string or null, not ${x.prettyName}", | ||||||||||||||||||||
| e.pos | ||||||||||||||||||||
|
||||||||||||||||||||
| 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
Uh oh!
There was an error while loading. Please reload this page.