cue
cue copied to clipboard
cmd/cue: stack overflow with let in _tool.cue file
What version of CUE are you using (cue version)?
$ cue version
cue version v0.0.0-20250207154230-e6fec6be488b
go version go1.23.5
-buildmode exe
-compiler gc
DefaultGODEBUG asynctimerchan=1,gotypesalias=0,httpservecontentkeepheaders=1,tls3des=1,tlskyber=0,x509keypairleaf=0,x509negativeserial=1
CGO_ENABLED 1
GOARCH arm64
GOOS linux
GOARM64 v8.0
vcs git
vcs.revision e6fec6be488b0f3aa12634ec9176eca9c4bf4aea
vcs.time 2025-02-07T15:42:30Z
vcs.modified false
cue.lang.version v0.13.0
Does this issue reproduce with the latest release?
Yes
What did you do?
Carved out from https://github.com/cue-lang/cue/issues/3736.
Note that this issue exists solely to record a stack overflow in evalv2 (at least we haven't yet seen it with evalv3). Hence the reason the test below appears to be written back-to-front, with evalv3 before evalv2.
However, and this is an important caveat, it might be that the expectation that cue cmd monthly fails with evalv3 is wrong. https://github.com/cue-lang/cue/issues/3737 captures an issue which, if confirmed as a bug and fixed, might mean the expectation for evalv3 below needs to be inverted. However, because of that issue affecting evalv3, and the stack overflow affect evalv2, we can't do much else (for now) other than assert that both should fail and then simply assert that don't get a stack overflow.
# -- evalv3 --
env CUE_EXPERIMENT=evalv3=1
! exec cue cmd monthly
! stderr 'fatal error: stack overflow'
# -- evalv2 --
env CUE_EXPERIMENT=evalv3=0
! exec cue cmd monthly
! stderr 'fatal error: stack overflow'
-- test.cue --
package test
import (
"list"
)
income: #Income & {
baseSalary: #PeriodicSummary & {
monthly: 1000.0
}
tax: rulings: "30%": true
}
#TaxBracket: {
lowerThreshold: float | *0
upperThreshold: float | *1_000_000_000_000_000 // One trillion should do it
rate: float & >=0 & <=1
taxableAmount: float & >=0
tax: taxableAmount * rate
}
#PeriodicSummary: {
annual: float & monthly*12
monthly: float & annual/12
}
let taxBrackets = [...#TaxBracket] & [
{rate: 0.3582},
{lowerThreshold: 38441.0, rate: 0.3748},
{lowerThreshold: 76817.0, rate: 0.4950},
]
#Income: {
baseSalary: #PeriodicSummary
allowances: [string]: #PeriodicSummary // Taxed
reimbursements: [string]: #PeriodicSummary // Untaxed
salary: #PeriodicSummary
salary: monthly: baseSalary.monthly + list.Sum([for allowance in allowances {allowance.monthly}])
taxableSalary: #PeriodicSummary | *salary
if tax.rulings["30%"] {
taxableSalary: monthly: salary.monthly * 0.7
}
netSalary: #PeriodicSummary & {
annual: salary.annual - tax.total.annual + list.Sum([for reimbursement in reimbursements {reimbursement.annual}])
}
tax: {
rulings: {
"30%": bool | *false
}
brackets: [...#TaxBracket] & [for i, bracket in taxBrackets {
lowerThreshold: bracket.lowerThreshold
if brackets[i+1] != _|_ {upperThreshold: brackets[i+1].lowerThreshold}
rate: bracket.rate
}]
brackets: [for i, bracket in brackets {
taxableAmount: list.Max([
0,
list.Min([taxableSalary.annual, bracket.upperThreshold]) - bracket.lowerThreshold,
]) * 1.0
}]
total: #PeriodicSummary & {
annual: list.Sum([for bracket in brackets {bracket.tax}]) * 1.0
}
}
}
-- test_tool.cue --
package test
import (
"tool/cli"
"text/tabwriter"
)
let incomeData = income
command: monthly: {
print: cli.Print & {
text: tabwriter.Write([
"BASE SALARY \tGROSS SALARY \tTOTAL TAX \tNET SALARY",
"\(incomeData.baseSalary.monthly)\t\(incomeData.salary.monthly) \t\(incomeData.tax.total.monthly) \t\(incomeData.netSalary.monthly)",
])
}
}
command: annual: {
print: cli.Print & {
text: tabwriter.Write([
"BASE SALARY \tGROSS SALARY \tTOTAL TAX \tNET SALARY",
"\(incomeData.baseSalary.annual)\t\(incomeData.salary.annual) \t\(incomeData.tax.total.annual) \t\(incomeData.netSalary.annual)",
])
}
}
What did you expect to see?
Passing test.
What did you see instead?
# -- evalv3 -- (0.043s)
# -- evalv2 -- (0.628s)
> env CUE_EXPERIMENT=evalv3=0
> ! exec cue cmd monthly
[stderr]
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0x4020752440 stack=[0x4020752000, 0x4040752000]
fatal error: stack overflow
runtime stack:
runtime.throw({0xb77b4a?, 0x200000008?})
/home/myitcv/gos/src/runtime/panic.go:1067 +0x38 fp=0xffff337ae6c0 sp=0xffff337ae690 pc=0x86b38
runtime.newstack()
/home/myitcv/gos/src/runtime/stack.go:1117 +0x460 fp=0xffff337ae800 sp=0xffff337ae6c0 pc=0x691d0
runtime.morestack()
/home/myitcv/gos/src/runtime/asm_arm64.s:342 +0x70 fp=0xffff337ae800 sp=0xffff337ae800 pc=0x8d410
goroutine 1 gp=0x40000021c0 m=10 mp=0x4000077c08 [running]:
cuelang.org/go/internal/core/adt.(*OpContext).unify(0x400018dc20?, 0x40004d74a0?, 0x7fff0102?)
/home/myitcv/dev/cuelang/cue/internal/core/adt/eval.go:157 +0xbe8 fp=0x4020752440 sp=0x4020752440 pc=0x300038
cuelang.org/go/internal/core/adt.(*OpContext).relNode(0x400018dc20, 0x0?)
/home/myitcv/dev/cuelang/cue/internal/core/adt/context.go:245 +0x48 fp=0x4020752470 sp=0x4020752440 pc=0x2e75e8
cuelang.org/go/internal/core/adt.(*FieldReference).resolve(0x400029e050, 0x400018dc20, 0x7fff0405)
/home/myitcv/dev/cuelang/cue/internal/core/adt/expr.go:761 +0x34 fp=0x40207524b0 sp=0x4020752470 pc=0x30ed04
cuelang.org/go/internal/core/adt.(*OpContext).resolveState(0x400018dc20, {0x40004cf900, {0xd279e0, 0x400029e050}, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...}}, ...)
/home/myitcv/dev/cuelang/cue/internal/core/adt/context.go:410 +0x10c fp=0x40207525b0 sp=0x40207524b0 pc=0x2e863c
cuelang.org/go/internal/core/adt.(*OpContext).Resolve(0x0?, {0x40004cf900, {0xd279e0, 0x400029e050}, {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...}}, ...)
/home/myitcv/dev/cuelang/cue/internal/core/adt/context.go:404 +0x94 fp=0x4020752620 sp=0x40207525b0 pc=0x2e8494
cuelang.org/go/internal/core/dep.(*visitor).markResolver(0x4000795800, 0x40004cf900, {0xd2c000, 0x400029e050})
/home/myitcv/dev/cuelang/cue/internal/core/dep/dep.go:363 +0x128 fp=0x4020752710 sp=0x4020752620 pc=0x33f3b8
cuelang.org/go/internal/core/dep.(*visitor).markExpr(0x4000795800, 0x40004cf900, {0xffff510a99e8?, 0x400029e050})
/home/myitcv/dev/cuelang/cue/internal/core/dep/dep.go:285 +0x320 fp=0x40207527c0 sp=0x4020752710 pc=0x33e820
cuelang.org/go/internal/core/dep.(*visitor).markExpr(0x4000795800, 0x40004cf900, {0xd2d488?, 0x40001c2090})
/home/myitcv/dev/cuelang/cue/internal/core/dep/dep.go:296 +0xbc8 fp=0x4020752870 sp=0x40207527c0 pc=0x33f0c8
cuelang.org/go/internal/core/dep.(*visitor).markExpr(0x4000795800, 0x40004cf900, {0xd2d488?, 0x40001c20c0})
/home/myitcv/dev/cuelang/cue/internal/core/dep/dep.go:297 +0xc04 fp=0x4020752920 sp=0x4020752870 pc=0x33f104
cuelang.org/go/internal/core/dep.(*visitor).markInternalResolvers.(*visitor).markConjuncts.func2({0x40004cf900, {0xd278c8, 0x400038d8a0}, {0x40004dca20, 0x0, 0x0, 0x0, 0x0, 0x0, {0x0, ...}}})
/home/myitcv/dev/cuelang/cue/internal/core/dep/dep.go:545 +0xa0 fp=0x4020752980 sp=0x4020752920 pc=0x340260
cuelang.org/go/internal/core/adt.VisitConjuncts({0x40004e6100?, 0x40004d74a0?, 0x4020752b38?}, 0x4020752a68)
/home/myitcv/dev/cuelang/cue/internal/core/adt/composite.go:681 +0xfc fp=0x4020752a20 sp=0x4020752980 pc=0x2d883c
cuelang.org/go/internal/core/adt.(*Vertex).VisitLeafConjuncts(...)
/home/myitcv/dev/cuelang/cue/internal/core/adt/composite.go:670
cuelang.org/go/internal/core/dep.(*visitor).markConjuncts(...)
/home/myitcv/dev/cuelang/cue/internal/core/dep/dep.go:542
cuelang.org/go/internal/core/dep.(*visitor).markInternalResolvers(0x4000795800, 0x40004cf900, {0xd2c000, 0x400029e070}, 0x0?)
/home/myitcv/dev/cuelang/cue/internal/core/dep/dep.go:570 +0x98 fp=0x4020752a90 sp=0x4020752a20 pc=0x340078
cuelang.org/go/internal/core/dep.(*visitor).reportDependency(0x4000795800, 0x40004cf900, {0xd2c000?, 0x400029e070?}, 0x40004d79a0)
/home/myitcv/dev/cuelang/cue/internal/core/dep/dep.go:441 +0x1ac fp=0x4020752bb0 sp=0x4020752a90 pc=0x33f80c
...
[exit status 2]
> ! stderr 'fatal error: stack overflow'
FAIL: /tmp/testscript383004678/repro.txtar/script.txtar:9: unexpected match for `fatal error: stack overflow` found in stderr: fatal error: stack overflow
error running repro.txtar in /tmp/testscript383004678/repro.txtar
Initial analysis. Applying the following diff allows the test to pass:
--- test_tool.cue.after 2025-02-07 19:35:43.974431073 +0000
+++ test_tool.cue.before 2025-02-07 19:35:00.354415819 +0000
@@ -5,7 +5,7 @@
"text/tabwriter"
)
-incomeData: income
+let incomeData = income
command: monthly: {
print: cli.Print & {
This also appears to be a long-standing issue.
Interesting that in this repro the change from a let binding to a field avoided the stack overflow. I tried this change in my project where I originally found the error and still get the stack overflow.
I've updated the original description in this issue with a cleaner repro, and also some explanation regarding the connection to https://github.com/cue-lang/cue/issues/3737 for the evalv3 case. Apologies, it might not be the clearest explanation, but it's a tricky situation to explain/assert!
Here is a reduction, which succeeds on evalv3 but stack overflows on evalv2:
# With the old evaluator.
env CUE_EXPERIMENT=evalv3=0
exec cue cmd monthly
# With the new evaluator.
env CUE_EXPERIMENT=evalv3=1
exec cue cmd monthly
-- test_tool.cue --
package test
import "tool/cli"
income: #Income & {
monthly: 1000.0
}
#Income: {
annual: monthly*12
monthly: annual/12
}
let incomeData = income
command: monthly: {
print: cli.Print & { text: "\(incomeData.annual)" }
}
However... Here is a reproducer which stack overflows on both evalv2 and evalv3:
# With the old evaluator.
env CUE_EXPERIMENT=evalv3=0
exec cue cmd monthly
# With the new evaluator.
env CUE_EXPERIMENT=evalv3=1
exec cue cmd monthly
-- test_tool.cue --
package test
import "tool/cli"
#Income: {
annual: monthly*12
monthly: annual/12
}
let incomeData = #Income & {
monthly: 1000.0
}
command: monthly: {
print: cli.Print & { text: "\(incomeData.annual)" }
}
So it seems like evalv3 is better but there is still a bug to be fixed.