diff --git a/cmd/cue/cmd/testdata/script/def_issue3476.txtar b/cmd/cue/cmd/testdata/script/def_issue3476.txtar new file mode 100644 index 000000000..be07ea6a7 --- /dev/null +++ b/cmd/cue/cmd/testdata/script/def_issue3476.txtar @@ -0,0 +1,25 @@ +# Verify that cue def does not stack overflow on recursive definitions. +# Issue #3476. + +exec cue def -e '#a' in.cue +cmp stdout stdout.golden + +-- in.cue -- +#a: (#b & {x: _}).x +#b: { + x?: #c + #c: [string]: #c +} +-- stdout.golden -- + +_#def +_#def: { + [string]: [string]: C.#x +} + +//cue:path: #c +let C = { + #x: { + [string]: C.#x + } +} diff --git a/internal/core/dep/dep.go b/internal/core/dep/dep.go index 42b95d899..13baaec8f 100644 --- a/internal/core/dep/dep.go +++ b/internal/core/dep/dep.go @@ -189,14 +189,15 @@ func Visit(cfg *Config, c *adt.OpContext, n *adt.Vertex, f VisitFunc) error { panic("nil context") } v := visitor{ - ctxt: c, - fn: f, - pkg: cfg.Pkg, - recurse: cfg.Descend, - all: cfg.Descend, - top: true, - cfgDynamic: cfg.Dynamic, - resolved: map[refEntry]bool{}, + ctxt: c, + fn: f, + pkg: cfg.Pkg, + recurse: cfg.Descend, + all: cfg.Descend, + top: true, + cfgDynamic: cfg.Dynamic, + resolved: map[refEntry]bool{}, + visitedInternal: map[*adt.Vertex]bool{}, } return v.visitReusingVisitor(n, true) } @@ -271,6 +272,10 @@ type visitor struct { // resolved dedups resolving references to prevent exponential blowup. resolved map[refEntry]bool + + // visitedInternal tracks vertices already processed by markInternalResolvers + // to prevent infinite recursion on recursive definitions like [string]: #c. + visitedInternal map[*adt.Vertex]bool } type refEntry struct { @@ -557,6 +562,11 @@ func (c *visitor) markConjuncts(v *adt.Vertex) { // proactive. For selectors and indices this means we need to evaluate their // objects to see exactly what the selector or index refers to. func (c *visitor) markInternalResolvers(env *adt.Environment, r adt.Resolver, v *adt.Vertex) { + if c.visitedInternal[v] { + return + } + c.visitedInternal[v] = true + saved := c.all // recursive traversal already done by this function. // As lets have no path and we otherwise will not process them, we set diff --git a/internal/core/export/self.go b/internal/core/export/self.go index 418fb6f12..291858079 100644 --- a/internal/core/export/self.go +++ b/internal/core/export/self.go @@ -91,6 +91,10 @@ type pivotter struct { refs []*refData refMap map[adt.Resolver]*refData + // inlining tracks vertices currently being inlined by refExpr to prevent + // infinite recursion on recursive definitions like [string]: #c. + inlining map[*adt.Vertex]bool + decls []ast.Decl } @@ -432,11 +436,18 @@ func (p *pivotter) refExpr(r adt.Resolver) ast.Expr { // Don't simplify for errors to make the position of the error clearer. case !n.IsConcrete() && p.x.inExpression > 0: // Don't simplify an expression that is known will fail. + case p.inlining[n]: + // Prevent infinite recursion on recursive definitions. case dst.usageCount() == 1 && p.x.inExpression == 0: // Used only once. fallthrough case n.IsConcrete() && len(n.Arcs) == 0: // Simple scalar value. + if p.inlining == nil { + p.inlining = map[*adt.Vertex]bool{} + } + p.inlining[n] = true + defer delete(p.inlining, n) return p.x.expr(nil, n) }