cue icon indicating copy to clipboard operation
cue copied to clipboard

cmd/cue: stack overflow with let in _tool.cue file

Open myitcv opened this issue 9 months ago • 4 comments

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

myitcv avatar Feb 07 '25 19:02 myitcv

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.

myitcv avatar Feb 07 '25 19:02 myitcv

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.

tvandinther avatar Feb 10 '25 13:02 tvandinther

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!

myitcv avatar Feb 15 '25 13:02 myitcv

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.

mvdan avatar May 18 '25 22:05 mvdan