@@ -30,10 +30,12 @@ import (
3030// - inline single-use .as() bindings
3131// - eliminate boolean comparisons (x == true → x, x == false → !x)
3232// - rewrite has(x.f) ? x.f : d and !has(x.f) ? d : x.f → x.?f.orValue(d)
33+ // - fold filter into comprehension: x.filter(v,c).map(v,t) → x.map(v,c,t)
3334func Simplify (a * ast.AST , src common.Source ) {
3435 inlineAs (a )
3536 elimBoolCmp (a )
3637 elimHasTernary (a , src )
38+ foldFilter (a )
3739}
3840
3941// inlineAs finds .as() macro calls where the bound variable is used at most
@@ -116,6 +118,25 @@ func countIdent(expr ast.Expr, ident string) int {
116118 return n
117119}
118120
121+ // countIdentInMacroTree counts IdentKind nodes named ident, recurring into
122+ // nested macro call expressions reachable via the SourceInfo macro call map.
123+ func countIdentInMacroTree (info * ast.SourceInfo , expr ast.Expr , ident string ) int {
124+ var n int
125+ var walk func (ast.Expr )
126+ walk = func (e ast.Expr ) {
127+ ast .PreOrderVisit (e , ast .NewExprVisitor (func (node ast.Expr ) {
128+ if node .Kind () == ast .IdentKind && node .AsIdent () == ident {
129+ n ++
130+ }
131+ if mcall , ok := info .GetMacroCall (node .ID ()); ok {
132+ walk (mcall )
133+ }
134+ }))
135+ }
136+ walk (expr )
137+ return n
138+ }
139+
119140// substituteIdent replaces all IdentKind nodes named ident with replacement.
120141// If info is non-nil and replacement has a macro call, the macro call is
121142// copied to each substitution site so the formatter can find it by ID.
@@ -132,6 +153,23 @@ func substituteIdent(expr ast.Expr, ident string, replacement ast.Expr, info *as
132153 }))
133154}
134155
156+ // substituteIdentInMacroTree replaces IdentKind nodes named ident with
157+ // replacement, recurring into nested macro call expressions.
158+ func substituteIdentInMacroTree (info * ast.SourceInfo , expr ast.Expr , ident string , replacement ast.Expr ) {
159+ var walk func (ast.Expr )
160+ walk = func (e ast.Expr ) {
161+ ast .PreOrderVisit (e , ast .NewExprVisitor (func (node ast.Expr ) {
162+ if node .Kind () == ast .IdentKind && node .AsIdent () == ident {
163+ node .SetKindCase (replacement )
164+ }
165+ if mcall , ok := info .GetMacroCall (node .ID ()); ok {
166+ walk (mcall )
167+ }
168+ }))
169+ }
170+ walk (expr )
171+ }
172+
135173// elimBoolCmp rewrites x == true → x and x == false → !x.
136174func elimBoolCmp (a * ast.AST ) {
137175 fac := ast .NewExprFactory ()
@@ -293,6 +331,134 @@ func isNegatedHasTest(e ast.Expr) bool {
293331 return len (args ) == 1 && isHasTest (args [0 ])
294332}
295333
334+ // foldFilter rewrites filter-then-comprehension chains into a single
335+ // comprehension with a filter argument:
336+ //
337+ // x.filter(v, cond).map(v, t) → x.map(v, cond, t)
338+ // x.filter(v, cond).transformList(_, v, t) → x.transformList(_, v, cond, t)
339+ // x.filter(v, cond).transformMap(_, v, t) → x.transformMap(_, v, cond, t)
340+ // x.filter(v, cond).transformMapEntry(_, v, t) → x.transformMapEntry(_, v, cond, t)
341+ //
342+ // For two-variable comprehensions the merge is only applied when the first
343+ // iteration variable (index/key) is "_", since the filter changes element
344+ // indices and merging would alter semantics if the index is used.
345+ func foldFilter (a * ast.AST ) {
346+ info := a .SourceInfo ()
347+ fac := ast .NewExprFactory ()
348+ for {
349+ folded := false
350+ for id , call := range info .MacroCalls () {
351+ if call .Kind () != ast .CallKind {
352+ continue
353+ }
354+ c := call .AsCall ()
355+ if ! c .IsMemberFunction () {
356+ continue
357+ }
358+ fn := c .FunctionName ()
359+ args := c .Args ()
360+
361+ // Identify comprehensions that accept an optional filter arg
362+ // and currently lack one (i.e. they have the minimum arg count).
363+ var valIdx int // index of the value iteration variable in args
364+ switch fn {
365+ case "map" :
366+ if len (args ) != 2 {
367+ continue
368+ }
369+ valIdx = 0
370+ case "transformList" , "transformMap" , "transformMapEntry" :
371+ if len (args ) != 3 {
372+ continue
373+ }
374+ valIdx = 1
375+ default :
376+ continue
377+ }
378+
379+ // The target of the macro call should reference the filter
380+ // comprehension by ID.
381+ target := c .Target ()
382+ filterCall , ok := info .GetMacroCall (target .ID ())
383+ if ! ok || filterCall .Kind () != ast .CallKind {
384+ continue
385+ }
386+ fc := filterCall .AsCall ()
387+ if fc .FunctionName () != "filter" || ! fc .IsMemberFunction () {
388+ continue
389+ }
390+ filterArgs := fc .Args ()
391+ if len (filterArgs ) != 2 {
392+ continue
393+ }
394+ filterVar := filterArgs [0 ]
395+ filterCond := filterArgs [1 ]
396+
397+ // The filter's iteration variable must match the consuming
398+ // comprehension's value variable, or be safely renameable.
399+ iterVar := args [valIdx ]
400+ if filterVar .Kind () != ast .IdentKind || iterVar .Kind () != ast .IdentKind {
401+ continue
402+ }
403+ needsRename := filterVar .AsIdent () != iterVar .AsIdent ()
404+ if needsRename && countIdentInMacroTree (info , filterCond , iterVar .AsIdent ()) > 0 {
405+ continue // target name already in condition — rename would capture
406+ }
407+
408+ // For two-variable comprehensions the first variable (key/index)
409+ // must be unused; otherwise the merge changes semantics because
410+ // the filter alters element indices.
411+ if valIdx == 1 {
412+ first := args [0 ]
413+ if first .Kind () != ast .IdentKind || first .AsIdent () != "_" {
414+ continue
415+ }
416+ }
417+
418+ // Build the merged macro call: target.fn(args..., cond, transform).
419+ filterTarget := fc .Target ()
420+ if needsRename {
421+ substituteIdentInMacroTree (info , filterCond , filterVar .AsIdent (), fac .NewIdent (0 , iterVar .AsIdent ()))
422+ }
423+ var newArgs []ast.Expr
424+ switch fn {
425+ case "map" :
426+ newArgs = []ast.Expr {iterVar , filterCond , args [1 ]}
427+ default :
428+ newArgs = []ast.Expr {args [0 ], iterVar , filterCond , args [2 ]}
429+ }
430+ newMCall := fac .NewMemberCall (0 , fn , filterTarget , newArgs ... )
431+ info .SetMacroCall (id , newMCall )
432+ info .ClearMacroCall (target .ID ())
433+
434+ // Update the AST: skip the filter comprehension so the
435+ // consuming comprehension iterates directly over the
436+ // filter's source.
437+ ast .PreOrderVisit (a .Expr (), ast .NewExprVisitor (func (e ast.Expr ) {
438+ if e .ID () != id || e .Kind () != ast .ComprehensionKind {
439+ return
440+ }
441+ iterRange := e .AsComprehension ().IterRange ()
442+ if iterRange .Kind () != ast .ComprehensionKind {
443+ return
444+ }
445+ filterSource := iterRange .AsComprehension ().IterRange ()
446+ if mcall , ok := info .GetMacroCall (filterSource .ID ()); ok {
447+ info .SetMacroCall (iterRange .ID (), mcall )
448+ info .ClearMacroCall (filterSource .ID ())
449+ }
450+ iterRange .SetKindCase (filterSource )
451+ }))
452+
453+ folded = true
454+ break
455+ }
456+ if ! folded {
457+ return
458+ }
459+ }
460+ }
461+
296462// exprEqual reports whether two expression trees are structurally identical,
297463// ignoring node IDs. Returns false for any kind it cannot compare.
298464func exprEqual (a , b ast.Expr ) bool {
0 commit comments