diff --git a/compiler/array.go b/compiler/array.go index 0b7c21f9..310bd2f2 100644 --- a/compiler/array.go +++ b/compiler/array.go @@ -299,9 +299,9 @@ func (c *Compiler) ArraySetCells(vec llvm.Value, cells []*Symbol, exprs []ast.Ex func (c *Compiler) compileArrayExpression(e *ast.ArrayLiteral, _ []*ast.Identifier) (res []*Symbol) { lit, info := c.resolveArrayLiteralRewrite(e) - // If ArrayLiteral has ranges, use with-loops path which creates accumulator - // pendingLoopRanges will filter already-bound ranges to prevent double-looping - if len(info.Ranges) == 0 { + // Array literals materialize their own collection domain instead of + // inheriting the parent expression's outer loop shape. + if len(info.CollectRanges) == 0 { return c.compileArrayLiteralImmediate(lit, info) } @@ -384,7 +384,7 @@ func (c *Compiler) compileArrayLiteralWithLoops(lit *ast.ArrayLiteral, info *Exp elemType := arr.ColTypes[0] acc := c.NewArrayAccumulator(arr) - c.withLoopNestVersioned(info.Ranges, lit, func() { + c.withCollectLoopNest(info.CollectRanges, func() { for _, cell := range lit.Rows[0] { c.compileAccumCell(acc, cell, elemType) } @@ -393,16 +393,17 @@ func (c *Compiler) compileArrayLiteralWithLoops(lit *ast.ArrayLiteral, info *Exp return []*Symbol{c.ArrayAccResult(acc)} } -// withValueRanges resolves the solver rewrite on a literal, then either -// wraps body in a loop nest (when the literal has value-level ranges) or -// calls it directly. The resolved literal is passed to body. -func (c *Compiler) withValueRanges(lit *ast.ArrayLiteral, body func(*ast.ArrayLiteral)) { - resolved, valueInfo := c.resolveArrayLiteralRewrite(lit) - if len(valueInfo.Ranges) == 0 { +// withPendingLiteralRanges resolves the solver rewrite on a literal, then +// iterates only the literal ranges still pending in the current outer context. +// This is used by top-level ranged accumulation, not by internal collection +// materialization. +func (c *Compiler) withPendingLiteralRanges(lit *ast.ArrayLiteral, body func(*ast.ArrayLiteral)) { + resolved, literalInfo := c.resolveArrayLiteralRewrite(lit) + if len(literalInfo.CollectRanges) == 0 { body(resolved) return } - c.withLoopNest(valueInfo.Ranges, func() { body(resolved) }) + c.withLoopNest(literalInfo.CollectRanges, func() { body(resolved) }) } // compileAccumCell compiles one cell under a fresh bounds guard and pushes @@ -435,7 +436,7 @@ func (c *Compiler) compileArrayLiteralCellExpr(cell ast.Expression) []*Symbol { // accumulator with per-cell bounds guards. func (c *Compiler) appendArrayLiteral(acc *ArrayAccumulator, lit *ast.ArrayLiteral) { elemType := acc.ElemType - c.withValueRanges(lit, func(resolved *ast.ArrayLiteral) { + c.withPendingLiteralRanges(lit, func(resolved *ast.ArrayLiteral) { for _, cell := range resolved.Rows[0] { c.compileAccumCell(acc, cell, elemType) } @@ -443,9 +444,9 @@ func (c *Compiler) appendArrayLiteral(acc *ArrayAccumulator, lit *ast.ArrayLiter } // appendArrayLiterals dispatches to the appropriate accumulation strategy for -// top-level 1D array-literal outputs from ranged conditional lowering. Single -// values use the direct per-cell path; tuples share one bounds guard so -// remaining guarded write/skip decisions stay synchronized across outputs. +// top-level 1D array-literal outputs from ranged conditional lowering. +// Each collector owns its own local value-range domain and appends +// independently of sibling array outputs. func (c *Compiler) appendArrayLiterals(accs []*ArrayAccumulator, values []*ast.ArrayLiteral) { if len(values) == 1 { c.appendArrayLiteral(accs[0], values[0]) @@ -454,54 +455,12 @@ func (c *Compiler) appendArrayLiterals(accs []*ArrayAccumulator, values []*ast.A c.appendTupleArrayLiterals(accs, values) } -// appendTupleArrayLiterals compiles cells from multiple accumulating -// array-literal outputs under a single shared bounds guard. Literal cells -// preserve shape via zero-fill, while any remaining guarded failures still -// keep the tuple outputs synchronized per iteration. +// appendTupleArrayLiterals appends each accumulating array-literal output +// independently. Local ranges belong to the collector that mentions them and +// do not leak across sibling array outputs in the same tuple assignment. func (c *Compiler) appendTupleArrayLiterals(accs []*ArrayAccumulator, values []*ast.ArrayLiteral) { - c.pushBoundsGuard("tuple_bounds_guard") - - accSyms := make([][]*Symbol, len(accs)) - accCells := make([][]ast.Expression, len(accs)) for i, lit := range values { - c.withValueRanges(lit, func(resolved *ast.ArrayLiteral) { - for _, cell := range resolved.Rows[0] { - vals := c.compileArrayLiteralCellExpr(cell) - accSyms[i] = append(accSyms[i], c.derefIfPointer(vals[0], "")) - accCells[i] = append(accCells[i], cell) - } - }) - } - - if !c.withActiveBoundsGuard( - "tuple_ok", - "tuple_push", - "tuple_skip", - "tuple_cont", - func() { c.pushTupleCells(accs, accSyms, accCells) }, - func() { c.freeTupleCells(accSyms, accCells) }, - ) { - c.pushTupleCells(accs, accSyms, accCells) - } - c.popBoundsGuard() -} - -// pushTupleCells pushes all compiled tuple cells into their accumulators. -func (c *Compiler) pushTupleCells(accs []*ArrayAccumulator, accSyms [][]*Symbol, accCells [][]ast.Expression) { - for i, acc := range accs { - for j, sym := range accSyms[i] { - _, isIdent := accCells[i][j].(*ast.Identifier) - c.pushAccumCellValue(acc, sym, !isIdent, acc.ElemType) - } - } -} - -// freeTupleCells frees all compiled tuple cell temporaries. -func (c *Compiler) freeTupleCells(accSyms [][]*Symbol, accCells [][]ast.Expression) { - for i := range accSyms { - for j := range accSyms[i] { - c.freeTemporary(accCells[i][j], []*Symbol{accSyms[i][j]}) - } + c.appendArrayLiteral(accs[i], lit) } } diff --git a/compiler/compiler.go b/compiler/compiler.go index 5f70ed88..bce35acb 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -63,9 +63,8 @@ type Symbol struct { // - Borrowed tracks lifetime/ownership (cleanup must skip when true). type FuncArgs struct { - Inputs []*Symbol // lowered function inputs (range iterators remain pointer-backed) - IterIndices []int // Indices of iterator params - Iters map[string]*Symbol // Current iterator values during loop + Inputs []*Symbol // lowered function inputs (range iterators remain pointer-backed) + IterIndices []int // Indices of iterator params } type callArg struct { @@ -2344,7 +2343,6 @@ func (c *Compiler) compileFuncIter(template *ast.FuncStatement, inputs []*Symbol fa := &FuncArgs{ Inputs: inputs, IterIndices: iterIndices, - Iters: make(map[string]*Symbol), } return c.funcLoopNest(template, fa, 0, currentOutput) } @@ -2366,7 +2364,7 @@ func (c *Compiler) compileFuncBlock(template *ast.FuncStatement, sig *callSignat c.bindFuncOutputs(template, outputs) if len(iterIndices) == 0 { - c.compileBlockWithArgs(template, map[string]*Symbol{}, map[string]*Symbol{}) + c.compileBlockWithArgs(template, nil) } else if sig.ABI.Return.Mode == ABIReturnDirect { // ABIReturnDirect is currently a single scalar output, so the seeded // binding above becomes the initial loop-carried SSA state here. @@ -2483,11 +2481,13 @@ func (c *Compiler) iterOverArrayRangeState(arrRangeSym *Symbol, currentOutput *S func (c *Compiler) funcLoopNest(fn *ast.FuncStatement, fa *FuncArgs, level int, currentOutput *Symbol) *Symbol { if level == len(fa.IterIndices) { + PushScope(&c.Scopes, BlockScope) + defer c.popScope() if currentOutput == nil { - c.compileBlockWithArgs(fn, map[string]*Symbol{}, fa.Iters) + c.compileBlockWithArgs(fn, nil) return nil } - return c.compileDirectOutputIterBody(fn, fa.Iters, currentOutput) + return c.compileDirectOutputIterBody(fn, currentOutput) } paramIdx := fa.IterIndices[level] @@ -2495,13 +2495,19 @@ func (c *Compiler) funcLoopNest(fn *ast.FuncStatement, fa *FuncArgs, level int, name := fn.Parameters[paramIdx].Value next := func(iterVal llvm.Value, iterType Type, current *Symbol) *Symbol { - fa.Iters[name] = &Symbol{ + iterSym := &Symbol{ Val: iterVal, Type: iterType, FuncArg: true, Borrowed: true, ReadOnly: false, } + // Function iterator params mirror statement loops: each nested iterator + // level gets its own shadow scope, so pre-iteration lookup can peel back + // one level at a time structurally. + PushIterScope(&c.Scopes) + Put(c.Scopes, name, iterSym) + defer c.popScope() return c.funcLoopNest(fn, fa, level+1, current) } @@ -2528,30 +2534,22 @@ func (c *Compiler) funcLoopNest(fn *ast.FuncStatement, fa *FuncArgs, level int, default: panic("unsupported iterator kind in funcLoopNest") } - delete(fa.Iters, name) return result } -func (c *Compiler) compileDirectOutputIterBody(fn *ast.FuncStatement, iters map[string]*Symbol, currentOutput *Symbol) *Symbol { - PushScope(&c.Scopes, BlockScope) - defer c.popScope() - - PutBulk(c.Scopes, iters) +func (c *Compiler) compileDirectOutputIterBody(fn *ast.FuncStatement, currentOutput *Symbol) *Symbol { // Direct-return ABI is single-output today, so the loop body only needs the // current scalar output binding for fn.Outputs[0]. - Put(c.Scopes, fn.Outputs[0].Value, currentOutput) - - for _, stmt := range fn.Body.Statements { - c.compileStatement(stmt) - } + c.compileBlockWithArgs(fn, map[string]*Symbol{fn.Outputs[0].Value: currentOutput}) output, _ := c.localValSymbol(fn.Outputs[0].Value, fn.Outputs[0].Value+"_iter_out") return output } -func (c *Compiler) compileBlockWithArgs(fn *ast.FuncStatement, scalars map[string]*Symbol, iters map[string]*Symbol) { +// compileBlockWithArgs executes a function body in the writable scope prepared +// by the caller, seeding any extra scalar bindings needed for that body entry. +func (c *Compiler) compileBlockWithArgs(fn *ast.FuncStatement, scalars map[string]*Symbol) { PutBulk(c.Scopes, scalars) - PutBulk(c.Scopes, iters) for _, stmt := range fn.Body.Statements { c.compileStatement(stmt) diff --git a/compiler/cond.go b/compiler/cond.go index d53e88b7..7cceb66c 100644 --- a/compiler/cond.go +++ b/compiler/cond.go @@ -239,6 +239,72 @@ func (c *Compiler) compileCondAssignmentsWithGuard(tempNames []*ast.Identifier, c.finishAssignmentsWithGuard(tempNames, dest, exprs, oldValues, syms, rhsNames, resCounts, guardPtr) } +func (c *Compiler) createStageTempOutputsFor(dest []*ast.Identifier) []*ast.Identifier { + tempNames := make([]*ast.Identifier, len(dest)) + for i, ident := range dest { + commitTempSym, _ := Get(c.Scopes, ident.Value) + ptrType := commitTempSym.Type.(Ptr) + outType := ptrType.Elem + + tempName := fmt.Sprintf("condstage_%s_%d", ident.Value, c.tmpCounter) + c.tmpCounter++ + tempIdent := &ast.Identifier{Value: tempName} + + ptr := c.createEntryBlockAlloca(c.mapToLLVMType(outType), tempName+".mem") + stageTempSym := &Symbol{ + Val: ptr, + Type: Ptr{Elem: outType}, + Borrowed: true, + } + seed := c.resolveDestSeed(ident, outType) + seed = c.deepCopyIfNeeded(seed) + c.storeSymbolToSlot(stageTempSym, seed, outType, tempName+"_seed") + Put(c.Scopes, tempName, stageTempSym) + tempNames[i] = tempIdent + } + return tempNames +} + +// Stage temps own their seeded copy or compiled temporary outright. Discarding +// a failed staged write therefore frees only stage-local storage and never +// touches the destination slot's current value. +func (c *Compiler) freeStageTempOutputs(tempNames []*ast.Identifier) { + for _, ident := range tempNames { + tempSym, ok := Get(c.Scopes, ident.Value) + if !ok { + continue + } + c.freeSymbolValue(c.valueSymbol(ident.Value, tempSym, ident.Value+"_stage_discard"), ident.Value+"_stage_discard") + } +} + +func (c *Compiler) commitStageTempOutputs(dest []*ast.Identifier, stageTempNames []*ast.Identifier) { + for i, ident := range dest { + stageSym, ok := Get(c.Scopes, stageTempNames[i].Value) + if !ok { + panic(fmt.Sprintf("internal: staged conditional temp %q not found in scope", stageTempNames[i].Value)) + } + destSym, ok := Get(c.Scopes, ident.Value) + if !ok { + panic(fmt.Sprintf("internal: conditional temp %q not found in scope", ident.Value)) + } + + oldValue := c.valueSymbol(ident.Value, destSym, ident.Value+"_stage_old") + ptrType, ok := destSym.Type.(Ptr) + if !ok { + panic(fmt.Sprintf("internal: conditional temp %q is not pointer-backed", ident.Value)) + } + + stagedValue := c.valueSymbol(stageTempNames[i].Value, stageSym, stageTempNames[i].Value+"_stage_final") + c.storeSymbolToSlot(destSym, stagedValue, ptrType.Elem, ident.Value+"_stage_commit") + + if c.skipBorrowedOldValueFree(oldValue) { + continue + } + c.freeSymbolValue(oldValue, ident.Value+"_stage_old") + } +} + // compileCondStatement lowers: // // name = cond value @@ -456,19 +522,6 @@ func (c *Compiler) splitCondRanges(conditions []ast.Expression) ([]*RangeInfo, [ return ranges, condExprs } -// mergeValueRanges merges value-level ranges from all value expressions -// into a base set of ranges. Returns the combined set. -func (c *Compiler) mergeValueRanges(base []*RangeInfo, values []ast.Expression) []*RangeInfo { - all := base - for _, expr := range values { - info := c.ExprCache[key(c.FuncNameMangled, expr)] - if len(info.Ranges) > 0 { - all = mergeUses(all, info.Ranges) - } - } - return all -} - // withCondRangeLoop sets up the shared loop+guard+branch scaffold used by // both accumulation and iteration paths: loop over all ranges, evaluate // conditions, branch on the combined result, and call body on the true path. @@ -493,10 +546,12 @@ func (c *Compiler) withCondRangeLoop(allRanges []*RangeInfo, condExprs []ast.Exp }) } -// compileCondRangedStatement lowers ranged conditions with per-output behavior. -// This intentionally gives mixed ranged tuples split semantics: top-level 1D -// array literals accumulate across true iterations, while all other outputs use -// normal conditional iteration (last value wins). +// compileCondRangedStatement lowers ranged statement conditions. +// Statement conditions are shared across the whole assignment. They determine +// the outer admitted iteration domain, while each RHS expression keeps any +// extra local drivers to itself inside that shared gate. Top-level 1D array +// literals accumulate across admitted iterations; all other outputs use normal +// conditional iteration (last value wins). func (c *Compiler) compileCondRangedStatement(stmt *ast.LetStatement, condRanges []*RangeInfo, condExprs []ast.Expression) { c.prePromoteConditionalCallArgs(stmt.Value) @@ -534,18 +589,15 @@ func (c *Compiler) compileCondRangedStatement(stmt *ast.LetStatement, condRanges } hasAssigns := len(assignDests) > 0 - assignHasCondExpr := c.valuesHaveCondExpr(assignExprs) var assignTempNames []*ast.Identifier if hasAssigns { assignTempNames = c.createConditionalTempOutputsFor(assignDests, assignOutTypes) } - allRanges := c.mergeValueRanges(condRanges, stmt.Value) - - c.withCondRangeLoop(allRanges, condExprs, "cond_iter_guard", "cond_iter_if", "cond_iter_cont", func() { + c.withCondRangeLoop(condRanges, condExprs, "cond_iter_guard", "cond_iter_if", "cond_iter_cont", func() { c.compileCondRangedIteration( - assignExprs, assignDests, assignTempNames, assignHasCondExpr, + assignExprs, assignDests, assignTempNames, accumAccs, accumLits, ) }) @@ -567,12 +619,11 @@ func (c *Compiler) compileCondRangedStatement(stmt *ast.LetStatement, condRanges // compileCondRangedIteration runs inside the per-iteration body of // compileCondRangedStatement. It compiles scalar assignments under a -// bounds guard and appends array literal cells to accumulators. +// shared bounds guard and appends array literal cells to accumulators. func (c *Compiler) compileCondRangedIteration( assignExprs []ast.Expression, assignDests []*ast.Identifier, - assignTempNames []*ast.Identifier, - assignHasCondExpr bool, + commitTempNames []*ast.Identifier, accumAccs []*ArrayAccumulator, accumLits []*ast.ArrayLiteral, ) { @@ -585,36 +636,75 @@ func (c *Compiler) compileCondRangedIteration( guardPtr := c.pushBoundsGuard("cond_value_guard") defer c.popBoundsGuard() - // Assigns without cond-exprs: compile values, optionally append accums, - // then finish with guard. Values and accums share the same bounds guard - // so OOB in either skips the whole iteration. - if !assignHasCondExpr { - assignOldValues, assignSyms, assignRhsNames, assignResCounts := c.compileCondAssignmentValues(assignTempNames, assignDests, assignExprs) - if len(accumLits) > 0 { - c.appendArrayLiterals(accumAccs, accumLits) - } - c.finishAssignmentsWithGuard(assignTempNames, assignDests, assignExprs, assignOldValues, assignSyms, assignRhsNames, assignResCounts, guardPtr) - return - } - - // Assigns with cond-exprs: each expression is wrapped individually - // so false cond-exprs preserve the old value. + // The outer statement condition admits one iteration here, but sibling RHS + // expressions may still fail bounds checks later in that same admitted step. + // To keep tuple order from becoming observable, each RHS first writes into a + // private stage temp. Only after every RHS (and any top-level collectors) + // has run do we branch on the final shared guard and either commit all stage + // temps into the real conditional commit temps or discard them all together. assignTargetIdx := 0 + stagedCommitTempGroups := make([][]*ast.Identifier, 0, len(assignExprs)) + stagedTempGroups := make([][]*ast.Identifier, 0, len(assignExprs)) + allAliases := c.aliasCondDests(assignDests, commitTempNames) for _, expr := range assignExprs { info := c.ExprCache[key(c.FuncNameMangled, expr)] numOutputs := len(info.OutTypes) - exprTempNames := assignTempNames[assignTargetIdx : assignTargetIdx+numOutputs] + exprCommitTempNames := commitTempNames[assignTargetIdx : assignTargetIdx+numOutputs] exprDestNames := assignDests[assignTargetIdx : assignTargetIdx+numOutputs] - exprValues := []ast.Expression{expr} - c.compileCondExprValue(expr, llvm.Value{}, func() { - c.compileCondAssignmentsWithGuard(exprTempNames, exprDestNames, exprValues, guardPtr) + stageTempNames := c.createStageTempOutputsFor(exprCommitTempNames) + + stageAliases := c.aliasCondDests(exprDestNames, stageTempNames) + c.withLoopNest(info.Ranges, func() { + if c.hasCondExprInTree(expr) { + c.compileCondExprValue(expr, llvm.Value{}, func() { + c.compileCondAssignmentsWithGuard(stageTempNames, exprDestNames, []ast.Expression{expr}, guardPtr) + }) + return + } + + c.compileCondAssignmentsWithGuard(stageTempNames, exprDestNames, []ast.Expression{expr}, guardPtr) }) + c.restoreCondDests(stageAliases) + + stagedCommitTempGroups = append(stagedCommitTempGroups, exprCommitTempNames) + stagedTempGroups = append(stagedTempGroups, stageTempNames) assignTargetIdx += numOutputs } + c.restoreCondDests(allAliases) if len(accumLits) > 0 { c.appendArrayLiterals(accumAccs, accumLits) } + + if !c.stmtBoundsUsed() { + for i, stageTempNames := range stagedTempGroups { + c.commitStageTempOutputs(stagedCommitTempGroups[i], stageTempNames) + DeleteBulk(c.Scopes, tempNamesToStrings(stageTempNames)) + } + return + } + + c.withGuardedBranch( + guardPtr, + "cond_stage_ok", + "cond_stage_write", + "cond_stage_skip", + "cond_stage_cont", + func() { + for i, stageTempNames := range stagedTempGroups { + c.commitStageTempOutputs(stagedCommitTempGroups[i], stageTempNames) + } + }, + func() { + for _, stageTempNames := range stagedTempGroups { + c.freeStageTempOutputs(stageTempNames) + } + }, + ) + + for _, stageTempNames := range stagedTempGroups { + DeleteBulk(c.Scopes, tempNamesToStrings(stageTempNames)) + } } // compileCondExprStatement handles let statements that have conditional diff --git a/compiler/loop.go b/compiler/loop.go index 0db9e2ac..73c7cea3 100644 --- a/compiler/loop.go +++ b/compiler/loop.go @@ -13,6 +13,37 @@ type Loop struct { Exit *llvm.BasicBlock } +func preIterationSymbol(scopes []Scope[*Symbol], name string) (*Symbol, bool) { + for i := len(scopes) - 1; i >= 0; i-- { + if scopes[i].ScopeKind == IterScope { + continue + } + + sym, ok := scopes[i].Elems[name] + if ok { + return sym, true + } + + if scopes[i].ScopeKind == FuncScope { + break + } + } + return nil, false +} + +func (c *Compiler) getPreIterationSymbol(name string) (*Symbol, bool) { + if sym, ok := preIterationSymbol(c.Scopes, name); ok { + return sym, true + } + + if c.CodeCompiler != nil { + if sym, ok := preIterationSymbol(c.CodeCompiler.Compiler.Scopes, name); ok { + return sym, true + } + } + return nil, false +} + // extractRangeSymbol loads a range aggregate from a symbol when needed. func (c *Compiler) extractRangeSymbol(sym *Symbol, name string) (*Symbol, bool) { switch t := sym.Type.(type) { @@ -53,6 +84,46 @@ func (c *Compiler) extractArrayRangeSymbol(sym *Symbol, name string) (*Symbol, b return nil, false } +func (c *Compiler) rangeAggregateFromSymbol(sym *Symbol, name string) llvm.Value { + if rangeSym, ok := c.extractRangeSymbol(sym, name); ok { + return rangeSym.Val + } + + if arrRangeSym, ok := c.extractArrayRangeSymbol(sym, name); ok { + return c.builder.CreateExtractValue(arrRangeSym.Val, 1, name+"_range") + } + + panic(fmt.Sprintf("internal: %q is not a Range or ArrayRange during lowering (got %s)", name, sym.Type.String())) +} + +func (c *Compiler) iterOverDriverSymbol(sym *Symbol, name string, body func(*Symbol)) { + if rangeSym, ok := c.extractRangeSymbol(sym, name); ok { + c.iterOverRange(rangeSym.Type.(Range), rangeSym.Val, func(iter llvm.Value, iterType Type) { + body(&Symbol{ + Val: iter, + Type: iterType, + FuncArg: rangeSym.FuncArg, + Borrowed: true, + }) + }) + return + } + + if arrRangeSym, ok := c.extractArrayRangeSymbol(sym, name); ok { + c.iterOverArrayRange(arrRangeSym, func(iter llvm.Value, iterType Type) { + body(&Symbol{ + Val: iter, + Type: iterType, + FuncArg: arrRangeSym.FuncArg, + Borrowed: true, + }) + }) + return + } + + panic(fmt.Sprintf("internal: %q is not a Range or ArrayRange during lowering (got %s)", name, sym.Type.String())) +} + // rangeAggregateForRI builds the {start,stop,step} aggregate for a driver. // Named ArrayRange drivers contribute their underlying range component. func (c *Compiler) rangeAggregateForRI(ri *RangeInfo) llvm.Value { @@ -64,16 +135,7 @@ func (c *Compiler) rangeAggregateForRI(ri *RangeInfo) llvm.Value { if !ok { panic(fmt.Sprintf("internal: range driver %q not found in scope (should have been caught by type solver)", ri.Name)) } - - if rangeSym, ok := c.extractRangeSymbol(sym, ri.Name); ok { - return rangeSym.Val - } - - if arrRangeSym, ok := c.extractArrayRangeSymbol(sym, ri.Name); ok { - return c.builder.CreateExtractValue(arrRangeSym.Val, 1, ri.Name+"_range") - } - - panic(fmt.Sprintf("internal: range driver %q expected Range or ArrayRange kind, got %s (should have been caught by type solver)", ri.Name, sym.Type.String())) + return c.rangeAggregateFromSymbol(sym, ri.Name) } // iterOverRangeInfo iterates a single driver, binding its per-iteration scalar value. @@ -91,32 +153,26 @@ func (c *Compiler) iterOverRangeInfo(ri *RangeInfo, body func(*Symbol)) { if !ok { panic(fmt.Sprintf("internal: range driver %q not found in scope (should have been caught by type solver)", ri.Name)) } + c.iterOverDriverSymbol(sym, ri.Name, body) +} - if rangeSym, ok := c.extractRangeSymbol(sym, ri.Name); ok { - c.iterOverRange(rangeSym.Type.(Range), rangeSym.Val, func(iter llvm.Value, iterType Type) { - body(&Symbol{ - Val: iter, - Type: iterType, - FuncArg: rangeSym.FuncArg, - Borrowed: true, - }) +// iterOverCollectRangeInfo re-opens a range used for collection even when an outer +// loop currently shadows the same name with a scalar iterator. +func (c *Compiler) iterOverCollectRangeInfo(ri *RangeInfo, body func(*Symbol)) { + if ri.RangeLit != nil { + rangeType := Range{Iter: Int{Width: 64}} + rangeVal := c.ToRange(ri.RangeLit, rangeType) + c.iterOverRange(rangeType, rangeVal, func(iter llvm.Value, iterType Type) { + body(&Symbol{Val: iter, Type: iterType, Borrowed: true}) }) return } - if arrRangeSym, ok := c.extractArrayRangeSymbol(sym, ri.Name); ok { - c.iterOverArrayRange(arrRangeSym, func(iter llvm.Value, iterType Type) { - body(&Symbol{ - Val: iter, - Type: iterType, - FuncArg: arrRangeSym.FuncArg, - Borrowed: true, - }) - }) - return + sym, ok := c.getPreIterationSymbol(ri.Name) + if !ok { + panic(fmt.Sprintf("internal: pre-iteration binding %q not found in scope (should have been caught by type solver)", ri.Name)) } - - panic(fmt.Sprintf("internal: range driver %q expected Range or ArrayRange kind, got %s (should have been caught by type solver)", ri.Name, sym.Type.String())) + c.iterOverDriverSymbol(sym, ri.Name, body) } // Build a nested loop over specs; at each level shadow specs[i].Name with the scalar iter; run body at innermost. @@ -133,7 +189,32 @@ func (c *Compiler) withLoopNest(ranges []*RangeInfo, body func()) { return } c.iterOverRangeInfo(ranges[i], func(iterSym *Symbol) { - PushScope(&c.Scopes, BlockScope) + PushIterScope(&c.Scopes) + Put(c.Scopes, ranges[i].Name, iterSym) + rec(i + 1) + c.popScope() + }) + } + rec(0) +} + +// withCollectLoopNest iterates the collection domain exactly as written in +// the expression, even when the same driver names are shadowed by outer scalar +// iterator bindings in the current scope. +func (c *Compiler) withCollectLoopNest(ranges []*RangeInfo, body func()) { + if len(ranges) == 0 { + body() + return + } + + var rec func(i int) + rec = func(i int) { + if i == len(ranges) { + body() + return + } + c.iterOverCollectRangeInfo(ranges[i], func(iterSym *Symbol) { + PushIterScope(&c.Scopes) Put(c.Scopes, ranges[i].Name, iterSym) rec(i + 1) c.popScope() diff --git a/compiler/scopes.go b/compiler/scopes.go index bd4b5c56..afa83509 100644 --- a/compiler/scopes.go +++ b/compiler/scopes.go @@ -9,6 +9,7 @@ type ScopeKind int const ( FuncScope ScopeKind = iota BlockScope + IterScope ) type Scope[T any] struct { @@ -27,6 +28,10 @@ func PushScope[T any](scopes *[]Scope[T], sk ScopeKind) { *scopes = append(*scopes, NewScope[T](sk)) } +func PushIterScope[T any](scopes *[]Scope[T]) { + *scopes = append(*scopes, NewScope[T](IterScope)) +} + func PopScope[T any](scopes *[]Scope[T]) { if len(*scopes) == 1 { panic("cannot pop global scope") diff --git a/compiler/solver.go b/compiler/solver.go index 33123382..d21ae4ec 100644 --- a/compiler/solver.go +++ b/compiler/solver.go @@ -26,6 +26,7 @@ const ( type ExprInfo struct { Ranges []*RangeInfo // either value from *ast.Identifier or a newly created value from tmp identifier for *ast.RangeLiteral + CollectRanges []*RangeInfo // ranges owned and materialized internally by this expression's collector (array literals) Rewrite ast.Expression // expression rewritten with a literal -> tmp value. (0:11) -> tmpIter0 etc. ExprLen int OutTypes []Type @@ -380,17 +381,19 @@ func cloneArrayIndices(indices map[string][]int) map[string][]int { return out } -func (ts *TypeSolver) HandleArrayLiteralRanges(al *ast.ArrayLiteral) (ranges []*RangeInfo, rew ast.Expression) { +func (ts *TypeSolver) HandleArrayLiteralRanges(al *ast.ArrayLiteral) ([]*RangeInfo, ast.Expression) { info := ts.ExprCache[key(ts.FuncNameMangled, al)] // Only 1D array literals are currently supported by the compiler. if !(len(al.Headers) == 0 && len(al.Rows) == 1) { info.Ranges = nil + info.CollectRanges = nil info.Rewrite = al return nil, al } row := al.Rows[0] + var ranges []*RangeInfo changed := false newRow := make([]ast.Expression, len(row)) for i, cell := range row { @@ -403,7 +406,7 @@ func (ts *TypeSolver) HandleArrayLiteralRanges(al *ast.ArrayLiteral) (ranges []* } } - rew = al + rew := ast.Expression(al) if changed { newLit := &ast.ArrayLiteral{ Token: al.Token, @@ -412,19 +415,21 @@ func (ts *TypeSolver) HandleArrayLiteralRanges(al *ast.ArrayLiteral) (ranges []* Indices: cloneArrayIndices(al.Indices), } infoCopy := &ExprInfo{ - OutTypes: info.OutTypes, - ExprLen: info.ExprLen, - Ranges: append([]*RangeInfo(nil), ranges...), - Rewrite: newLit, + OutTypes: info.OutTypes, + ExprLen: info.ExprLen, + CollectRanges: append([]*RangeInfo(nil), ranges...), + Rewrite: newLit, } ts.ExprCache[key(ts.FuncNameMangled, newLit)] = infoCopy rew = newLit } - info.Ranges = ranges + info.Ranges = nil + info.CollectRanges = ranges + info.HasRanges = false info.Rewrite = rew - return ranges, rew + return nil, rew } func (ts *TypeSolver) HandleArrayRangeExpression(ar *ast.ArrayRangeExpression) (ranges []*RangeInfo, rew ast.Expression) { @@ -867,23 +872,19 @@ func (ts *TypeSolver) TypeArrayExpression(al *ast.ArrayLiteral) []Type { if len(al.Headers) == 0 && len(al.Rows) == 1 { colTypes := []Type{Unresolved{}} row := al.Rows[0] - hasRanges := false for col := 0; col < len(row); col++ { cellT, ok := ts.typeCell(row[col], al.Tok()) if !ok { continue } colTypes[0] = ts.mergeColType(colTypes[0], cellT, 0, al.Tok()) - // Propagate HasRanges from cells - if ts.ExprCache[key(ts.FuncNameMangled, row[col])].HasRanges { - hasRanges = true - } } // Length = number of elements in the row (for vectors) arr := Array{Headers: nil, ColTypes: colTypes, Length: len(row)} - // Cache this expression's resolved type for the compiler - ts.ExprCache[key(ts.FuncNameMangled, al)] = &ExprInfo{OutTypes: []Type{arr}, ExprLen: 1, HasRanges: hasRanges} + // Array literals materialize their own internal ranges instead of + // propagating them upward into the parent expression's loop shape. + ts.ExprCache[key(ts.FuncNameMangled, al)] = &ExprInfo{OutTypes: []Type{arr}, ExprLen: 1, HasRanges: false} return []Type{arr} } // unsupported as of now diff --git a/compiler/solver_test.go b/compiler/solver_test.go index 1434594f..db004d0d 100644 --- a/compiler/solver_test.go +++ b/compiler/solver_test.go @@ -608,7 +608,8 @@ res = [idx]` info := ts.ExprCache[key(ts.FuncNameMangled, arrLit)] require.NotNil(t, info) - require.Len(t, info.Ranges, 1) + require.Empty(t, info.Ranges) + require.Len(t, info.CollectRanges, 1) require.NotNil(t, info.Rewrite) require.IsType(t, &ast.ArrayLiteral{}, info.Rewrite) } diff --git a/docs/Pluto Range Semantics.md b/docs/Pluto Range Semantics.md index abca8b7d..ea5bbd20 100644 --- a/docs/Pluto Range Semantics.md +++ b/docs/Pluto Range Semantics.md @@ -1,381 +1,286 @@ # Pluto Range Semantics -## Core Principle: Base Function Model +## Core Model -In Pluto, **every operation is a function**. Each expression has a **base function** that determines where range iteration occurs. Ranges are expanded as loops **inside** the base function's body. +Expressions that mention ranges produce ordered per-iteration values. +Those values are not arrays by default. -### Finding the Base Function +There are two explicit closing steps: -Every expression is a function call. The base function is found by: +1. `[]` closes a value stream into an array. +2. The root expression of a scalar assignment closes any remaining outer + iteration by taking the final yielded value in iteration order. -``` -findBase(f): - if f has exactly one argument AND that argument is a function call g: - return findBase(g) # descend into single-arg function - else: - return f # f is base -``` +This keeps array materialization and scalar finalization separate. -**Rules:** -1. Start at the root function of the expression -2. If it has exactly **one argument** that is itself a **function call**, descend into it -3. Continue until you hit a function with multiple args, or whose single arg is a value (range, variable, literal) -4. The base function's body contains all range iteration loops - -### Examples - -| Expression | Tree | Base | Reason | -|------------|------|------|--------| -| `f(0:5)` | `f(range)` | `f` | single arg is value | -| `f(g(0:5))` | `f(g(range))` | `g` | descend through single-arg `f` | -| `f(g(h(0:5)))` | `f(g(h(range)))` | `h` | keep descending | -| `f(a, b)` | `f(a, b)` | `f` | multiple args | -| `a + b` | `Add(a, b)` | `Add` | multiple args | -| `-x` | `Negate(x)` | `Negate` | single arg is value | -| `-(0:5)` | `Negate(range)` | `Negate` | single arg is value | -| `√(x + y)` | `Sqrt(Add(x, y))` | `Add` | descend through single-arg `Sqrt` | -| `√(x + 0:5)` | `Sqrt(Add(x, range))` | `Add` | descend to `Add` | -| `arr[0:5] + 1` | `Add(Index(arr, range), 1)` | `Add` | multiple args | - ---- - -## Range Basics - -A range `start:stop` or `start:stop:step` defines iteration bounds: - -```python -i = 0:5 # Iterates: 0, 1, 2, 3, 4 -j = 0:10:2 # Iterates: 0, 2, 4, 6, 8 -k = 5:0:-1 # Iterates: 5, 4, 3, 2, 1 -``` +## Ranges And Drivers + +A range or array-range used in an expression contributes an iteration driver. +Multiple distinct drivers form a nested iteration domain in source order. +Repeated use of the same driver name refers to the same loop, not a nested copy. -Ranges are values that describe iteration - they expand when used in expressions. +Example: + +```pluto +i = 0:5 +x = i + 1 +``` ---- +This iterates `i` over `0, 1, 2, 3, 4` and the root assignment keeps the final +value, so `x = 5`. -## Loop Generation Inside Base Function +## Calls, Infix, And Prefix -Once the base function is determined, all ranges in its arguments become nested loops around its body: +Calls, infix operators, and prefix operators all follow the same rule: +they transform the current per-iteration values of their range drivers. +They do not choose a special "base function" that owns the loop. -### Example: Simple Function Call +Examples: -```python +```pluto i = 0:5 x = Square(i) ``` -- Base function: `Square` (single arg is range value) -- Range `i` in args → loop inside Square's body +This evaluates `Square` for each yielded `i` value, then the root assignment +keeps the final result, so `x = 16`. -**Generated structure:** -``` -Square: - for i_val in 0:5: - x = i_val * i_val +```pluto +i = 0:5 +x = i + 1 ``` -### Example: Nested Single-Arg Functions +This yields `1, 2, 3, 4, 5` across the `i` stream and the root assignment keeps +the final value, so `x = 5`. -```python -x = √(Square(0:5)) +```pluto +i = 0:5 +x = √(i + 1) ``` -- Tree: `Sqrt(Square(range))` -- `Sqrt` has single arg `Square(...)` which is a function → descend -- `Square` has single arg `0:5` (value, not function) → **base is `Square`** +The infix expression first yields `1, 2, 3, 4, 5`, the prefix `√` is applied to +each yielded value, and the root assignment keeps the final result. -**Generated structure:** -``` -Square: - for i_val in 0:5: - tmp = i_val * i_val -Sqrt(tmp) # Applied to final result -``` +## Comparisons, Skip, And Fallback -### Example: Multiple Arguments +Comparisons in value position are filters, not booleans. -```python -x = f(0:3, 10:13) +```pluto +i > 2 ``` -- `f` has 2 args → **base is `f`** -- Both ranges become nested loops +This yields `i` when true and yields nothing when false. -**Generated structure:** -``` -f: - for tmp1 in 0:3: - for tmp2 in 10:13: - +`or` is a fallback on skip: + +```pluto +i > 2 or 0 ``` -### Example: Infix with Range +This yields `i` when the comparison succeeds, otherwise `0`. -```python -x = a + 0:5 -``` +## Array Literals -- Tree: `Add(a, range)` -- `Add` has 2 args → **base is `Add`** +`[]` always materializes an array at the point where it appears. -**Generated structure:** -``` -Add: - for i_val in 0:5: - x = a + i_val -``` +The collector owns its own iteration domain: -### Example: Prefix Applied to Infix +- It iterates every range used inside the literal. +- It does not leak those ranges upward into the parent expression. +- If an outer loop currently shadows the same name with a scalar iterator, + the collector reopens the original range driver instead of treating the + scalar shadow as a singleton. -```python -x = √(a + 0:5) +Example: + +```pluto +i = 0:5 +res = i + 1 + [i + 1] ``` -- Tree: `Sqrt(Add(a, range))` -- `Sqrt` has single arg `Add(...)` which is a function → descend -- `Add` has 2 args → **base is `Add`** +`[i + 1]` first materializes `[1 2 3 4 5]`. +The outer expression then iterates `i` and broadcasts over that array, giving +`[6 7 8 9 10]` as the final value. -**Generated structure:** -``` -Add: - for i_val in 0:5: - tmp = a + i_val -Sqrt(tmp) # Applied to final result +Likewise: + +```pluto +i = 0:5 +arr = [Square(i)] ``` ---- +collects the per-iteration results of `Square(i)` into `[0 1 4 9 16]`. -## Loop Generation Modes +## Zero-Fill Inside `[]` -When ranges expand, the behavior depends on the assignment operator: +Array literals preserve shape. +If a cell yields nothing, the collector inserts the zero value of the element +type at that position. -### Mode 1: Assignment (Last Value) +That applies to: -Simple assignment keeps the last iteration value: +- failed comparison cells +- out-of-bounds array access inside a cell -```python -i = 0:5 -x = i + 1 -``` +Examples: -**Generated code:** -```c -for (int64_t i_val = 0; i_val < 5; i_val++) { - x = i_val + 1; -} -// x = 5 (last value) +```pluto +i = 0:10 +[i > 2 < 8] ``` -### Mode 2: Compound Assignment (Accumulate) - -Compound operators accumulate across iterations: +produces: -```python -x = 0 -x += i +```pluto +[0 0 0 3 4 5 6 7 0 0 0] ``` -**Generated code:** -```c -for (int64_t i_val = 0; i_val < 5; i_val++) { - x = x + i_val; -} -// x = 0+1+2+3+4 = 10 -``` +and -### Mode 3: Array Literal (Collect) +```pluto +[i > 2 < 8 or 2] +``` -Wrapping in `[...]` collects values into an array: +produces: -```python -arr = [i * 2] +```pluto +[2 2 2 3 4 5 6 7 2 2 2] ``` -**Generated code:** -```c -arr = allocate_array(5); -for (int64_t i_val = 0; i_val < 5; i_val++) { - arr[i_val] = i_val * 2; -} -// arr = [0, 2, 4, 6, 8] -``` +`or` is resolved before the collector sees the final cell result, so explicit +fallback values win over zero-fill. ---- +## Gated Collection -## Multiple Ranges: Nested Loops +Statement conditions outside `[]` gate the active iteration domain. +They do not preserve shape. -When multiple ranges appear in the base function's arguments, they become **nested loops**: +Example: -```python -i = 0:2 -j = 0:3 -result = f(i, j) +```pluto +i = 0:10 +arr = i > 2, i < 8 [i] ``` -**Generated structure:** -``` -f: - for i_val in 0:2: - for j_val in 0:3: - -``` +produces: -This produces `2 × 3 = 6` iterations (Cartesian product). +```pluto +[3 4 5 6 7] +``` -### Same Range Variable = Single Loop (Zip) +The conditions select which outer iterations execute the collector at all. -If the same range variable appears multiple times, it's a single loop: +By contrast: -```python -i = 0:5 -result = i + i * 2 +```pluto +arr = [i > 2 < 8] ``` -**Generated code:** -```c -for (int64_t i_val = 0; i_val < 5; i_val++) { - result = i_val + i_val * 2; -} -``` +keeps the full array shape and zero-fills failed positions. ---- +## Statement Conditions And Tuples -## Conditional Assignment +Statement conditions are shared across the whole assignment. +They determine the admitted outer iteration domain for every output in the +statement. -You can add a conditional guard to selectively update values: +Sibling RHS expressions do not share their local value drivers with each +other. +Each RHS adds only the extra drivers mentioned inside that expression. -```python -res = condition expression -``` +Examples: -**Desugars to:** -```c -for each iteration: - if (condition): - res = expression +```pluto +i = 0:3 +j = 0:2 +x, y = i < 2 [1], j ``` -### Example: Maximum Value +The statement condition `i < 2` is shared. +`x` collects once for each admitted `i`, producing `[1 1]`. +`y` uses its own local `j` driver inside that shared gate and ends with the +final `j` value `1`. -```python -arr = [3 1 4 1 5] -i = 0:5 -res = arr[i] > res arr[i] -``` +Likewise: -**Generated code:** -```c -res = 0; -for (int64_t i_val = 0; i_val < 5; i_val++) { - if (arr[i_val] > res) { - res = arr[i_val]; - } -} -// res = 5 (maximum) +```pluto +i = 0:10 +j = 0:5 +x, y = i < 8, j > 2 i + 1, (i + j) < 10 ``` ---- +The outer gate is `i < 8, j > 2`, so both outputs run only on admitted +iterations. +Inside that shared gate: -## Array Indexing with Ranges +- `x` uses only `i + 1`, so it ends with `8` +- `y` applies its own value-position comparison and ends with `9` -Using a range to index an array creates a view or loop: +If a statement condition and an RHS expression mention the same driver name, +the statement condition opens that outer loop first. +Inside the RHS, the same name refers to the current scalar iterator value, not +to a fresh nested loop. -```python -arr = [10 20 30 40 50] -i = 0:3 -slice = arr[i] # View (ArrayRange type) -values = [arr[i]] # Collect to new array [10, 20, 30] -res += arr[i] # Sum: res = 10+20+30 = 60 -``` +For non-collector tuple outputs, one admitted statement iteration is still one +shared scalar update step. +If one non-collector RHS hits an out-of-bounds failure on that iteration, +sibling non-collector outputs keep their previous values for that same +iteration. +Top-level `[]` collectors still use their own local zero-fill rules for cells. ---- +## Nested Collectors -## Pass-by-Reference Semantics +Nested collectors materialize before the surrounding expression continues. -Function arguments are passed by reference. When the same variable is used for input and output, they alias: +Example: -```python -rebuilt = [1 2 3] -rebuilt = Rebuild(rebuilt, 10:13) # Input and output alias! +```pluto +i = 0:5 +j = 0:3 +Square([i][j]) ``` -**Aliased behavior:** Changes to output affect subsequent reads of input within the same iteration. - -**No aliasing (literal array):** -```python -result = Rebuild([1 2 3], 10:13) # Fresh array, no aliasing -``` +`[i]` first materializes `[0 1 2 3 4]`. +Then `[j]` indexes or slices that materialized array in the surrounding +expression context. ---- +This is a semantic materialization boundary. +The compiler may later hoist or fuse loops as an optimization, but that does +not change the language meaning. -## Functions with Range Parameters +## Scalar Contexts -When a function receives a range parameter, iteration happens **inside** the function body: +Outside `[]`, ranged expressions remain per-iteration values until the root +assignment or statement consumes them. -```python -res = AddMul(x, 0:5) - i = 10 - res = i * x + y # y iterates over 0:5 -``` +Examples: -**Generated structure:** -``` -AddMul: - for y_val in 0:5: - i = 10 - res = i * x + y_val +```pluto +i = 0:5 +x = i + 1 ``` -The function receives the range, and its body contains the loop. +`x` becomes `5`. ---- - -## Prefix Operators - -Prefix operators (`-`, `√`, etc.) are single-argument functions. The base function rule applies: - -```python -x = -(0:5) # Tree: Negate(range) → base is Negate -x = √(a + 0:5) # Tree: Sqrt(Add(a, range)) → descend to Add +```pluto +arr = [i + 1] ``` ---- +`arr` becomes `[1 2 3 4 5]`. -## Intermediate Values Become Scalars +## Singleton Arrays -When you assign a range expression to a variable, the loop executes immediately: +If no range drivers are open inside `[]`, the literal evaluates once and +produces a singleton array. -```python -i = 0:5 -x = i + 1 # Loop executes NOW: x = 5 (scalar) -y = i + 2 # Loop executes NOW: y = 7 (scalar) -res = x / y # No loop! Just: res = 5 / 7 -``` +Example: -**For iteration over both:** -```python -i = 0:5 -res = (i + 1) / (i + 2) # Single loop, both computed per iteration +```pluto +x = 7 +[x] ``` ---- - -## Summary - -| Concept | Rule | -|---------|------| -| **Base function** | Descend through single-arg function calls until multiple args or value arg | -| **Range expansion** | Nested loops inside base function's body | -| **Multiple ranges** | Nested loops (Cartesian product) | -| **Same variable** | Single loop (zip behavior) | -| **Assignment** | Last value kept | -| **Compound assignment** | Accumulate across iterations | -| **Array literal** | Collect all values | -| **Pass-by-reference** | Input/output can alias | - -**Core Design:** -- Every operation is a function -- Deterministic base function selection -- Ranges expand as loops inside base function -- No side effects in functions -- Simple, predictable semantics +produces `[7]`. + +This is not a special array-literal mode. +It is the same collector rule applied to an expression with no active drivers. diff --git a/tests/array/array_capture.exp b/tests/array/array_capture.exp index 847486dd..a2f630a3 100644 --- a/tests/array/array_capture.exp +++ b/tests/array/array_capture.exp @@ -1,6 +1,6 @@ CaptureLiteral: [0 1 2 3 4] LeftConcat: [0 1 2 3 4] -RightConcat: [4 3 2 1 0] +RightConcat: [0 1 2 3 4] LeftConcatExtra: [0 1 2 3 4] ScaledCapture: [0 2.3 4.6 6.9 9.2] ScaledWithoutSpace: [3 12] diff --git a/tests/array/array_expr.exp b/tests/array/array_expr.exp index 5c197cbb..d1b3d4f1 100644 --- a/tests/array/array_expr.exp +++ b/tests/array/array_expr.exp @@ -6,6 +6,7 @@ Negation of array: [-1 2 -3] Float Array + Scalar: [2 3 4] Float Array + Int Scalar: [3.25 4.75 5.25] Array + Range: [13 14 15] +Range + Collected Range: [6 7 8 9 10] Range + Array: [8 9 10] Array + (Range * Scalar): [25 35 45] (Array + Scalar) * Range: [44 84 124] diff --git a/tests/array/array_expr.spt b/tests/array/array_expr.spt index 8abba020..f2aac8f2 100644 --- a/tests/array/array_expr.spt +++ b/tests/array/array_expr.spt @@ -36,6 +36,11 @@ arr3 = [1 2 3] res7 = arr3 + (10:13) "Array + Range: -res7" +# Nested collection materializes before the outer ranged expression is finalized +i = 0:5 +res7b = i + 1 + [i + 1] +"Range + Collected Range: -res7b" + # Range + Array res8 = (5:8) + arr3 "Range + Array: -res8" diff --git a/tests/array/array_func.exp b/tests/array/array_func.exp index 8d0beec8..4293013d 100644 --- a/tests/array/array_func.exp +++ b/tests/array/array_func.exp @@ -12,7 +12,8 @@ SquareScalar: 49 DirectConcatScalar: [0] DirectConcatRange: [0 1 2 3 4] SquareVecRange: [0 1 4 9 16] -SquareInline: [16] +SquareInline: [0 1 4 9 16] +SquareSameDriver: [4 5 8 13 20] RebuildAssign: [26] RebuildNoAlias: [14] RebuildAssign2: [2] diff --git a/tests/array/array_func.spt b/tests/array/array_func.spt index 7f189728..b7d95c81 100644 --- a/tests/array/array_func.spt +++ b/tests/array/array_func.spt @@ -38,6 +38,11 @@ squareVec = [Square(0:5)] squareInline = Square([0:5]) "SquareInline: -squareInline" +# Same driver name outside and inside [] should still recollect the full range +i = 0:5 +squareSameDriver = i + Square([i]) +"SquareSameDriver: -squareSameDriver" + # Ensure function-internal array assignment builds a fresh array (no accumulation on param) rebuilt = [1 2 3] rebuilt = Rebuild(rebuilt, 10:13) diff --git a/tests/array/array_range.exp b/tests/array/array_range.exp index ead9e0cc..b9c6439c 100644 --- a/tests/array/array_range.exp +++ b/tests/array/array_range.exp @@ -9,7 +9,7 @@ NestedOp: 103 DirectLiteral: [5 6 7 8][0:3] IdentRange: [10 20 30 40 50][0:5] PrefixRange: -40 -LiteralIterChain: [1 1 1 1 1 1] +LiteralIterChain: [1 1 0 0 0 0] CallRangeRoot: 100 CallRootVec: [20 40 60 80 100] CallRangeInfix: 104 diff --git a/tests/array/array_range.spt b/tests/array/array_range.spt index a8ce4b8a..96c96fe4 100644 --- a/tests/array/array_range.spt +++ b/tests/array/array_range.spt @@ -35,6 +35,8 @@ pref = -arr[1:4] "PrefixRange: -pref" chain = [1] +# Each [chain[idx]] recollects against the current chain value and zero-fills +# out-of-bounds indexes instead of reusing the outer idx loop as a singleton. chain = chain ⊕ [chain[idx]] "LiteralIterChain: -chain" diff --git a/tests/array/array_scalar_assign.exp b/tests/array/array_scalar_assign.exp index e3b1158e..e866837b 100644 --- a/tests/array/array_scalar_assign.exp +++ b/tests/array/array_scalar_assign.exp @@ -1,3 +1,3 @@ Result: [1] -ArrayAdd: [6] +ArrayAdd: [1 2 3 4 5 6] ArraySetAdd: [4 5 6] diff --git a/tests/array/cond_accum.exp b/tests/array/cond_accum.exp index e08136f7..4980b4e7 100644 --- a/tests/array/cond_accum.exp +++ b/tests/array/cond_accum.exp @@ -38,7 +38,12 @@ SeedLit: 88 77 StrView: c [a b] -TupleValueRangesA: [0 1 0 1]. TupleValueRangesB: [0 0 1 1] +TupleValueRangesA: [0 1 0 1]. TupleValueRangesB: [0 1] +CondLocalDriversA: [1 1]. CondLocalDriversB: 1 +CondSharedReuse: [0 0 1 1 2 2] +SharedTupleCondX: 8. SharedTupleCondY: 9 +SharedBoundsTupleX: 20. SharedBoundsTupleY: 101 +SharedBoundsFlipX: 101. SharedBoundsFlipY: 20 MixedLastWinsA: [0 1 2]. MixedLastWinsB: 12 MixedCondExprA: [0 1 2]. MixedCondExprB: 1 NeverCondExprA: [0 1 2]. NeverCondExprB: 0 @@ -48,4 +53,5 @@ InterleavedAccumX: [10 1 20 20 1 30 30 1 40 40 1 0]. InterleavedAccumY: 3. Inter CondLastWins: 2 12 RangePrefixRewrite: -2 +SameDriverCondNested: [100 101 102 103 104] [9 9] diff --git a/tests/array/cond_accum.spt b/tests/array/cond_accum.spt index acc4cf9e..d1fb4c19 100644 --- a/tests/array/cond_accum.spt +++ b/tests/array/cond_accum.spt @@ -113,7 +113,8 @@ i = 0:5 a, b, c = i < 3 [i], [i * 10], [5.2i] a, b, c -# Multi-output with OOB literal cells zero-filled in sync +# Tuple collectors are independent: OOB in one collector zero-fills there +# without suppressing sibling collectors on the same iteration. data = [10 20] i = 0:4 a, b = i < 3 [data[i]], [i] @@ -215,11 +216,45 @@ i = 0:3 stracc = i < 2 [sarr2[i]] stracc -# Tuple accumulation with mixed value-level ranges +# Tuple accumulation with mixed value-level ranges keeps each [] collector local. i = 0:3 tc, td = i < 2 [0:2], [i] "TupleValueRangesA: -tc. TupleValueRangesB: -td" +# Statement conditions are shared across tuple outputs, but sibling RHS +# drivers stay local to the expression that mentions them. +i = 0:3 +j = 0:2 +localLeakA, localLeakB = i < 2 [1], j +"CondLocalDriversA: -localLeakA. CondLocalDriversB: -localLeakB" + +# When a statement condition and an RHS mention the same driver, the condition +# opens the outer loop and the RHS sees the current scalar iterator value. +i = 0:4 +j = 0:4 +sharedReuse = i < 3, j > 1 [i] +"CondSharedReuse: -sharedReuse" + +# Statement-level conditions gate the whole tuple; value-position comparisons +# still apply only to the expression that contains them. +i = 0:10 +j = 0:5 +sharedTupleX, sharedTupleY = i < 8, j > 2 i + 1, (i + j) < 10 +"SharedTupleCondX: -sharedTupleX. SharedTupleCondY: -sharedTupleY" + +# Non-collector tuple outputs still share one admitted statement iteration: +# an OOB scalar/indexed RHS keeps sibling scalar outputs at their prior value. +data = [10 20] +i = 0:4 +sharedBoundsX, sharedBoundsY = i < 4 data[i], i + 100 +"SharedBoundsTupleX: -sharedBoundsX. SharedBoundsTupleY: -sharedBoundsY" + +# The same shared-bounds rule must hold regardless of tuple order. +data = [10 20] +i = 0:4 +sharedBoundsFlipX, sharedBoundsFlipY = i < 4 i + 100, data[i] +"SharedBoundsFlipX: -sharedBoundsFlipX. SharedBoundsFlipY: -sharedBoundsFlipY" + # Mixed tuple: array literal accumulates while scalar keeps last true value i = 0:4 ma, mb = i < 3 [i], i + 10 @@ -270,6 +305,11 @@ i = 0:3 pl = i < 2 -(0:3) "RangePrefixRewrite: -pl" +# Nested [] under a ranged condition still reopens the full driver range +i = 0:5 +sameCondNested = i < 2 [i] + 100 +"SameDriverCondNested: -sameCondNested" + # Existing array replaced on successful accumulation (leak check) seed = [1 2 3] p = 0:3 diff --git a/tests/math/func_nested_range.exp b/tests/math/func_nested_range.exp index e48ffa22..c12a0d6b 100644 --- a/tests/math/func_nested_range.exp +++ b/tests/math/func_nested_range.exp @@ -1,4 +1,4 @@ -Square([i][j]): 16 -SumSquares([i][j], [i][k]): 0 -SumSquares(arr[m], stored[n+0]): 0 +Square([i][j]): 4 +SumSquares([i][j], [i][k]): 13 +SumSquares(arr[m], stored[n+0]): 909 SumSquares(arr[m], arr2[n]): 909 diff --git a/tests/math/func_nested_range.spt b/tests/math/func_nested_range.spt index 2eb0cbb0..863e2a95 100644 --- a/tests/math/func_nested_range.spt +++ b/tests/math/func_nested_range.spt @@ -1,32 +1,28 @@ -# Test nested range: [i][j] iterates both i and j as outer loops +# Nested collectors materialize their own range domain before outer indexing. i = 0:5 j = 0:3 -# Nested loops: for i in 0:5, for j in 0:3 -# [i] is [current_i] (single element), [i][j] accesses index j -# Only j=0 is valid, j>0 is OOB (returns 0) -# Last iteration: i=4, j=2 -> [4][2] = 0 (OOB) -> Square(0) = 0 +# [i] materializes to [0 1 2 3 4], then [j] slices/indexes that array-range. +# Last iteration for j is 2, so Square([0 1 2]) keeps the last value 4. nested = Square([i][j]) "Square([i][j]): -nested" -# Similar with SumSquares - both args are [i][j] style +# Similar with SumSquares - both collectors materialize first, then the views iterate. k = 1:4 -# Last iteration: i=4, j=2, k=3 -# [4][2] = 0 (OOB), [4][3] = 0 (OOB) -# SumSquares(0, 0) = 0 + 0 = 0 +# Last values are 2 and 3, so SumSquares = 2^2 + 3^2 = 13. sumNested = SumSquares([i][j], [i][k]) "SumSquares([i][j], [i][k]): -sumNested" -# Mix: first arg is bare range arr[m], second is stored array with expr index +# Mix: first arg is bare range arr[m], second recollects [i] before indexing. arr = [10 20 30 40 50] m = 0:3 n = 1:4 arr2 = [i] -# arr[m] is ArrayRange (loop inside), [i][n+0] loops outside giving 0 -# SumSquares(30, 3) = 900 + 0 = 900 +# arr[m] is ArrayRange (loop inside), [i][n+0] materializes [0 1 2 3 4] then indexes it. +# SumSquares(30, 3) = 900 + 9 = 909 mixedNested = SumSquares(arr[m], [i][n + 0]) "SumSquares(arr[m], stored[n+0]): -mixedNested" -# Below function should loop inside as both array ranges are direct +# Stored top-level array materialization stays unchanged here. mixedNested2 = SumSquares(arr[m], arr2[n]) "SumSquares(arr[m], arr2[n]): -mixedNested2" diff --git a/tests/math/range_prefix.exp b/tests/math/range_prefix.exp index 8a2ceeca..1d661720 100644 --- a/tests/math/range_prefix.exp +++ b/tests/math/range_prefix.exp @@ -8,5 +8,5 @@ 1 -8 2 -[4] +[0 1 2 3 4] 7 diff --git a/tests/math/range_prefix.spt b/tests/math/range_prefix.spt index c9027398..ffbfaf1b 100644 --- a/tests/math/range_prefix.spt +++ b/tests/math/range_prefix.spt @@ -38,12 +38,13 @@ j = 5:7 x = (-i) + (-j) x -# Prefix behaves like functions (iterate inside prefix) +# Prefix still iterates its direct range operand. i = 0:5 root_last = √i root_last i = 0:5 +# Collector-local [] materializes before the prefix sees the value. roots = √[i * i] roots diff --git a/tests/print_range.exp b/tests/print_range.exp index 3d3b97dc..0d10f2a6 100644 --- a/tests/print_range.exp +++ b/tests/print_range.exp @@ -16,3 +16,6 @@ 1 0 2 1 3 4 +10 [0 1 2] +11 [0 1 2] +12 [0 1 2] diff --git a/tests/print_range.spt b/tests/print_range.spt index 8ad3bbd2..67975be0 100644 --- a/tests/print_range.spt +++ b/tests/print_range.spt @@ -21,3 +21,7 @@ j, j + 10 # Multiple expressions with same range k = 0:3 k + 1, k * k + +# Same driver used in an iterating expression and inside [] should still print the fully collected array +i = 0:3 +i + 10, [i] diff --git a/tests/range_shadow.exp b/tests/range_shadow.exp index d179638a..a61f3466 100644 --- a/tests/range_shadow.exp +++ b/tests/range_shadow.exp @@ -3,4 +3,4 @@ AddMul res is: 1234 AddMul res with negative step is: 542 AddMul res2 with array range arr[i] is: 123450 AddMul res2 with array range negative step arr[j] is: 2030 -PendingRanges: [160 150 130 99 57 4] +PendingRanges: [160 150 151 152 153 154 129 130 131 132 133 97 98 99 100 101 54 55 56 57 58 0 1 2 3 4]