diff --git a/docs/language-spec.md b/docs/language-spec.md index e7adc63..20b3ead 100644 --- a/docs/language-spec.md +++ b/docs/language-spec.md @@ -91,6 +91,44 @@ must be ordered, within the byte length, and on UTF-8 codepoint boundaries. Invalid bounds or boundaries trap at runtime. The owner remains responsible for backing storage and is dropped exactly once. +## Iteration + +`for item in source` supports built-in ranges and sequences. Their source is +evaluated once, including when a call produces a sequence. + +Custom iteration uses an explicit call returning `?T`: + +```peep +fn Produce(counter: &mut Counter, limit: i32) -> ?i32 { + if counter.value >= limit { return none; } + let value = counter.value; + counter.value = counter.value + 1; + return value; +} + +for item in Produce(&mut counter, 10) { + println(item); +} +``` + +Every attempt evaluates the entire call, including its callee, receiver, and +arguments, using ordinary call evaluation order. A present result binds an item +of type `T`; `none` terminates the loop. The terminating attempt also evaluates +all arguments. `continue` starts another attempt; `break` and `return` do not. +No source or argument is implicitly captured for the lifetime of the loop. + +Free functions, explicit method calls of any name (for example +`counter.Take(10)`), and pipe calls (`counter |> Produce(10)`) use canonical call +checking. There is no implicit `Next` method protocol or added runtime interface +protocol. Explicit calls retain ordinary dispatch, argument, move, borrow, +reference-provenance, effect, and cleanup semantics. + +Custom loops provide one item binding, not an index. Maintain a separate counter +when needed. Bare optional values, function values without a call, and objects +with a `Next` method are not producers. Write the call explicitly. Optionals are +idempotent (`??T` is `?T`), so an optional result does not encode a separate +optional item layer. + ## Generic Named Types Structs, enums, interfaces, and transparent type aliases may declare type @@ -116,16 +154,19 @@ and monomorphization are not part of current language surface. ## Optional Values And Flow Narrowing `?T` contains either one `T` value or `none`. `none` is valid only where an -optional type is expected. A `T` value promotes to `?T`; this permits one-layer -promotion such as `?T` to `??T` when the outer optional is expected. Assigning +optional type is expected. A `T` value promotes to `?T`. Optionals are +idempotent: `??T` and `? ?T` mean `?T`, with no extra absence state. Explicit +syntactic nesting emits informational diagnostic `S0006`, asking to remove each +redundant `?`. Nesting revealed through aliases, generic substitution, or +wrapping an optional function result silently canonicalizes to `?T`. Assigning or passing a whole optional to an explicit optional destination preserves its carrier type instead of reading its payload. Comparing a stable optional place with `none` establishes presence on one CFG edge. `x != none` proves presence on the true edge; `x == none` proves presence on the false edge. Reversed operands have identical meaning. Each proof unwraps -one optional layer, so nested optionals require one proof per layer. A proven -ordinary value use has payload type `T`; an unproven use retains `?T` and cannot +the optional carrier; redundant optional markers do not require extra proofs. +A proven ordinary value use has payload type `T`; an unproven use retains `?T` and cannot stand in for `T`. Stable places are variables, field and nested-field projections, constant-folded diff --git a/internal/diagnostics/codes.go b/internal/diagnostics/codes.go index 2bf5df3..e9a3849 100644 --- a/internal/diagnostics/codes.go +++ b/internal/diagnostics/codes.go @@ -95,6 +95,7 @@ const ( InfoRedundantComma = "S0003" InfoRedundantPreludeImport = "S0004" InfoRedundantGlobalQualifier = "S0005" + InfoRedundantOptional = "S0006" // Warnings (W prefix) WarnUnreachableCode = "W0001" diff --git a/internal/frontend/ast/clone.go b/internal/frontend/ast/clone.go index a8793ed..1a2365d 100644 --- a/internal/frontend/ast/clone.go +++ b/internal/frontend/ast/clone.go @@ -4,6 +4,12 @@ import "sync/atomic" var nextSyntheticNodeID atomic.Uint32 +// NewSyntheticNodeID shares one identity space across checked expansions and +// default-argument clones, disjoint from parser-assigned nodes. +func NewSyntheticNodeID() NodeID { + return NodeID(nextSyntheticNodeID.Add(1) | (1 << 31)) +} + // SubstituteExpr clones an expression for call-site expansion. Parameter // identifiers are replaced with their already-evaluated argument expressions; // every cloned node gets a separate high-range ID so semantic caches cannot @@ -18,11 +24,8 @@ func SubstituteExpr(expr Expr, substitutions map[string]Expr) (cloned Expr, defa } defaultClones = make(map[NodeID]NodeID) argumentClones = make(map[NodeID]NodeID) - cloneID := func() NodeID { - return NodeID(nextSyntheticNodeID.Add(1) | (1 << 31)) - } newID := func(original NodeID, fromArgument bool) NodeID { - id := cloneID() + id := NewSyntheticNodeID() if fromArgument { argumentClones[id] = original } else { diff --git a/internal/frontend/parser/parse_types.go b/internal/frontend/parser/parse_types.go index ad2ee7f..804e2a0 100644 --- a/internal/frontend/parser/parse_types.go +++ b/internal/frontend/parser/parse_types.go @@ -20,7 +20,7 @@ func (p *Parser) parseTypeExpr() ast.TypeExpr { switch tok.Kind { case token.AMP: return p.parseRefTypeExpr() - case token.QUESTION: + case token.QUESTION, token.QQ: return p.parseOptionalTypeExpr() case token.ASTERISK: return p.parseOwnedPtrTypeExpr() @@ -147,18 +147,35 @@ func (p *Parser) parseRefTypeExpr() ast.TypeExpr { } func (p *Parser) parseOptionalTypeExpr() ast.TypeExpr { - start := p.consume(token.QUESTION, "expected '?' in optional type") - if start == nil { - return nil + markers := make([]source.Position, 0, 2) + for p.at(token.QUESTION) || p.at(token.QQ) { + marker := p.advance() + markers = append(markers, marker.Start) + if marker.Kind == token.QQ { + second := marker.Start + second.Advance("?") + markers = append(markers, second) + } } inner := p.parseTypeExpr() if inner == nil { return nil } - return reg(p, &ast.OptionalType{ - Inner: inner, - Location: source.NewLocation(p.filePath, start.Start, ast.EndOf(inner)), - }) + for index := len(markers) - 1; index >= 0; index-- { + inner = reg(p, &ast.OptionalType{ + Inner: inner, + Location: source.NewLocation(p.filePath, markers[index], ast.EndOf(inner)), + }) + } + for _, start := range markers[:len(markers)-1] { + end := start + end.Advance("?") + p.diag.Add(diagnostics.NewInfo("redundant optional marker"). + WithCode(diagnostics.InfoRedundantOptional). + WithPrimaryLabel(source.NewLocation(p.filePath, start, end), "remove redundant `?`"). + WithNote("nested optional types are the same as a single optional type")) + } + return inner } func (p *Parser) parseOwnedPtrTypeExpr() ast.TypeExpr { diff --git a/internal/frontend/parser/parser_test.go b/internal/frontend/parser/parser_test.go index 4d4a9f3..9b096e8 100644 --- a/internal/frontend/parser/parser_test.go +++ b/internal/frontend/parser/parser_test.go @@ -2,6 +2,7 @@ package parser import ( "fmt" + "reflect" "strings" "testing" @@ -2640,3 +2641,95 @@ func TestEmitterNewFormatNoSeverityPrefix(t *testing.T) { t.Fatalf("expected location in output:\n%s", out) } } + +func TestParseOptionalMarkersPreservesSyntaxAndTokens(t *testing.T) { + for _, spelling := range []string{"?i32", "??i32", "???i32", "????i32", "? ?i32", "?? ?i32"} { + t.Run(spelling, func(t *testing.T) { + src := "fn value() -> " + spelling + " { return none; }" + diag := diagnostics.NewDiagnosticBag() + stream := lexer.New("test.peep", src, diag).Tokenize() + original := append(stream[:0:0], stream...) + mod := New("test.peep", stream, diag).ParseModule() + if diag.HasErrors() { + t.Fatalf("unexpected parser errors: %s", diag.EmitAllToString()) + } + if !reflect.DeepEqual(stream, original) { + t.Fatal("optional parsing mutated lexer tokens") + } + typ := mod.Stmts[0].(*ast.FnDecl).ReturnType + for index, char := range src { + if char != '?' { + continue + } + optional, ok := typ.(*ast.OptionalType) + if !ok || ast.StartOf(optional).Index != index { + t.Fatalf("missing source optional at %d: %#v", index, typ) + } + typ = optional.Inner + } + if named, ok := typ.(*ast.NamedType); !ok || named.Name != "i32" { + t.Fatalf("unexpected payload syntax: %#v", typ) + } + }) + } +} + +func TestParseRedundantOptionalSyntax(t *testing.T) { + for _, test := range []struct { + name string + source string + notes int + }{ + {"double", "type Value = ??i32;", 1}, + {"triple", "type Value = ???i32;", 2}, + {"spaced", "type Value = ? ?i32;", 1}, + {"spaced triple", "type Value = ? ? ?i32;", 2}, + {"single", "type Value = ?i32;", 0}, + {"alias", "type Maybe = ?i32; type Value = ?Maybe;", 0}, + {"forward alias", "type Value = ?Maybe; type Maybe = ?i32;", 0}, + {"generic", "type Maybe = ?T; type Value = Maybe; type Again = ?Value;", 0}, + {"generic source once", "type Maybe = ??T; type A = Maybe; type B = Maybe;", 1}, + {"array boundary", "type Value = ?[2]?i32;", 0}, + {"reference boundary", "type Value = ?&?i32;", 0}, + {"field", "struct Box { value: ??i32 }", 1}, + {"parameter and return", "fn Read(value: ??i32) -> ? ?i32 { return value; }", 2}, + {"local", "fn main() { let value: ???i32 = none; }", 2}, + {"function type", "type Callback = fn(value: ??i32) -> ??i32;", 2}, + {"coalescing", "fn Read(value: ?i32) { let result = value ?? 7; }", 0}, + } { + t.Run(test.name, func(t *testing.T) { + _, diag := parseTestModule(test.source) + if diag.HasErrors() != (test.name == "coalescing") { + t.Fatalf("unexpected error state:\n%s", diag.EmitAllToString()) + } + notes := 0 + positions := make(map[int]bool) + for _, item := range diag.Diagnostics() { + if item.Code != diagnostics.InfoRedundantOptional { + continue + } + notes++ + if item.Severity != diagnostics.Info { + t.Fatalf("severity = %v, want info", item.Severity) + } + if len(item.Labels) != 1 || !strings.Contains(item.Labels[0].Message, "remove redundant `?`") { + t.Fatalf("missing removal advice: %#v", item) + } + loc := item.Labels[0].Location + if loc == nil || loc.Start == nil || loc.End == nil { + t.Fatal("missing source span") + } + if loc.End.Index != loc.Start.Index+1 || test.source[loc.Start.Index:loc.End.Index] != "?" { + t.Fatalf("span must select one redundant question mark: %v", loc) + } + if positions[loc.Start.Index] { + t.Fatalf("duplicate diagnostic at %v", loc) + } + positions[loc.Start.Index] = true + } + if notes != test.notes { + t.Fatalf("notes = %d, want %d:\n%s", notes, test.notes, diag.EmitAllToString()) + } + }) + } +} diff --git a/internal/ir/cfg/build.go b/internal/ir/cfg/build.go index 2d6b184..ca34384 100644 --- a/internal/ir/cfg/build.go +++ b/internal/ir/cfg/build.go @@ -34,6 +34,7 @@ type LoopEntryQuery func(ast.NodeID) bool type BuildQueries struct { MatchCases MatchCaseQuery LoopGuaranteedEntry LoopEntryQuery + CheckedIterations map[ast.NodeID]*ast.BlockStmt } // BuildModule creates immutable control-flow topology from typed source syntax. @@ -184,6 +185,11 @@ func (b *builder) buildStmt(stmt ast.Stmt, current *Block, scopeID ir.NodeID) *B } return join case *ast.ForStmt: + if node.Iterable != nil { + if checked := b.queries.CheckedIterations[node.ID()]; checked != nil { + return b.buildStmt(checked, current, scopeID) + } + } loopID := ir.NodeID(node.ID()) init := b.newBlock(BlockLoopInit, ast.LocOf(node)) bodyBlock := b.newBlock(BlockLoopBody, ast.LocOf(node)) diff --git a/internal/ir/hir/lower/lower_types.go b/internal/ir/hir/lower/lower_types.go index e362c80..0c9ace9 100644 --- a/internal/ir/hir/lower/lower_types.go +++ b/internal/ir/hir/lower/lower_types.go @@ -322,7 +322,7 @@ func loweredRuntimeType(module *project.Module, t typeinfo.Type, seen map[*typei if typ == nil { return nil } - return &typeinfo.OptionalType{Inner: loweredRuntimeType(module, typ.Inner, seen)} + return typeinfo.NewOptional(loweredRuntimeType(module, typ.Inner, seen)) case *typeinfo.ArrayType: if typ == nil { return nil diff --git a/internal/ir/hir/lower/module_lower.go b/internal/ir/hir/lower/module_lower.go index 74a96d6..7315eb6 100644 --- a/internal/ir/hir/lower/module_lower.go +++ b/internal/ir/hir/lower/module_lower.go @@ -236,6 +236,12 @@ func appendStmt(module *project.Module, scope *symbols.Scope, out *hir.Block, st } out.Stmts = append(out.Stmts, ifStmt) case *ast.ForStmt: + if node.Iterable != nil { + if checked := module.Typechecking.CheckedIterations[node.ID()]; checked != nil { + appendStmt(module, scope, out, checked, returnType, ctx) + return + } + } out.Stmts = append(out.Stmts, lowerForStmt(ctx, module, scope, node, returnType)) case *ast.MatchStmt: evidence, found := module.Typechecking.Matches[node.ID()] diff --git a/internal/ir/hir/lower/module_lower_test.go b/internal/ir/hir/lower/module_lower_test.go index bac4b41..165b94b 100644 --- a/internal/ir/hir/lower/module_lower_test.go +++ b/internal/ir/hir/lower/module_lower_test.go @@ -48,6 +48,7 @@ func generateTestHIR(t *testing.T, filePath, importPath, src string, beforeLower module.CFG = cfg.BuildModule(module.AST, cfg.BuildQueries{ MatchCases: module.Typechecking.MatchCases, LoopGuaranteedEntry: module.Typechecking.ForLoopGuaranteedEntry, + CheckedIterations: module.Typechecking.CheckedIterations, }) module.Flow = typechecker.CheckFlow(ctx, module) if diag.HasErrors() { @@ -60,6 +61,71 @@ func generateTestHIR(t *testing.T, filePath, importPath, src string, beforeLower return out } +func TestGenerateHIRConsumesCallIteratorEvidence(t *testing.T) { + out := generateTestHIR(t, "hir_iterator_test"+peeper.SourceExt, "hir_iterator_test", `struct Cursor {} +fn (self: &mut Cursor) Next() -> ?i32 { return none; } +fn main() { + let mut cursor = Cursor.{}; + for item in cursor.Next() { if item == 1 { continue; } } +}`, func(module *project.Module) { module.Bindings.MethodsByReceiver = nil }) + var loop *hir.For + for _, fn := range out.Funcs { + for _, stmt := range fn.Body.Stmts { + if expansion, ok := stmt.(*hir.Block); ok { + loop, _ = expansion.Stmts[len(expansion.Stmts)-1].(*hir.For) + } + } + } + if loop == nil || loop.Init != nil || loop.Cond != nil || loop.Next != nil || loop.Bindings != nil { + t.Fatalf("custom loop unexpectedly has numeric segments: %#v", loop) + } + result := loop.Body.Stmts[0].(*hir.Binding) + if _, ok := result.Value.(*ir.Call); !ok { + t.Fatalf("advancement = %T, want static call", result.Value) + } + if _, ok := loop.Body.Stmts[1].(*hir.If); !ok { + t.Fatalf("missing optional exhaustion branch: %#v", loop.Body.Stmts) + } +} + +func TestGenerateHIRCallIteratorUsesOrdinaryInterfaceDispatch(t *testing.T) { + out := generateTestHIR(t, "hir_iterator_interface_test"+peeper.SourceExt, "hir_iterator_interface_test", `iface Producer { fn (&mut Self) Take(value: i32) -> ?i32 } +fn Iterate(producer: &mut Producer) { for item in producer.Take(1) {} }`) + fn := out.Funcs[len(out.Funcs)-1] + expansion := fn.Body.Stmts[0].(*hir.Block) + loop := expansion.Stmts[0].(*hir.For) + call := loop.Body.Stmts[0].(*hir.Binding).Value + if _, ok := call.(*ir.InterfaceCall); !ok { + t.Fatalf("explicit method call = %T, want ordinary interface dispatch", call) + } +} + +func TestGenerateHIRKeepsIteratorArgumentsInsideLoop(t *testing.T) { + for _, call := range []string{"Produce(Value())", "Value() |> Produce()"} { + t.Run(call, func(t *testing.T) { + out := generateTestHIR(t, "hir_iterator_arguments_test"+peeper.SourceExt, "hir_iterator_arguments_test", `fn Value() -> i32 { return 1; } +fn Produce(value: i32) -> ?i32 { return none; } +fn main() { for item in `+call+` {} }`) + main := out.Funcs[len(out.Funcs)-1] + if len(main.Body.Stmts) != 1 { + t.Fatalf("producer escaped loop: %#v", main.Body.Stmts) + } + expansion := main.Body.Stmts[0].(*hir.Block) + if len(expansion.Stmts) != 1 { + t.Fatalf("argument captured before loop: %#v", expansion.Stmts) + } + loop := expansion.Stmts[0].(*hir.For) + producer := loop.Body.Stmts[0].(*hir.Binding).Value.(*ir.Call) + if len(producer.Args) != 1 { + t.Fatalf("producer arguments = %#v", producer.Args) + } + if _, ok := producer.Args[0].(*ir.Call); !ok { + t.Fatalf("argument evaluation = %T, want nested call", producer.Args[0]) + } + }) + } +} + func TestGenerateHIRLowersRangeForIntoStructuredSegments(t *testing.T) { out := generateTestHIR(t, "hir_for_range_test"+peeper.SourceExt, "hir_for_range_test", `fn main() { for index, value in 1i64..3i64 {} @@ -481,18 +547,19 @@ func TestGenerateHIRPreservesExplicitOptionalCarrierInsideProof(t *testing.T) { } } -func TestGenerateHIRPromotesOneOptionalLayer(t *testing.T) { - out := generateTestHIR(t, "hir_optional_promotion_test"+peeper.SourceExt, "hir_optional_promotion_test", `fn promote(inner: ?i32) -> ? ?i32 { +func TestGenerateHIRPreservesRedundantOptionalCarrierIdentity(t *testing.T) { + out := generateTestHIR(t, "hir_optional_identity_test"+peeper.SourceExt, "hir_optional_identity_test", `fn keep(inner: ?i32) -> ? ?i32 { return inner; }`) - ret := out.Funcs[0].Body.Stmts[0].(*hir.Return) - outer, ok := ret.Value.(*ir.VariantMake) - if !ok || out.Types.Text(outer.TypeID()) != "??i32" { - t.Fatalf("promotion = %#v, want outer ??i32 VariantMake", ret.Value) - } - payload, ok := outer.Payload.(*ir.Ident) - if !ok || out.Types.Text(payload.TypeID()) != "?i32" { - t.Fatalf("promotion payload = %#v, want intact ?i32 carrier", outer.Payload) + fn := out.Funcs[0] + ret := fn.Body.Stmts[0].(*hir.Return) + carrier, ok := ret.Value.(*ir.Ident) + if !ok || carrier.SymbolID != fn.Params[0].SymbolID { + t.Fatalf("return = %#v, want unchanged parameter carrier, not VariantMake", ret.Value) + } + if carrier.TypeID() != fn.Params[0].Type || carrier.TypeID() != fn.ReturnType || out.Types.Text(carrier.TypeID()) != "?i32" { + t.Fatalf("carrier type = %s (%d), parameter = %d, return = %d; want one shared ?i32 type", + out.Types.Text(carrier.TypeID()), carrier.TypeID(), fn.Params[0].Type, fn.ReturnType) } } diff --git a/internal/lsp/server_test.go b/internal/lsp/server_test.go index 1bace6c..5931eb5 100644 --- a/internal/lsp/server_test.go +++ b/internal/lsp/server_test.go @@ -97,7 +97,8 @@ func runTimedLSPChanges(t *testing.T, root, filePath, initial string, changes [] }, ContentChanges: []TextDocumentContentChangeEvent{{Text: text}}, }) - time.Sleep(diagnosticsDebounceDelay + 25*time.Millisecond) + // Race-instrumented compilation can outlast the debounce interval. + time.Sleep(diagnosticsDebounceDelay + 250*time.Millisecond) } if err := inputWriter.Close(); err != nil { t.Fatalf("close LSP input: %v", err) @@ -493,6 +494,24 @@ fn inspect(value: result::Alias) { } } +func TestHoverShowsCallIteratorItemType(t *testing.T) { + root := t.TempDir() + filePath := filepath.Join(root, "main"+peeper.SourceExt) + src := `struct Item { value: i32 } +struct Cursor {} +fn (self: &Cursor) Next() -> ?Item { return none; } +fn main() { + let cursor = Cursor.{}; + for item in cursor.Next() { return __CURSOR__item.value; } +}` + state := NewServerState() + state.RootDir = root + hover := hoverAtSource(t, state, filePath, src) + if hover == nil || !strings.Contains(hover.Contents.Value, "(var) item: Item") { + t.Fatalf("iterator item hover = %#v, want Item", hover) + } +} + func TestHoverShowsExactCaseFieldType(t *testing.T) { root := t.TempDir() filePath := filepath.Join(root, "main"+peeper.SourceExt) diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 9bfe49c..ac9e527 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -438,6 +438,7 @@ func advanceModulePhase(ctx *project.CompilerContext, module *project.Module, di module.CFG = cfg.BuildModule(module.AST, cfg.BuildQueries{ MatchCases: module.Typechecking.MatchCases, LoopGuaranteedEntry: module.Typechecking.ForLoopGuaranteedEntry, + CheckedIterations: module.Typechecking.CheckedIterations, }) // Structure is checkable regardless of source validity: CFG construction // promises the same topology for a program that will not compile, and a diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index 49f2008..b72a9fd 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -2604,8 +2604,9 @@ fn main() -> i32 { func TestPipelineAcceptsOptionalNarrowingAcrossCFGAndStablePlaces(t *testing.T) { tests := []struct { - name string - src string + name string + src string + redundantInfo bool }{ { name: "polarity and reversed operands", @@ -2668,31 +2669,33 @@ fn fields(outer: Outer, holder: Holder, index: usize) -> i32 { }`, }, { - name: "nested optional proofs", + name: "redundant optional needs one proof", + redundantInfo: true, src: `fn nested(value: ? ?i32) -> i32 { if value != none { - if value != none { - return value; - } + return value; } return 0; }`, }, { - name: "nested optional test in eager boolean", + name: "redundant optional test in eager boolean", + redundantInfo: true, src: `fn nested(value: ? ?i32, enabled: bool) -> bool { return value != none && enabled; }`, }, { name: "nested inferred carrier and shadowed identity", - src: `fn inferred(value: ? ?i32) -> i32 { + src: `struct Holder { field: ?i32, fallback: i32 } + +fn inferred(value: ?Holder) -> i32 { if value == none { return 0; } - let inner = value; + let inner = value.field; if inner == none { - return 0; + return value.fallback; } return inner; } @@ -2919,6 +2922,9 @@ fn valid(owner: ?*Holder) -> i32 { if diag.HasErrors() { t.Fatalf("optional narrowing failed:\n%s", diag.EmitAllToString()) } + if got := hasDiagnosticCode(diag, diagnostics.InfoRedundantOptional); got != tt.redundantInfo { + t.Fatalf("redundant optional info = %v, want %v:\n%s", got, tt.redundantInfo, diag.EmitAllToString()) + } }) } } @@ -2949,18 +2955,20 @@ fn invalid(value: ?Holder) -> i32 { return value.Get(); }`, }`, }, { - name: "join recheck clears stale nested payload evidence", + name: "join recheck clears stale field payload evidence", code: "T0041", - src: `fn invalid(value: ? ?i32) -> i32 { - if value != none { + src: `struct Holder { field: ?i32, other: ?i32 } + +fn invalid(value: Holder) -> i32 { + if value.field != none { } else { print(0); print(1); } - if value == none { + if value.other == none { return 0; } - return value; + return value.field; }`, }, { diff --git a/internal/project/generic_types.go b/internal/project/generic_types.go index 5dc8a7d..243ddfa 100644 --- a/internal/project/generic_types.go +++ b/internal/project/generic_types.go @@ -1,6 +1,7 @@ package project import ( + "slices" "strconv" "strings" @@ -17,6 +18,7 @@ type namedTypeDeclaration struct { type namedTypeInstance struct { ownerModuleID moduleid.ID + base *typeinfo.DefinedType typ *typeinfo.DefinedType ready chan struct{} complete bool @@ -138,6 +140,7 @@ func (ctx *CompilerContext) instantiateType(base *typeinfo.DefinedType, argument // applications resolve back to this exact object. ctx.typeInstances[identity] = namedTypeInstance{ ownerModuleID: declarationModule.ID, + base: base, typ: instance, ready: make(chan struct{}), } @@ -149,12 +152,7 @@ func (ctx *CompilerContext) instantiateType(base *typeinfo.DefinedType, argument applicationText: applicationText, node: node, }) - opts := TypeSyntaxOptions(ctx, declarationModule, nil, true) - opts.TypeParameters = typeinfo.TypeParameterBindings(base.TypeParameters, canonicalArguments) - opts.Instantiate = func(nestedBase *typeinfo.DefinedType, nestedArguments []typeinfo.Type, nestedNode ast.TypeExpr) typeinfo.Type { - return ctx.instantiateType(nestedBase, nestedArguments, nestedNode, chain) - } - instance.Underlying = typeinfo.TypeFromSyntax(declaration.syntax.UnderlyingType(), opts) + instance.Underlying = ctx.typeInstanceUnderlying(declarationModule, declaration, instance, chain) valid := !typeinfo.ContainsInvalid(instance.Underlying) ctx.finishTypeInstance(identity, instance, valid) if !valid { @@ -163,6 +161,67 @@ func (ctx *CompilerContext) instantiateType(base *typeinfo.DefinedType, argument return instance } +func (ctx *CompilerContext) typeInstanceUnderlying(declarationModule *Module, declaration namedTypeDeclaration, instance *typeinfo.DefinedType, chain []typeInstantiationFrame) typeinfo.Type { + opts := TypeSyntaxOptions(ctx, declarationModule, nil, true) + opts.TypeParameters = typeinfo.TypeParameterBindings(declaration.base.TypeParameters, instance.TypeArguments) + opts.Instantiate = func(nestedBase *typeinfo.DefinedType, nestedArguments []typeinfo.Type, nestedNode ast.TypeExpr) typeinfo.Type { + return ctx.instantiateType(nestedBase, nestedArguments, nestedNode, chain) + } + return typeinfo.TypeFromSyntax(declaration.syntax.UnderlyingType(), opts) +} + +// CompleteTypeInstances rebuilds cached instances in place after binder fills +// every shell in a legal declaration cycle. Pointer identity remains stable for +// recursive references and existing cache consumers. +func (ctx *CompilerContext) CompleteTypeInstances(bases []*typeinfo.DefinedType) { + if ctx == nil || len(bases) == 0 { + return + } + selected := make(map[*typeinfo.DefinedType]bool, len(bases)) + for _, base := range bases { + if base != nil && len(base.TypeParameters) > 0 { + selected[base] = true + } + } + if len(selected) == 0 { + return + } + + ctx.mu.RLock() + identities := make([]string, 0) + for identity, cached := range ctx.typeInstances { + if cached.complete && cached.typ != nil && selected[cached.base] { + identities = append(identities, identity) + } + } + ctx.mu.RUnlock() + slices.Sort(identities) + + for _, identity := range identities { + ctx.mu.RLock() + cached := ctx.typeInstances[identity] + declarationModule := ctx.typeDeclarations[cached.base.Identity] + declaration := namedTypeDeclaration{} + if declarationModule != nil { + declaration = declarationModule.namedTypeDeclarations[cached.base.Identity] + } + ctx.mu.RUnlock() + if declarationModule == nil || declaration.syntax == nil || declaration.base != cached.base { + continue + } + chain := []typeInstantiationFrame{{ + declarationIdentity: cached.base.Identity, + applicationIdentity: identity, + applicationText: cached.typ.Text(), + node: declaration.syntax.UnderlyingType(), + }} + underlying := ctx.typeInstanceUnderlying(declarationModule, declaration, cached.typ, chain) + if !typeinfo.ContainsInvalid(underlying) { + cached.typ.Underlying = underlying + } + } +} + // finishTypeInstance publishes or removes one provisional cache entry and // wakes any concurrent application waiting on the same semantic identity. func (ctx *CompilerContext) finishTypeInstance(identity string, instance *typeinfo.DefinedType, valid bool) { diff --git a/internal/project/modules.go b/internal/project/modules.go index 276e5ad..120f6cd 100644 --- a/internal/project/modules.go +++ b/internal/project/modules.go @@ -120,14 +120,25 @@ func (m *Module) RebuildTypedASTIndex() { if m.Typechecking == nil { return } + indexGenerated := func(node ast.Node) bool { + if sourceLoop, ok := node.(*ast.ForStmt); ok { + // An outer expansion still contains original nested loops. Do not + // overwrite their checked trees depending on map traversal order. + if sourceLoop.Iterable != nil && m.Typechecking.CheckedIterations[sourceLoop.ID()] != nil { + return false + } + } + if node != nil { + m.TypedASTNodes[node.ID()] = node + } + return true + } + for _, loop := range m.Typechecking.CheckedIterations { + ast.Inspect(loop, indexGenerated) + } for _, args := range m.Typechecking.EffectiveCallArguments { for _, arg := range args { - ast.Inspect(arg, func(node ast.Node) bool { - if node != nil { - m.TypedASTNodes[node.ID()] = node - } - return true - }) + ast.Inspect(arg, indexGenerated) } } } diff --git a/internal/semantics/binder/binder.go b/internal/semantics/binder/binder.go index d8e68ef..d09513d 100644 --- a/internal/semantics/binder/binder.go +++ b/internal/semantics/binder/binder.go @@ -24,11 +24,18 @@ func Bind(ctx *project.CompilerContext, module *project.Module) { } func (b *binder) bindModule() { - ast.ForEachDecl(b.module.AST, func(decl ast.Decl) bool { - if typeDecl, ok := decl.(ast.TypeDecl); ok { - b.bindTypeDecl(typeDecl) - return true + ordered, completionCycle := b.typeDeclarationOrder() + for _, decl := range ordered { + b.bindTypeDecl(decl) + } + completed := make([]*typeinfo.DefinedType, 0, len(completionCycle)) + for _, decl := range completionCycle { + if defined := b.bindTypeDecl(decl); defined != nil { + completed = append(completed, defined) } + } + b.ctx.CompleteTypeInstances(completed) + ast.ForEachDecl(b.module.AST, func(decl ast.Decl) bool { switch node := decl.(type) { case *ast.FnDecl: b.bindFunctionDecl(node) @@ -42,7 +49,6 @@ func (b *binder) bindModule() { slices.SortFunc(b.module.Bindings.OperationFunctions, func(left, right *symbols.Symbol) int { return cmp.Compare(left.Name, right.Name) }) - b.validateTypeDeclCycles() } // Bind function and top-level declaration signatures into module scope. @@ -84,18 +90,18 @@ func (b *binder) bindModuleBinding(name *ast.Ident, typ ast.TypeExpr) { // Bind named type declarations using one stable shell per symbol. // Recursive self-references must see same DefinedType object. -func (b *binder) bindTypeDecl(decl ast.TypeDecl) { +func (b *binder) bindTypeDecl(decl ast.TypeDecl) *typeinfo.DefinedType { if b == nil || b.module == nil || decl == nil { - return + return nil } name := decl.DeclName() typ := decl.UnderlyingType() if name == nil || name.Name == "" { - return + return nil } sym := b.moduleScopeSymbol(name.Name) if sym == nil { - return + return nil } defined, ok := sym.Type.(*typeinfo.DefinedType) if ok && defined != nil { @@ -112,7 +118,7 @@ func (b *binder) bindTypeDecl(decl ast.TypeDecl) { opts := project.TypeSyntaxOptions(b.ctx, b.module, nil, true) opts.TypeParameters = typeinfo.TypeParameterBindings(defined.TypeParameters, nil) defined.Underlying = typeinfo.TypeFromSyntax(typ, opts) - b.registerTypeDecl(name.Name, typ) + return defined } func (b *binder) moduleScopeSymbol(name string) *symbols.Symbol { diff --git a/internal/semantics/binder/binder_test.go b/internal/semantics/binder/binder_test.go index d1223c7..1d35925 100644 --- a/internal/semantics/binder/binder_test.go +++ b/internal/semantics/binder/binder_test.go @@ -233,6 +233,68 @@ fn Use(alias: Choice, canonical: Choice) {}` } } +func TestBindCompletesGenericArgumentDependencies(t *testing.T) { + for _, source := range []string{ + `type Early = Box; struct Box { value: T } type Maybe = ?Later; type Later = ?i32;`, + `type Early = Box; struct Box { value: T } type Maybe = Optional; type Optional = ?T; type Later = ?i32;`, + `type Early = Box; type Maybe = ?Later; struct Box { value: Later } type Later = ?i32;`, + `struct Node { link: ?Link } type Link = ?*Node; type Early = Box; struct Box { value: T } type Maybe = ?i32;`, + `type Link = ?*Node; struct Node { link: ?Link } type Early = Box; struct Box { value: T } type Maybe = ?i32;`, + `type Linked = Node; struct Node { link: ?Link } type Link = ?*Node; type Early = Box; struct Box { value: T } type Maybe = ?i32;`, + } { + t.Run(source, func(t *testing.T) { + const filePath = "binder_forward_optional_test" + peeper.SourceExt + source += ` fn Use(early: Early, canonical: Box) {}` + diag := diagnostics.NewDiagnosticBag() + ctx := project.New(".", peeper.SourceExt, diag) + module := &project.Module{ + ID: moduleid.ID{Origin: string(project.ModuleOriginLocal), ImportPath: strings.TrimSuffix(filePath, peeper.SourceExt)}, + FilePath: filePath, Content: source, + AST: parser.New(filePath, lexer.New(filePath, source, diag).Tokenize(), diag).ParseModule(), + Imports: make(map[string]project.ResolvedImport), + } + collector.Collect(ctx, module) + Bind(ctx, module) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + use, ok := module.ModuleScope.LookupLocal("Use") + if !ok { + t.Fatal("missing Use function") + } + fn := use.Type.(*typeinfo.FuncType) + if typeinfo.Unalias(fn.Params[0]) != fn.Params[1] { + t.Fatal("forward alias split canonical generic instance") + } + field := typeinfo.Underlying(fn.Params[0]).(*typeinfo.StructType).Fields[0].Type + optional, ok := typeinfo.Unalias(field).(*typeinfo.OptionalType) + if !ok || typeinfo.TypeText(optional.Inner) != "i32" { + t.Fatalf("field = %s, want one optional i32", typeinfo.TypeText(field)) + } + seen := make(map[typeinfo.Type]bool) + var check func(typeinfo.Type) + check = func(typ typeinfo.Type) { + if typ == nil || seen[typ] { + return + } + seen[typ] = true + if optional, ok := typ.(*typeinfo.OptionalType); ok { + if _, nested := typeinfo.Unalias(optional.Inner).(*typeinfo.OptionalType); nested { + t.Error("completed type retains nested optional carriers") + } + } + typeinfo.ForEachChild(typ, func(child typeinfo.TypeChild) bool { + check(child.Type) + return true + }) + } + for _, sym := range module.ModuleScope.Symbols() { + check(sym.Type) + } + }) + } +} + func TestBindRejectsExpandingGenericRecursion(t *testing.T) { if os.Getenv("PEEPER_TEST_EXPANDING_GENERIC_RECURSION") == "1" { tests := []struct { diff --git a/internal/semantics/binder/type_decl_cycles.go b/internal/semantics/binder/type_decl_cycles.go index 4d58246..993b208 100644 --- a/internal/semantics/binder/type_decl_cycles.go +++ b/internal/semantics/binder/type_decl_cycles.go @@ -2,6 +2,7 @@ package binder import ( "fmt" + "slices" "strings" "compiler/internal/diagnostics" @@ -10,42 +11,50 @@ import ( "compiler/internal/moduleid" "compiler/internal/project" "compiler/internal/semantics/symbols" + "compiler/internal/semantics/typeinfo" ) const ( - graphEdgeTypeValueRef graph.EdgeKind = "type_value_ref" - graphEdgeTypeIndirectRef graph.EdgeKind = "type_indirect_ref" + graphEdgeTypeValueRef graph.EdgeKind = "type_value_ref" + graphEdgeTypeIndirectRef graph.EdgeKind = "type_indirect_ref" + graphEdgeTypeCompletionRef graph.EdgeKind = "type_completion_ref" ) -func (b *binder) registerTypeDecl(name string, typ ast.TypeExpr) { - if b == nil || b.ctx == nil || b.ctx.Graph == nil || b.module == nil || name == "" { - return - } - owner := typeDeclNodeID(b.module.ID, name) - // Value edges require full layout; indirect references do not force target expansion. - b.addTypeDeclEdges(owner, typ, false) -} - -func (b *binder) validateTypeDeclCycles() { +// typeDeclarationOrder registers dependencies once and orders construction +// before any generic applications can cache incomplete alias representations. +// Legal completion cycles get one bounded completion pass after every shell is +// populated. Only value edges are illegal cycles. +func (b *binder) typeDeclarationOrder() ([]ast.TypeDecl, []ast.TypeDecl) { if b == nil || b.ctx == nil || b.ctx.Graph == nil || b.ctx.Diagnostics == nil || b.module == nil || b.module.ModuleScope == nil { - return - } - nodeIDs := make([]graph.NodeID, 0) - for _, sym := range b.module.ModuleScope.Symbols() { - if sym == nil || sym.Kind != symbols.SymbolType { - continue + return nil, nil + } + var nodeIDs []graph.NodeID + declarations := make(map[graph.NodeID]ast.TypeDecl) + ast.ForEachDecl(b.module.AST, func(decl ast.Decl) bool { + typeDecl, ok := decl.(ast.TypeDecl) + if !ok || typeDecl.DeclName() == nil { + return true } - nodeIDs = append(nodeIDs, typeDeclNodeID(b.module.ID, sym.Name)) - } - if len(nodeIDs) == 0 { - return - } + sym := b.moduleScopeSymbol(typeDecl.DeclName().Name) + if sym == nil || sym.ASTNode != decl { + return true + } + id := typeDeclNodeID(b.module.ID, sym.Name) + nodeIDs = append(nodeIDs, id) + declarations[id] = typeDecl + b.addTypeDeclEdges(id, typeDecl.UnderlyingType(), false, typeDecl.DeclarationTypeParams()) + return true + }) // Only value-layout edges participate in illegal cycle detection. _, cycles := b.ctx.Graph.TopoSort(nodeIDs, graphEdgeTypeValueRef) + illegal := make(map[graph.NodeID]bool) for _, cycle := range cycles { if len(cycle) == 0 { continue } + for _, id := range cycle { + illegal[id] = true + } firstName := typeDeclNameFromNodeID(cycle[0]) firstSym, ok := b.module.ModuleScope.LookupLocal(firstName) if !ok || firstSym == nil { @@ -68,6 +77,23 @@ func (b *binder) validateTypeDeclCycles() { "break the cycle with indirection such as a pointer", ) } + order, completionCycles := b.ctx.Graph.TopoSort(nodeIDs, graphEdgeTypeCompletionRef) + ordered := make([]ast.TypeDecl, 0, len(order)) + for _, id := range order { + ordered = append(ordered, declarations[id]) + } + seen := make(map[graph.NodeID]bool) + completion := make([]ast.TypeDecl, 0) + for _, cycle := range completionCycles { + for _, id := range cycle { + if illegal[id] || seen[id] || declarations[id] == nil { + continue + } + seen[id] = true + completion = append(completion, declarations[id]) + } + } + return ordered, completion } func typeDeclNodeID(moduleID moduleid.ID, name string) graph.NodeID { @@ -77,57 +103,70 @@ func typeDeclNodeID(moduleID moduleid.ID, name string) graph.NodeID { return graph.NodeID("type:" + moduleID.String() + ":" + name) } -func (b *binder) addTypeDeclEdges(owner graph.NodeID, typ ast.TypeExpr, indirect bool) { +func (b *binder) addTypeDeclEdges(owner graph.NodeID, typ ast.TypeExpr, indirect bool, parameters []ast.TypeParam) { if b == nil || b.ctx == nil || b.ctx.Graph == nil || b.module == nil || owner == "" || typ == nil { return } switch node := typ.(type) { case *ast.NamedType: - b.addTypeDeclEdge(owner, b.lookupTypeDeclNodeID(node.Name), indirect) + target, alias := b.lookupTypeDeclNodeID(node.Name, parameters) + b.addTypeDeclEdge(owner, target, indirect, alias) case *ast.AppliedType: if node.Name != nil { - b.addTypeDeclEdge(owner, b.lookupTypeDeclNodeID(node.Name.Name), indirect) + target, _ := b.lookupTypeDeclNodeID(node.Name.Name, parameters) + b.addTypeDeclEdge(owner, target, indirect, true) + } + // Arguments must be canonical before instance keys are computed. An + // argument occurrence alone does not establish an inline layout edge. + for _, argument := range node.TypeArgs { + b.addTypeDeclEdges(owner, argument, true, parameters) } case *ast.ScopeResolution: - b.addTypeDeclEdge(owner, b.lookupQualifiedTypeDeclNodeID(node), indirect) + target, alias := b.lookupQualifiedTypeDeclNodeID(node) + b.addTypeDeclEdge(owner, target, indirect, alias || len(node.Segments[len(node.Segments)-1].TypeArgs) > 0) + for _, segment := range node.Segments { + for _, argument := range segment.TypeArgs { + b.addTypeDeclEdges(owner, argument, true, parameters) + } + } case *ast.RawPtrType: // Raw pointers carry no pointee layout dependency. case *ast.EnumType: for _, variant := range node.Variants { - b.addTypeDeclEdges(owner, variant.Payload, indirect) + b.addTypeDeclEdges(owner, variant.Payload, indirect, parameters) } case *ast.OwnedPtrType: // Pointer target is not a layout dependency. - b.addTypeDeclEdges(owner, node.Target, true) + b.addTypeDeclEdges(owner, node.Target, true, parameters) case *ast.RefType: // Reference target is not owned inline storage. - b.addTypeDeclEdges(owner, node.Target, true) + b.addTypeDeclEdges(owner, node.Target, true, parameters) case *ast.OptionalType: - b.addTypeDeclEdges(owner, node.Inner, indirect) + b.addTypeDeclEdges(owner, node.Inner, indirect, parameters) case *ast.ArrayType: - b.addTypeDeclEdges(owner, node.Elem, indirect || node.Shape != ast.ArrayFixed || node.Len == nil) + b.addTypeDeclEdges(owner, node.Elem, indirect || node.Shape != ast.ArrayFixed || node.Len == nil, parameters) case *ast.StructType: for _, field := range node.Fields { - b.addTypeDeclEdges(owner, field.Type, indirect) + b.addTypeDeclEdges(owner, field.Type, indirect, parameters) } case *ast.FuncType: for _, param := range node.Params { - b.addTypeDeclEdges(owner, param.Type, true) + b.addTypeDeclEdges(owner, param.Type, true, parameters) } - b.addTypeDeclEdges(owner, node.Return, true) + b.addTypeDeclEdges(owner, node.Return, true, parameters) case *ast.InterfaceType: for _, method := range node.Methods { for _, param := range method.Params { - b.addTypeDeclEdges(owner, param.Type, true) + b.addTypeDeclEdges(owner, param.Type, true, parameters) } - b.addTypeDeclEdges(owner, method.ReturnType, true) + b.addTypeDeclEdges(owner, method.ReturnType, true, parameters) } default: panic(fmt.Sprintf("binder type dependencies: unhandled type syntax %T", typ)) } } -func (b *binder) addTypeDeclEdge(owner, target graph.NodeID, indirect bool) { +func (b *binder) addTypeDeclEdge(owner, target graph.NodeID, indirect, complete bool) { if target == "" { return } @@ -136,32 +175,44 @@ func (b *binder) addTypeDeclEdge(owner, target graph.NodeID, indirect bool) { kind = graphEdgeTypeIndirectRef } b.ctx.Graph.AddEdge(owner, target, kind) + // Nominal references only need their collected shell. Aliases and applied + // declarations must finish first, even when used behind an indirection. + if complete && owner != target { + b.ctx.Graph.AddEdge(owner, target, graphEdgeTypeCompletionRef) + } } -func (b *binder) lookupTypeDeclNodeID(name string) graph.NodeID { +func (b *binder) lookupTypeDeclNodeID(name string, parameters []ast.TypeParam) (graph.NodeID, bool) { if b == nil || b.module == nil || b.module.ModuleScope == nil || name == "" { - return "" + return "", false + } + if slices.ContainsFunc(parameters, func(parameter ast.TypeParam) bool { + return parameter.Name != nil && parameter.Name.Name == name + }) { + return "", false } sym, ok := b.module.ModuleScope.Lookup(name) if !ok || sym == nil || sym.Kind != symbols.SymbolType { - return "" + return "", false } - return typeDeclNodeID(b.module.ID, sym.Name) + defined, ok := sym.Type.(*typeinfo.DefinedType) + return typeDeclNodeID(b.module.ID, sym.Name), ok && defined.Kind == typeinfo.DefinedKindAlias } -func (b *binder) lookupQualifiedTypeDeclNodeID(node *ast.ScopeResolution) graph.NodeID { +func (b *binder) lookupQualifiedTypeDeclNodeID(node *ast.ScopeResolution) (graph.NodeID, bool) { if b == nil || b.ctx == nil || b.module == nil || node == nil { - return "" + return "", false } qualifier, member, imported := node.ImportMember() if !imported { - return "" + return "", false } resolved, ok := project.LookupImportedSymbol(b.ctx, b.module, qualifier.Name, member.Name) if !ok || resolved.Module == nil || resolved.Symbol == nil || resolved.Symbol.Kind != symbols.SymbolType { - return "" + return "", false } - return typeDeclNodeID(resolved.Module.ID, resolved.Symbol.Name) + defined, ok := resolved.Symbol.Type.(*typeinfo.DefinedType) + return typeDeclNodeID(resolved.Module.ID, resolved.Symbol.Name), ok && defined.Kind == typeinfo.DefinedKindAlias } func typeDeclNameFromNodeID(id graph.NodeID) string { diff --git a/internal/semantics/definiteinit/initialization_test.go b/internal/semantics/definiteinit/initialization_test.go index e98c65d..9eac6f9 100644 --- a/internal/semantics/definiteinit/initialization_test.go +++ b/internal/semantics/definiteinit/initialization_test.go @@ -42,6 +42,7 @@ func analyzeInitializationSource(t *testing.T, source string) (*functionResult, module.CFG = cfg.BuildModule(module.AST, cfg.BuildQueries{ MatchCases: module.Typechecking.MatchCases, LoopGuaranteedEntry: module.Typechecking.ForLoopGuaranteedEntry, + CheckedIterations: module.Typechecking.CheckedIterations, }) symbol, found := module.ModuleScope.Lookup("choose") if !found || symbol == nil { @@ -70,6 +71,20 @@ func analyzeInitializationSource(t *testing.T, source string) (*functionResult, return result, diag, module } +func TestCallIterationDoesNotGuaranteeEntry(t *testing.T) { + _, diag, _ := analyzeInitializationSource(t, `struct Cursor {} +fn (self: &Cursor) Next() -> ?i32 { return none; } +fn choose() -> i32 { + let cursor = Cursor.{}; + let mut result: i32; + for item in cursor.Next() { result = item; } + return result; +}`) + if !diag.HasErrors() || !strings.Contains(diag.EmitAllToString(), "used before it's initialized") { + t.Fatalf("expected zero-entry uninitialized diagnostic:\n%s", diag.EmitAllToString()) + } +} + func TestInitializationIgnoresTerminatingBranchAtJoin(t *testing.T) { result, diag, _ := analyzeInitializationSource(t, `fn choose(flag: bool) -> i32 { let mut value: i32; diff --git a/internal/semantics/effect/build_test.go b/internal/semantics/effect/build_test.go index 2fb9fd9..9635402 100644 --- a/internal/semantics/effect/build_test.go +++ b/internal/semantics/effect/build_test.go @@ -42,6 +42,7 @@ func buildEffects(t *testing.T, source string) (effect.Result, *project.Module) module.CFG = cfg.BuildModule(module.AST, cfg.BuildQueries{ MatchCases: module.Typechecking.MatchCases, LoopGuaranteedEntry: module.Typechecking.ForLoopGuaranteedEntry, + CheckedIterations: module.Typechecking.CheckedIterations, }) if diag.HasErrors() { t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) @@ -136,6 +137,31 @@ func describe(op effect.Op) string { return "unknown" } +func TestCallIterationPublishesOrdinaryReceiverCall(t *testing.T) { + result, module := buildEffects(t, `struct Cursor {} +fn (self: &mut Cursor) Next() -> ?i32 { return none; } +fn probe() { + let mut cursor = Cursor.{}; + for item in cursor.Next() { if item == 1 { continue; } } +}`) + calls, ends, borrows := 0, 0, 0 + for _, op := range publishedOps(t, result, module, "probe") { + switch op { + case "call": + calls++ + case "end": + ends++ + case "borrow cursor": + borrows++ + case "iterate cursor": + t.Fatal("custom iteration acquired a builtin sequence lifetime") + } + } + if calls != 1 || ends != 1 || borrows != 1 { + t.Fatalf("advancement effects: calls=%d ends=%d borrows=%d", calls, ends, borrows) + } +} + func TestBuildPublishesProjectionOperands(t *testing.T) { for _, test := range []struct { name string diff --git a/internal/semantics/ownership/ownership_test.go b/internal/semantics/ownership/ownership_test.go index aea44b7..78fde2d 100644 --- a/internal/semantics/ownership/ownership_test.go +++ b/internal/semantics/ownership/ownership_test.go @@ -53,6 +53,7 @@ func checkOwnershipSource(t *testing.T, src string) *ownershipResult { module.CFG = cfg.BuildModule(module.AST, cfg.BuildQueries{ MatchCases: module.Typechecking.MatchCases, LoopGuaranteedEntry: module.Typechecking.ForLoopGuaranteedEntry, + CheckedIterations: module.Typechecking.CheckedIterations, }) module.Flow = typechecker.CheckFlow(ctx, module) module.Effects = effect.Build(module.CFG, module.TypedASTNodes, effect.BuildQueries{ @@ -70,6 +71,84 @@ func checkOwnershipSource(t *testing.T, src string) *ownershipResult { return &ownershipResult{DiagnosticBag: diag, ctx: ctx, module: module} } +func TestCallIterationUsesOrdinaryCallGuards(t *testing.T) { + for _, test := range []struct { + name, receiver, before, body, after, diagnostic string + }{ + {name: "mutable receiver", receiver: "&mut Cursor"}, + {name: "shared receiver", receiver: "&Cursor"}, + {name: "body access between advances", receiver: "&mut Cursor", body: "cursor.value = item;"}, + {name: "consuming backedge", receiver: "Cursor", diagnostic: "moved"}, + {name: "consuming single attempt", receiver: "Cursor", body: "break;"}, + {name: "moved source", receiver: "&mut Cursor", before: "Consume(cursor);", diagnostic: "moved"}, + {name: "body move before backedge", receiver: "&mut Cursor", body: "Consume(cursor);", diagnostic: "moved"}, + {name: "live conflicting borrow", receiver: "&mut Cursor", before: "let borrowed = &cursor;", after: "let value = borrowed.value;", diagnostic: "borrow"}, + } { + t.Run(test.name, func(t *testing.T) { + for _, implicit := range []bool{false, true} { + loop := "for { let result = cursor.Next(); if result == none { break; } let item: i32 = result; " + test.body + " }" + if implicit { + loop = "for item in cursor.Next() { " + test.body + " }" + } + result := checkOwnershipSource(t, "struct Cursor { value: i32, limit: i32 }\nfn (self: "+test.receiver+") Next() -> ?i32 { return none; }\nfn Consume(cursor: Cursor) {}\nfn main() { let mut cursor = Cursor.{ value = 0, limit = 3 }; "+test.before+loop+test.after+" }") + if test.diagnostic == "" { + if result.HasErrors() { + t.Fatalf("implicit=%v unexpected diagnostics:\n%s", implicit, result.EmitAllToString()) + } + } else if !result.HasErrors() || !strings.Contains(result.EmitAllToString(), test.diagnostic) { + t.Fatalf("implicit=%v expected %q:\n%s", implicit, test.diagnostic, result.EmitAllToString()) + } + } + }) + } +} + +func TestIteratorFactoryArgumentCleanup(t *testing.T) { + for _, body := range []string{"", "continue;", "break;", "return;"} { + t.Run(body, func(t *testing.T) { + for _, expanded := range []bool{false, true} { + loop := "for { let result = Produce(Make()); if result == none { break; } let item: i32 = result; " + body + " }" + if expanded { + loop = "for item in Produce(Make()) { " + body + " }" + } + result := checkOwnershipSource(t, `struct Argument { held: *i32, value: i32 } +fn Make() -> Argument { return Argument.{ held = alloc(1), value = 0 }; } +fn Produce(argument: Argument) -> ?i32 { + if argument.value == 0 { return none; } + return argument.value; +} +fn main() { `+loop+` }`) + if result.HasErrors() { + t.Fatalf("expanded=%v unexpected diagnostics:\n%s", expanded, result.EmitAllToString()) + } + producer := result.module.AST.Stmts[2].(*ast.FnDecl) + plan := cleanupPlanForFunction(t, result, producer) + returns := []*ast.ReturnStmt{ + producer.Body.Stmts[0].(*ast.IfStmt).Then.Stmts[0].(*ast.ReturnStmt), + producer.Body.Stmts[1].(*ast.ReturnStmt), + } + for _, ret := range returns { + if got := cleanupSymbolNames(result.module, plan.BeforeReturn[ir.NodeID(ret.ID())]); !slices.Equal(got, []string{"argument"}) { + t.Fatalf("expanded=%v argument cleanup = %v, want [argument]", expanded, got) + } + } + if expanded { + fn := result.module.AST.Stmts[3].(*ast.FnDecl) + sourceLoop := fn.Body.Stmts[0].(*ast.ForStmt) + expansion := result.module.Typechecking.CheckedIterations[sourceLoop.ID()] + if len(expansion.Stmts) != 1 { + t.Fatal("factory argument captured outside repeated call") + } + checked := expansion.Stmts[0].(*ast.ForStmt) + if checked.Body.Stmts[0].(*ast.LetDecl).Value != sourceLoop.Iterable { + t.Fatal("producer call replaced instead of checked unchanged") + } + } + } + }) + } +} + func inspectFunctionAnalysis(t *testing.T, result *ownershipResult, name string) *analyzer { t.Helper() sym, found := result.module.ModuleScope.Lookup(name) diff --git a/internal/semantics/symbols/scope.go b/internal/semantics/symbols/scope.go index 1aa1cd0..741ad74 100644 --- a/internal/semantics/symbols/scope.go +++ b/internal/semantics/symbols/scope.go @@ -23,6 +23,14 @@ func NewScope(parent *Scope) *Scope { } } +// InsertParent adds a generated scope between this scope and its lexical +// parent without invalidating resolved child symbols. An explicit parent keeps +// repeated semantic checks from nesting stale generated scopes. +func (s *Scope) InsertParent(parent *Scope) *Scope { + s.parent = NewScope(parent) + return s.parent +} + func (s *Scope) Parent() *Scope { if s == nil { return nil diff --git a/internal/semantics/typechecker/check_call.go b/internal/semantics/typechecker/check_call.go index ee574f8..62fb66a 100644 --- a/internal/semantics/typechecker/check_call.go +++ b/internal/semantics/typechecker/check_call.go @@ -56,6 +56,11 @@ func (c *checker) typePrintExpr(scope *symbols.Scope, node *ast.PrintExpr) typei } func (c *checker) typeCallExpr(scope *symbols.Scope, node *ast.CallExpr) typeinfo.Type { + if c.flow == nil && c.reusedCall == node { + if typ, checked := c.module.Typechecking.ExprTypes[node.ID()]; checked { + return typ + } + } effectiveArgs := c.module.Typechecking.CallArgumentsOrSource(node) if c.flow == nil { effectiveArgs = append([]ast.Expr(nil), node.Args...) diff --git a/internal/semantics/typechecker/check_stmt.go b/internal/semantics/typechecker/check_stmt.go index 9fcb477..743cb4d 100644 --- a/internal/semantics/typechecker/check_stmt.go +++ b/internal/semantics/typechecker/check_stmt.go @@ -508,9 +508,9 @@ func (c *checker) checkBinding(scope *symbols.Scope, node ast.Stmt, requireIniti } } -// checkForInStmt types a `for x in iterable` loop. The iterable may be a range -// expression or an indexable sequence; strings are rejected because string -// element access requires an explicit as_bytes/as_chars view. +// checkForInStmt types a `for x in iterable` loop over a range, indexable +// sequence, or optional-producing call. Strings require an explicit +// as_bytes/as_chars view. func (c *checker) checkForInStmt(scope *symbols.Scope, node *ast.ForStmt, returnType typeinfo.Type) { indexType := typeinfo.DefaultIntegerType() evidence := typecheckresult.ForIteration{} @@ -625,9 +625,32 @@ func (c *checker) checkForInStmt(scope *symbols.Scope, node *ast.ForStmt, return } evidence.ElementType = elem } else { + if valid && !c.siteOnly { + call, callExpr := node.Iterable.(*ast.CallExpr) + optional, optionalResult := typeinfo.Underlying(iterableType).(*typeinfo.OptionalType) + switch { + case !callExpr: + c.ctx.Diagnostics.Add(invalidExpressionError(node.Iterable, "cannot iterate over "+typeinfo.TypeText(iterableType)). + WithHelp("use a range, array, slice, or an explicit call returning an optional item, such as `for item in producer()`"). + WithNote("iterator calls, including arguments, are evaluated on every attempt; a bare optional value is not a producer")) + case !optionalResult || optional.Inner == nil: + c.ctx.Diagnostics.Add(invalidExpressionError(node.Iterable, "iterator call must return an optional item"). + WithHelp("return an optional type, such as `?i32`: return an item to continue, or `none` to end the loop")) + case node.Index != nil: + c.ctx.Diagnostics.Add(invalidExpressionError(node.Index, "iterator loops provide an item, not an index"). + WithHelp("use `for item in producer()`; if you need an index, maintain a separate counter")) + default: + c.expandCallIteration(scope, node) + } + if checked := c.module.Typechecking.CheckedIterations[node.ID()]; checked != nil { + previous := c.reusedCall + c.reusedCall = call + c.checkStmt(scope, checked, returnType) + c.reusedCall = previous + return + } + } valid = false - c.ctx.Diagnostics.Add(invalidExpressionError(node.Iterable, - "cannot iterate over "+typeinfo.TypeText(iterableType))) } } if node.Index != nil { @@ -667,6 +690,95 @@ func (c *checker) checkForInStmt(scope *symbols.Scope, node *ast.ForStmt, return c.loopDepth-- } +// expandCallIteration publishes ordinary checked statements before flow and +// ownership. Keeping the entire source call inside the loop repeats its receiver +// and arguments on every attempt, including the terminating attempt. +func (c *checker) expandCallIteration(scope *symbols.Scope, node *ast.ForStmt) { + location := ast.LocOf(node) + expansion := &ast.BlockStmt{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Location: location, + } + resultName := fmt.Sprintf("$for.result.%d", node.ID()) + result := &ast.LetDecl{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Name: &ast.Ident{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Name: resultName, + Location: location, + }, + Value: node.Iterable, + Location: location, + } + stop := &ast.IfStmt{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Cond: &ast.BinaryExpr{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Left: &ast.Ident{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Name: resultName, + Location: location, + }, + Op: "==", + Right: &ast.NoneLit{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Location: location, + }, + Location: location, + }, + Then: &ast.BlockStmt{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Stmts: []ast.Stmt{&ast.BreakStmt{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Location: location, + }}, + Location: location, + }, + Location: location, + } + item := &ast.LetDecl{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Name: node.Value, + Value: &ast.Ident{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Name: resultName, + Location: location, + }, + Location: location, + } + body := *node.Body + body.Stmts = append([]ast.Stmt{item}, node.Body.Stmts...) + checked := &ast.ForStmt{ + NodeIDHolder: node.NodeIDHolder, + Body: &ast.BlockStmt{ + NodeIDHolder: ast.NodeIDHolder{NodeID: ast.NewSyntheticNodeID()}, + Stmts: []ast.Stmt{result, stop, &body}, + Location: location, + }, + Location: location, + } + expansion.Stmts = append(expansion.Stmts, checked) + bodyScope := c.module.Bindings.BlockScopes[node.Body.ID()] + expansionScope := symbols.NewScope(scope) + c.module.Bindings.BlockScopes[expansion.ID()] = expansionScope + iterationScope := bodyScope.InsertParent(expansionScope) + + c.module.Bindings.BlockScopes[checked.Body.ID()] = iterationScope + c.module.Bindings.BlockScopes[stop.Then.ID()] = symbols.NewScope(iterationScope) + resultSymbol := symbols.New(resultName, symbols.SymbolVar, result, location) + resultSymbol.Used = true + if err := iterationScope.Declare(resultSymbol); err != nil { + panic(err) + } + c.module.Bindings.NodeSymbols[result.Name.ID()] = resultSymbol + c.module.Bindings.NodeSymbols[stop.Cond.(*ast.BinaryExpr).Left.ID()] = resultSymbol + c.module.Bindings.NodeSymbols[item.Value.ID()] = resultSymbol + + itemSymbol := c.module.Bindings.NodeSymbols[node.Value.ID()] + itemSymbol.ASTNode = item + c.module.Typechecking.CheckedIterations[node.ID()] = expansion +} + func (c *checker) bindLoopVariable(name *ast.Ident, typ typeinfo.Type) { if typ == nil { return diff --git a/internal/semantics/typechecker/flow_test.go b/internal/semantics/typechecker/flow_test.go index 044ad1a..17a646f 100644 --- a/internal/semantics/typechecker/flow_test.go +++ b/internal/semantics/typechecker/flow_test.go @@ -42,11 +42,63 @@ func checkFlowSource(t *testing.T, src string) (*project.Module, *diagnostics.Di module.CFG = cfg.BuildModule(module.AST, cfg.BuildQueries{ MatchCases: module.Typechecking.MatchCases, LoopGuaranteedEntry: module.Typechecking.ForLoopGuaranteedEntry, + CheckedIterations: module.Typechecking.CheckedIterations, }) module.Flow = CheckFlow(ctx, module) return module, diag } +func TestCallIterationPublishesCheckedOperations(t *testing.T) { + module, diag := checkFlowSource(t, `struct Cursor { value: i32, limit: i32 } +fn (self: &mut Cursor) Next() -> ?i32 { return none; } +fn main() { + let mut cursor = Cursor.{ value = 0, limit = 3 }; + for cursor in cursor.Next() { + let item: i32 = cursor; + let mut inner = Cursor.{ value = item, limit = 3 }; + for value in inner.Next() { if value == 1 { continue; } } + } +}`) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + if len(module.Typechecking.CheckedIterations) != 2 || len(module.Typechecking.ForIterations) != 0 { + t.Fatalf("iteration evidence = %#v", module.Typechecking) + } + if err := module.CFG.Validate(); err != nil { + t.Fatal(err) + } + for id, expansion := range module.Typechecking.CheckedIterations { + loop := expansion.Stmts[len(expansion.Stmts)-1].(*ast.ForStmt) + if module.TypedASTNodes[expansion.ID()] != expansion { + t.Fatal("source scope not indexed") + } + if module.TypedASTNodes[id] != loop || loop.Iterable != nil || loop.Cond != nil { + t.Fatalf("checked loop not indexed: %#v", loop) + } + result := loop.Body.Stmts[0].(*ast.LetDecl) + call := result.Value.(*ast.CallExpr) + selector := call.Callee.(*ast.SelectorExpr) + if module.Bindings.NodeSymbols[selector.Name.ID()] == nil { + t.Fatal("missing static method evidence") + } + if mutable, found := module.Typechecking.ReferenceArguments[selector.Expr.ID()]; !found || !mutable { + t.Fatal("generated receiver missing ordinary mutable-reference evidence") + } + body := loop.Body.Stmts[2].(*ast.BlockStmt) + item := body.Stmts[0].(*ast.LetDecl) + if got := typeinfo.TypeText(module.Bindings.NodeSymbols[item.Name.ID()].Type); got != "i32" { + t.Fatalf("item type = %s", got) + } + ast.Inspect(expansion, func(node ast.Node) bool { + if node != nil && module.TypedASTNodes[node.ID()] == nil { + t.Errorf("generated node %T/%d not indexed", node, node.ID()) + } + return true + }) + } +} + func TestNamedEnumCaseTestsRefineExactFields(t *testing.T) { module, diag := checkFlowSource(t, `enum Choice { Left: { value: i32 }, diff --git a/internal/semantics/typechecker/for_in_test.go b/internal/semantics/typechecker/for_in_test.go index ab0dfa7..502e22b 100644 --- a/internal/semantics/typechecker/for_in_test.go +++ b/internal/semantics/typechecker/for_in_test.go @@ -10,6 +10,200 @@ import ( "compiler/internal/target" ) +func TestCallIterationRecognition(t *testing.T) { + for _, test := range []struct { + name, method, binding, header, diagnostic, hint string + }{ + {name: "mutable scalar", method: "fn (self: &mut Cursor) Next() -> ?i32 { return none; }"}, + {name: "shared bool", method: "fn (self: &Cursor) Next() -> ?bool { return none; }", binding: "let cursor = Cursor.{};"}, + {name: "bare object", method: "fn (self: &Cursor) Next() -> ?i32 { return none; }", header: "item in cursor", diagnostic: "cannot iterate over", hint: "explicit call returning an optional item"}, + {name: "bare optional", binding: "let cursor: ?i32 = none;", header: "item in cursor", diagnostic: "cannot iterate over", hint: "a bare optional value is not a producer"}, + {name: "lowercase method", method: "fn (self: &Cursor) next() -> ?i32 { return none; }", header: "item in cursor.next()"}, + {name: "arbitrary method", method: "fn (self: &Cursor) Take(value: i32) -> ?i32 { return value; }", header: "item in cursor.Take(3)"}, + {name: "free call", method: "fn Produce() -> ?i32 { return none; }", header: "item in Produce()"}, + {name: "pipe call", method: "fn Produce(cursor: &Cursor) -> ?i32 { return none; }", header: "item in cursor |> Produce()"}, + {name: "wrong return", method: "fn (self: &Cursor) Next() -> i32 { return 0; }", diagnostic: "must return an optional item", hint: "return an item to continue, or `none` to end the loop"}, + {name: "default argument", method: "fn (self: &Cursor) Next(value: i32 = 0) -> ?i32 { return value; }"}, + {name: "invalid argument", method: "fn (self: &Cursor) Next(value: bool) -> ?i32 { return none; }", header: "item in cursor.Next(1)", diagnostic: "bool"}, + {name: "index", method: "fn (self: &Cursor) Next() -> ?i32 { return none; }", header: "index, item in cursor.Next()", diagnostic: "provide an item, not an index", hint: "maintain a separate counter"}, + {name: "immutable", method: "fn (self: &mut Cursor) Next() -> ?i32 { return none; }", binding: "let cursor = Cursor.{};", diagnostic: "mutable"}, + {name: "bare literal", header: "item in Cursor.{}", diagnostic: "cannot iterate over"}, + {name: "object factory", method: "fn (self: &mut Cursor) Next() -> ?i32 { return none; } fn Make() -> Cursor { return Cursor.{}; }", header: "item in Make()", diagnostic: "must return an optional item"}, + {name: "reference source", method: "fn (self: &mut Cursor) Next() -> ?i32 { return none; }", binding: "let mut original = Cursor.{}; let cursor = &mut original;"}, + {name: "function value is not a call", method: "fn Produce() -> ?i32 { return none; }", header: "item in Produce", diagnostic: "cannot iterate over"}, + } { + t.Run(test.name, func(t *testing.T) { + binding, header := test.binding, test.header + if binding == "" { + binding = "let mut cursor = Cursor.{};" + } + if header == "" { + header = "item in cursor.Next()" + } + module, diag := checkTypeModule(t, "struct Cursor {}\n"+test.method+"\nfn main() { "+binding+" for "+header+" {} }") + if test.diagnostic == "" { + if diag.HasErrors() || len(module.Typechecking.CheckedIterations) != 1 { + t.Fatalf("missing checked iteration:\n%s", diag.EmitAllToString()) + } + } else if !diag.HasErrors() || !strings.Contains(diag.EmitAllToString(), test.diagnostic) { + t.Fatalf("expected %q:\n%s", test.diagnostic, diag.EmitAllToString()) + } + if test.hint != "" && !strings.Contains(diag.EmitAllToString(), test.hint) { + t.Fatalf("missing actionable hint %q:\n%s", test.hint, diag.EmitAllToString()) + } + }) + } +} + +func TestCallIterationReusesCheckedProducerEvidence(t *testing.T) { + module, diag := checkTypeModule(t, `iface Reader { fn (&Self) read() -> i32 } +struct Counter { value: i32 } +fn (self: &Counter) read() -> i32 { return self.value; } +fn Produce(value: &Counter, reader: &Reader = value) -> ?i32 { return reader.read(); } +fn main() { + let counter = Counter.{ value = 1 }; + for item in Produce(&counter) {} +}`) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } + var producer *ast.CallExpr + for _, stmt := range module.AST.Stmts { + ast.Inspect(stmt, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if ok && ast.ExprText(call.Callee) == "Produce" { + producer = call + } + return producer == nil + }) + if producer != nil { + break + } + } + if producer == nil { + t.Fatal("producer call not found") + } + effective := module.Typechecking.EffectiveCallArguments[producer.ID()] + if len(effective) != 2 { + t.Fatalf("effective arguments = %d, want 2", len(effective)) + } + if got := len(module.Typechecking.InterfaceImplementations); got != 2 { + t.Fatalf("interface evidence entries = %d, want declaration default plus one call expansion", got) + } + if implementations := module.Typechecking.InterfaceImplementations[effective[1].ID()]; len(implementations) != 1 { + t.Fatalf("effective default evidence = %#v, want one implementation", implementations) + } +} + +func TestCallIterationMatchesExplicitOperations(t *testing.T) { + for _, test := range []struct { + name, source, implicit, explicit string + }{ + { + name: "field source", + source: `struct Cursor {} +fn (self: &mut Cursor) Next() -> ?i32 { return none; } +struct Holder { cursor: Cursor } +fn main() { + let mut holder = Holder.{ cursor = Cursor.{} }; + __LOOP__ +}`, + implicit: "for item in holder.cursor.Next() { let value: i32 = item; }", + explicit: "for { let result = holder.cursor.Next(); if result == none { break; } let item: i32 = result; let value: i32 = item; }", + }, + { + name: "dynamic indexed source", + source: `struct Cursor {} +fn (self: &mut Cursor) Next() -> ?i32 { return none; } +fn main() { + let mut cursors = [2]Cursor{Cursor.{}, Cursor.{}}; + let index: i32 = 0; + __LOOP__ +}`, + implicit: "for item in cursors[index].Next() { let value: i32 = item; }", + explicit: "for { let result = cursors[index].Next(); if result == none { break; } let item: i32 = result; let value: i32 = item; }", + }, + { + name: "reference parameter source", + source: `struct Cursor {} +fn (self: &mut Cursor) Next() -> ?i32 { return none; } +fn Walk(cursor: &mut Cursor) { __LOOP__ } +fn main() { let mut cursor = Cursor.{}; Walk(&mut cursor); }`, + implicit: "for item in cursor.Next() { let value: i32 = item; }", + explicit: "for { let result = cursor.Next(); if result == none { break; } let item: i32 = result; let value: i32 = item; }", + }, + { + name: "aggregate item", + source: `struct Item { value: i32 } +struct Cursor {} +fn (self: &Cursor) Next() -> ?Item { return none; } +fn main() { let cursor = Cursor.{}; __LOOP__ }`, + implicit: "for item in cursor.Next() { let value: i32 = item.value; }", + explicit: "for { let result = cursor.Next(); if result == none { break; } let item: Item = result; let value: i32 = item.value; }", + }, + { + name: "owned item", + source: `struct Cursor {} +fn (self: &Cursor) Next() -> ?*i32 { return none; } +fn main() { let cursor = Cursor.{}; __LOOP__ }`, + implicit: "for item in cursor.Next() { free(item); }", + explicit: "for { let result = cursor.Next(); if result == none { break; } let item: *i32 = result; free(item); }", + }, + { + name: "reference item", + source: `struct Cursor { value: i32 } +fn (self: &mut Cursor) Next() -> ?&i32 from self { return none; } +fn main() { let mut cursor = Cursor.{ value = 1 }; __LOOP__ }`, + implicit: "for item in cursor.Next() { let value: &i32 = item; }", + explicit: "for { let result = cursor.Next(); if result == none { break; } let item: &i32 = result; let value: &i32 = item; }", + }, + } { + t.Run(test.name, func(t *testing.T) { + for _, implicit := range []bool{false, true} { + loop := test.explicit + if implicit { + loop = test.implicit + } + module, diag := checkTypeModule(t, strings.Replace(test.source, "__LOOP__", loop, 1)) + if diag.HasErrors() { + t.Fatalf("implicit=%v unexpected diagnostics:\n%s", implicit, diag.EmitAllToString()) + } + expectedExpansions := 0 + if implicit { + expectedExpansions = 1 + } + if got := len(module.Typechecking.CheckedIterations); got != expectedExpansions { + t.Fatalf("implicit=%v checked expansions = %d", implicit, got) + } + } + }) + } +} + +func TestRejectedCallIterationStillChecksBody(t *testing.T) { + for _, test := range []struct{ header, diagnostic string }{ + {"item in cursor.Next()", "must return an optional item"}, + {"item in cursor", "cannot iterate over"}, + {"index, item in Produce(1)", "provide an item, not an index"}, + {"item in Produce(true)", "cannot implicitly convert bool to i32"}, + {"item in Missing()", "Missing"}, + } { + t.Run(test.header, func(t *testing.T) { + _, diag := checkTypeModule(t, `struct Cursor {} +fn (self: &Cursor) Next() -> i32 { return 0; } +fn Produce(value: i32) -> ?i32 { return none; } +fn main() { + let cursor = Cursor.{}; + for `+test.header+` { let invalid: bool = 1; } +}`) + text := diag.EmitAllToString() + if !strings.Contains(text, test.diagnostic) || !strings.Contains(text, "cannot be used as bool") { + t.Fatalf("expected header and body diagnostics:\n%s", text) + } + }) + } +} + func TestCheckForInOverRange(t *testing.T) { src := `fn main() -> i32 { let mut total: i32 = 0; diff --git a/internal/semantics/typechecker/optional_redundancy_test.go b/internal/semantics/typechecker/optional_redundancy_test.go new file mode 100644 index 0000000..03ab7a3 --- /dev/null +++ b/internal/semantics/typechecker/optional_redundancy_test.go @@ -0,0 +1,107 @@ +package typechecker + +import ( + "fmt" + "os" + "strings" + "testing" + + "compiler/internal/diagnostics" + "compiler/internal/semantics/typeinfo" +) + +func TestOptionalForwardGenericField(t *testing.T) { + declarations := []string{ + "type Early = Box;", + "struct Box { value: ?Later }", + "type Later = ?i32;", + } + for first := range len(declarations) { + for second := range len(declarations) { + if first == second { + continue + } + t.Run(fmt.Sprintf("order_%d%d", first, second), func(t *testing.T) { + source := strings.Join([]string{declarations[first], declarations[second], declarations[3-first-second]}, "\n") + _, diag := checkFlowSource(t, source+` +fn Read(box: &Early) -> i32 { + if box.value == none { return 0; } + return box.value; +}`) + if diag.HasErrors() { + t.Fatalf("forward generic field must need one proof:\n%s", diag.EmitAllToString()) + } + }) + } + } +} + +func TestOptionalRedundancyCanonicalTypes(t *testing.T) { + for _, test := range []struct { + name string + declarations string + spelling string + notes int + }{ + {"explicit double", "", "??i32", 1}, + {"explicit triple", "", "???i32", 2}, + {"explicit spaced", "", "? ?i32", 1}, + {"alias", "type Maybe = ?i32;", "?Maybe", 0}, + {"forward alias", "type Maybe = ?Later; type Later = ?i32;", "Maybe", 0}, + {"generic", "type Maybe = ?T;", "Maybe", 0}, + {"wrapped generic", "type Maybe = ?T;", "?Maybe", 0}, + } { + t.Run(test.name, func(t *testing.T) { + module, diag := checkFlowSource(t, test.declarations+` +fn Some() -> ?i32 { return 42; } +fn Wrap() -> `+test.spelling+` { return Some(); } +fn Read() -> i32 { + let value = Wrap(); + if value == none { return 0; } + return value; +}`) + if diag.HasErrors() { + t.Fatalf("unexpected errors:\n%s", diag.EmitAllToString()) + } + notes := 0 + for _, item := range diag.Diagnostics() { + if item.Code == diagnostics.InfoRedundantOptional { + notes++ + if item.Severity != diagnostics.Info { + t.Fatalf("severity = %v", item.Severity) + } + } + } + if notes != test.notes { + t.Fatalf("notes = %d, want %d:\n%s", notes, test.notes, diag.EmitAllToString()) + } + wrap, ok := module.ModuleScope.LookupLocal("Wrap") + if !ok { + t.Fatal("missing Wrap symbol") + } + fn, ok := typeinfo.Unalias(wrap.Type).(*typeinfo.FuncType) + if !ok { + t.Fatalf("Wrap type = %T", wrap.Type) + } + optional, ok := typeinfo.Unalias(fn.Return).(*typeinfo.OptionalType) + if !ok || typeinfo.TypeText(optional.Inner) != "i32" { + t.Fatalf("return type = %s, want one optional i32 carrier", typeinfo.TypeText(fn.Return)) + } + descriptor, ok := typeinfo.VariantDescriptorOf(fn.Return) + if !ok || len(descriptor.Cases) != 2 || typeinfo.TypeText(descriptor.Cases[1].Payload) != "i32" { + t.Fatalf("optional representation = %#v", descriptor) + } + }) + } +} + +func TestOptionalRedundancyFixtureSemantics(t *testing.T) { + source, err := os.ReadFile("../../../x_test/runtime_optional_redundancy/src/main.peep") + if err != nil { + t.Fatal(err) + } + _, diag := checkFlowSource(t, string(source)) + if diag.HasErrors() { + t.Fatalf("fixture semantic errors:\n%s", diag.EmitAllToString()) + } +} diff --git a/internal/semantics/typechecker/typechecker.go b/internal/semantics/typechecker/typechecker.go index 26e4220..49b34d5 100644 --- a/internal/semantics/typechecker/typechecker.go +++ b/internal/semantics/typechecker/typechecker.go @@ -17,6 +17,9 @@ type checker struct { optionalTestContext int wholeCarrierExpr ast.Expr loopDepth int + // reusedCall is the already-checked source call embedded at the root of a + // generated producer loop. Nested calls and flow visits still check normally. + reusedCall *ast.CallExpr } // Concrete references convert to satisfied interface borrows, while owned diff --git a/internal/semantics/typecheckresult/result.go b/internal/semantics/typecheckresult/result.go index 1e68d7d..e46cbda 100644 --- a/internal/semantics/typecheckresult/result.go +++ b/internal/semantics/typecheckresult/result.go @@ -146,7 +146,14 @@ type Result struct { CaseTests map[ast.NodeID]CaseTest Matches map[ast.NodeID]Match ForIterations map[ast.NodeID]ForIteration - ExprTypes map[ast.NodeID]typeinfo.Type + // CheckedIterations contains optional-producing calls expanded into ordinary checked + // statements before CFG/flow/effects/ownership. These loops have no numeric + // cursor and never appear in the builtin ForIterations table. Source syntax + // stays unchanged; CFG, typed-node indexing and HIR consume this same tree. + // Each block contains the checked loop, which retains the source loop's ID. + // The block has its own scope ID; the whole call runs inside the loop. + CheckedIterations map[ast.NodeID]*ast.BlockStmt + ExprTypes map[ast.NodeID]typeinfo.Type // ValueUses classifies every ownership-relevant value use, keyed by the // used expression's node ID. Reference parameters publish UseRead; the // borrow machinery in ownership still governs them. @@ -174,6 +181,7 @@ func New() *Result { CaseTests: make(map[ast.NodeID]CaseTest), Matches: make(map[ast.NodeID]Match), ForIterations: make(map[ast.NodeID]ForIteration), + CheckedIterations: make(map[ast.NodeID]*ast.BlockStmt), ExprTypes: make(map[ast.NodeID]typeinfo.Type), ValueUses: make(map[ast.NodeID]typeinfo.UseKind), ReferenceArguments: make(map[ast.NodeID]bool), diff --git a/internal/semantics/typeinfo/relations.go b/internal/semantics/typeinfo/relations.go index 04fdd19..62f8dca 100644 --- a/internal/semantics/typeinfo/relations.go +++ b/internal/semantics/typeinfo/relations.go @@ -350,7 +350,7 @@ func ReplaceAbstractSelf(t Type, ownerType Type) Type { if typ == nil { return nil } - return &OptionalType{Inner: ReplaceAbstractSelf(typ.Inner, ownerType)} + return NewOptional(ReplaceAbstractSelf(typ.Inner, ownerType)) case *ArrayType: if typ == nil { return nil diff --git a/internal/semantics/typeinfo/syntax.go b/internal/semantics/typeinfo/syntax.go index 2ee431b..e69b0cb 100644 --- a/internal/semantics/typeinfo/syntax.go +++ b/internal/semantics/typeinfo/syntax.go @@ -94,7 +94,7 @@ func TypeFromSyntax(node ast.TypeExpr, opts SyntaxOptions) Type { if typ == nil { return nil } - return &OptionalType{Inner: TypeFromSyntax(typ.Inner, opts)} + return NewOptional(TypeFromSyntax(typ.Inner, opts)) case *ast.ArrayType: if typ == nil { return nil diff --git a/internal/semantics/typeinfo/types.go b/internal/semantics/typeinfo/types.go index 6da4528..3f895d5 100644 --- a/internal/semantics/typeinfo/types.go +++ b/internal/semantics/typeinfo/types.go @@ -277,6 +277,26 @@ func TypeParameterBindings(parameters []*TypeParameterType, arguments []Type) ma return bindings } +// NewOptional collapses consecutive optional layers, including transparent +// aliases, without crossing nominal or pointer/array boundaries. Callers must +// resolve alias dependencies before construction; nominal recursive shells may +// remain incomplete. +func NewOptional(inner Type) Type { + seen := make(map[Type]bool) + for { + inner = Unalias(inner) + optional, ok := inner.(*OptionalType) + if !ok || optional == nil { + return &OptionalType{Inner: inner} + } + if seen[optional] { + return &InvalidType{} + } + seen[optional] = true + inner = optional.Inner + } +} + // Unalias returns canonical transparent-alias storage without erasing nominal // structs, interfaces, or enums. Invalid alias cycles terminate as invalid. func Unalias(t Type) Type { diff --git a/x_test/negative_structural_iterator/peeper.toml b/x_test/negative_structural_iterator/peeper.toml new file mode 100644 index 0000000..6a5d676 --- /dev/null +++ b/x_test/negative_structural_iterator/peeper.toml @@ -0,0 +1,8 @@ +name = "negative_structural_iterator" +build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["must return an optional item", "provide an item, not an index", "mutable", "cannot iterate over", "cannot be used as bool", "maintain a separate counter", "explicit call returning an optional item", "a bare optional value is not a producer"] +stderr_excludes = ["structural iteration", "local concrete cursor", "scalar items", "$for."] diff --git a/x_test/negative_structural_iterator/src/main.peep b/x_test/negative_structural_iterator/src/main.peep new file mode 100644 index 0000000..f8ee813 --- /dev/null +++ b/x_test/negative_structural_iterator/src/main.peep @@ -0,0 +1,23 @@ +struct WrongReturn {} +fn (self: &WrongReturn) Next() -> i32 { return 1; } + +struct Cursor {} +fn (self: &mut Cursor) Next() -> ?i32 { return none; } +fn Produce(value: bool) -> ?i32 { return none; } + +iface Erased { fn (&mut Self) Next() -> ?i32 } +fn RejectBareErased(cursor: &mut Erased) { for item in cursor {} } + +fn main() { + let wrong = WrongReturn.{}; + for item in wrong.Next() { let invalid: bool = 1; } + let mut cursor = Cursor.{}; + for item in cursor {} + for index, item in cursor.Next() {} + let immutable = Cursor.{}; + for item in immutable.Next() {} + let optional: ?i32 = none; + for item in optional {} + for item in Produce {} + for item in Produce(1) {} +} diff --git a/x_test/negative_structural_iterator_move/peeper.toml b/x_test/negative_structural_iterator_move/peeper.toml new file mode 100644 index 0000000..6e9fd07 --- /dev/null +++ b/x_test/negative_structural_iterator_move/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_structural_iterator_move" +build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["moved"] diff --git a/x_test/negative_structural_iterator_move/src/main.peep b/x_test/negative_structural_iterator_move/src/main.peep new file mode 100644 index 0000000..c5106c7 --- /dev/null +++ b/x_test/negative_structural_iterator_move/src/main.peep @@ -0,0 +1,14 @@ +struct Consumed { value: i32, limit: i32 } +fn (self: Consumed) Next() -> ?i32 { return self.value; } + +struct Cursor { value: i32, limit: i32 } +fn (self: &mut Cursor) Next() -> ?i32 { return none; } +fn Consume(cursor: Cursor) {} + +fn main() { + let consumed = Consumed.{ value = 0, limit = 2 }; + for item in consumed.Next() {} + let mut moved = Cursor.{ value = 0, limit = 2 }; + Consume(moved); + for item in moved.Next() {} +} diff --git a/x_test/runtime_iterator_factory/peeper.toml b/x_test/runtime_iterator_factory/peeper.toml new file mode 100644 index 0000000..4ad9d65 --- /dev/null +++ b/x_test/runtime_iterator_factory/peeper.toml @@ -0,0 +1,6 @@ +name = "runtime_iterator_factory" +build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_iterator_factory/src/main.peep b/x_test/runtime_iterator_factory/src/main.peep new file mode 100644 index 0000000..8be52b8 --- /dev/null +++ b/x_test/runtime_iterator_factory/src/main.peep @@ -0,0 +1,93 @@ +struct Argument { held: *i32, serial: i32 } +struct Counts { made: i32, ticks: i32, phase: i32, errors: i32 } +struct Cursor { value: i32, limit: i32, calls: i32 } + +fn Make(counts: &mut Counts) -> Argument { + if counts.phase == 1 { counts.errors = counts.errors + 1; } + counts.phase = 1; + counts.made = counts.made + 1; + return Argument.{ held = alloc(7), serial = counts.made }; +} + +fn Tick(counts: &mut Counts) -> i32 { + if counts.phase != 1 { counts.errors = counts.errors + 1; } + counts.phase = 2; + counts.ticks = counts.ticks + 1; + return counts.made; +} + +// The owned argument uses ordinary parameter cleanup on present and none returns. +fn Produce(argument: Argument, tick: i32, cursor: &mut Cursor, offset: i32 = 0) -> ?i32 { + cursor.calls = cursor.calls + 1; + if argument.serial != tick { return 1000; } + if cursor.value >= cursor.limit { return none; } + let value = cursor.value; + cursor.value = cursor.value + 1; + return value + offset; +} + +// Separate loop spellings are deliberate: compare normal calls against expansion. +fn Run(expanded: bool, mode: i32, limit: i32, counts: &mut Counts) -> i32 { + let mut cursor = Cursor.{ value = 0, limit = limit, calls = 0 }; + let mut total: i32 = 0; + if expanded { + for item in Produce(Make(counts), Tick(counts), &mut cursor) { + if item == 1000 || cursor.calls > 10 { return -100; } + let owned = alloc(item); + if mode == 1 { return item + 10; } + if mode == 2 { break; } + if item == 1 { continue; } + total = total + item; + } + } else { + for { + let result = Produce(Make(counts), Tick(counts), &mut cursor); + if result == none { break; } + let item: i32 = result; + if item == 1000 || cursor.calls > 10 { return -100; } + let owned = alloc(item); + if mode == 1 { return item + 10; } + if mode == 2 { break; } + if item == 1 { continue; } + total = total + item; + } + } + return total * 100 + cursor.calls; +} + +fn NestedReturn(counts: &mut Counts) -> i32 { + let mut outer = Cursor.{ value = 0, limit = 2, calls = 0 }; + for left in Produce(Make(counts), Tick(counts), &mut outer) { + let mut inner = Cursor.{ value = 0, limit = 3, calls = 0 }; + for right in Produce(Make(counts), Tick(counts), &mut inner) { + return left + right; + } + } + return -1; +} + +fn main() -> i32 { + for mode in 0..3 { + let mut expanded = Counts.{ made = 0, ticks = 0, phase = 0, errors = 0 }; + let mut explicit = Counts.{ made = 0, ticks = 0, phase = 0, errors = 0 }; + let actual = Run(true, mode, 4, &mut expanded); + let expected = Run(false, mode, 4, &mut explicit); + if actual != expected || actual < 0 { return 1; } + if expanded.made != explicit.made || expanded.ticks != explicit.ticks { return 2; } + if expanded.errors != 0 || explicit.errors != 0 || expanded.phase != 2 { return 3; } + if mode == 0 { + if actual != 505 || expanded.made != 5 || expanded.ticks != 5 { return 4; } + } else { + if expanded.made != 1 || expanded.ticks != 1 { return 5; } + if mode == 1 && actual != 10 { return 6; } + if mode == 2 && actual != 1 { return 7; } + } + } + let mut empty = Counts.{ made = 0, ticks = 0, phase = 0, errors = 0 }; + if Run(true, 0, 0, &mut empty) != 1 { return 8; } + if empty.made != 1 || empty.ticks != 1 || empty.errors != 0 { return 9; } + let mut nested = Counts.{ made = 0, ticks = 0, phase = 0, errors = 0 }; + if NestedReturn(&mut nested) != 0 { return 10; } + if nested.made != 2 || nested.ticks != 2 || nested.errors != 0 { return 11; } + return 0; +} diff --git a/x_test/runtime_iterator_general/peeper.toml b/x_test/runtime_iterator_general/peeper.toml new file mode 100644 index 0000000..3a8deb7 --- /dev/null +++ b/x_test/runtime_iterator_general/peeper.toml @@ -0,0 +1,6 @@ +name = "runtime_iterator_general" +build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_iterator_general/src/main.peep b/x_test/runtime_iterator_general/src/main.peep new file mode 100644 index 0000000..a15a333 --- /dev/null +++ b/x_test/runtime_iterator_general/src/main.peep @@ -0,0 +1,83 @@ +struct Cursor { value: i32, limit: i32 } +fn (self: &mut Cursor) Next() -> ?i32 { + if self.value >= self.limit { return none; } + let value = self.value; + self.value = self.value + 1; + return value; +} + +struct Holder { cursor: Cursor, marker: i32 } + +fn Sum(cursor: &mut Cursor) -> i32 { + let mut total: i32 = 0; + for item in cursor.Next() { total = total + item; } + return total; +} + +struct Pair { left: i32, right: i32 } +struct PairCursor { value: i32, limit: i32 } +fn (self: &mut PairCursor) Next() -> ?Pair { + if self.value >= self.limit { return none; } + let value = self.value; + self.value = self.value + 1; + return Pair.{ left = value, right = value + 10 }; +} + +struct OwnerCursor { value: i32, limit: i32 } +fn (self: &mut OwnerCursor) Next() -> ?*i32 { + if self.value >= self.limit { return none; } + let value = self.value; + self.value = self.value + 1; + return alloc(value); +} + +struct RefCursor { value: i32, done: bool } +fn (self: &mut RefCursor) Next() -> ?&i32 from self { + if self.done { return none; } + self.done = true; + return &self.value; +} + +fn main() -> i32 { + let mut holder = Holder.{ cursor = Cursor.{ value = 0, limit = 3 }, marker = 9 }; + let mut fieldTotal: i32 = 0; + for item in holder.cursor.Next() { fieldTotal = fieldTotal + item; } + if fieldTotal != 3 || holder.cursor.value != 3 || holder.marker != 9 { return 1; } + + let mut cursors = [2]Cursor{ + Cursor.{ value = 0, limit = 2 }, + Cursor.{ value = 10, limit = 12 } + }; + let mut selected: i32 = 0; + let mut index: i32 = 0; + for item in cursors[index].Next() { + selected = selected + item; + index = 1; + } + if selected != 21 || cursors[0].value != 1 || cursors[1].value != 12 { return 2; } + + let mut parameter = Cursor.{ value = 2, limit = 5 }; + if Sum(&mut parameter) != 9 || parameter.value != 5 { return 3; } + + let mut pairs = PairCursor.{ value = 0, limit = 2 }; + let mut pairTotal: i32 = 0; + for pair in pairs.Next() { pairTotal = pairTotal + pair.left + pair.right; } + if pairTotal != 22 { return 4; } + + let mut owners = OwnerCursor.{ value = 0, limit = 3 }; + let mut ownerCount: i32 = 0; + for owned in owners.Next() { + ownerCount = ownerCount + 1; + free(owned); + } + if ownerCount != 3 { return 5; } + + let mut refs = RefCursor.{ value = 42, done = false }; + let mut referenced: i32 = 0; + for reference in refs.Next() { + let item: &i32 = reference; + referenced = referenced + 1; + } + if referenced != 1 { return 7; } + return 0; +} diff --git a/x_test/runtime_optional_narrowing/src/main.peep b/x_test/runtime_optional_narrowing/src/main.peep index 39d4d1f..f948bc4 100644 --- a/x_test/runtime_optional_narrowing/src/main.peep +++ b/x_test/runtime_optional_narrowing/src/main.peep @@ -182,12 +182,11 @@ fn main() -> i32 { return 7; } let outer: ? ?i32 = inner; - if outer != none { - if outer != none { - if outer != 13 { - return 7; - } - } + if outer == none { + return 7; + } + if outer != 13 { + return 7; } let ownerResult = TakeOptional(Token.{value = 23}); diff --git a/x_test/runtime_optional_redundancy/peeper.toml b/x_test/runtime_optional_redundancy/peeper.toml new file mode 100644 index 0000000..5d19722 --- /dev/null +++ b/x_test/runtime_optional_redundancy/peeper.toml @@ -0,0 +1,6 @@ +name = "runtime_optional_redundancy" +build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_optional_redundancy/src/main.peep b/x_test/runtime_optional_redundancy/src/main.peep new file mode 100644 index 0000000..9fd9d55 --- /dev/null +++ b/x_test/runtime_optional_redundancy/src/main.peep @@ -0,0 +1,51 @@ +type Maybe = ?i32; +type Optional = ?T; + +type Early = Box; +struct Box { value: ?Later } +type Later = ?i32; + +fn Read(box: &Early) -> i32 { + if box.value == none { return 0; } + return box.value; +} + +type Linked = Node; +struct Node { link: ?Link } +type Link = ?*Node; + +fn TakeLink(link: ?Link) -> *Node { + if link == none { return alloc(Node.{ link = none }); } + return link; +} + +fn Some() -> ?i32 { return 42; } +fn Wrapped() -> ?Maybe { return Some(); } +fn Absent() -> ?Optional { return none; } + +fn main() -> i32 { + let double: ??i32 = Wrapped(); + if double == none { return 1; } + if double != 42 { return 2; } + + let triple: ???i32 = Some(); + if triple == none { return 3; } + if triple != 42 { return 4; } + + let spaced: ? ?i32 = Some(); + if spaced == none { return 5; } + if spaced != 42 { return 6; } + + let generic: Optional = Wrapped(); + if generic == none { return 7; } + if generic != 42 { return 8; } + + let absent: ??i32 = Absent(); + if absent != none { return 9; } + + let present = Early.{ value = 42 }; + if Read(&present) != 42 { return 10; } + let empty = Early.{ value = none }; + if Read(&empty) != 0 { return 11; } + return 0; +} diff --git a/x_test/runtime_structural_iterator/peeper.toml b/x_test/runtime_structural_iterator/peeper.toml new file mode 100644 index 0000000..62f0c34 --- /dev/null +++ b/x_test/runtime_structural_iterator/peeper.toml @@ -0,0 +1,6 @@ +name = "runtime_structural_iterator" +build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_structural_iterator/src/main.peep b/x_test/runtime_structural_iterator/src/main.peep new file mode 100644 index 0000000..3934f03 --- /dev/null +++ b/x_test/runtime_structural_iterator/src/main.peep @@ -0,0 +1,59 @@ +struct Counter { + value: i32, + limit: i32, + calls: i32 +} + +fn Advance(self: &mut Counter) -> ?i32 { + self.calls = self.calls + 1; + if self.value >= self.limit { return none; } + let value = self.value; + self.value = self.value + 1; + return value; +} + +struct Constant {} +fn (self: &Constant) Next() -> ?i32 { return 9; } + +fn First() -> i32 { + let mut cursor = Counter.{ value = 4, limit = 8, calls = 0 }; + for item in Advance(&mut cursor) { return item; } + return -1; +} + +fn main() -> i32 { + let mut cursor = Counter.{ value = 0, limit = 5, calls = 0 }; + let mut total: i32 = 0; + for item in cursor |> Advance() { + let owned = alloc(item); + if item == 1 { continue; } + total = total + item; + if item == 2 { break; } + } + if total != 2 || cursor.value != 3 || cursor.calls != 3 { return 1; } + for item in Advance(&mut cursor) { total = total + item; } + if total != 9 || cursor.value != 5 || cursor.calls != 6 { return 2; } + for item in Advance(&mut cursor) { return 3; } + if cursor.calls != 7 { return 4; } + + let mut empty = Counter.{ value = 0, limit = 0, calls = 0 }; + for item in empty |> Advance() { return 5; } + if empty.calls != 1 { return 6; } + + let mut outer = Counter.{ value = 0, limit = 3, calls = 0 }; + let mut pairs: i32 = 0; + for left in Advance(&mut outer) { + let mut inner = Counter.{ value = 0, limit = 2, calls = 0 }; + for right in inner |> Advance() { pairs = pairs + left + right; } + if inner.calls != 3 { return 7; } + } + if pairs != 9 || outer.calls != 4 { return 8; } + + let constant = Constant.{}; + for item in constant.Next() { + if item != 9 { return 9; } + break; + } + if First() != 4 { return 10; } + return 0; +}