From f88fd0fd25af370f8d770a5b39b12e2fea2edff8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 25 Aug 2025 17:57:28 +0200 Subject: [PATCH] Implemented resource safety --- docs/readme.md | 87 ++++++++++++- lib/fs/fs_linux.q | 4 +- lib/fs/fs_mac.q | 4 +- lib/fs/fs_windows.q | 4 +- src/compiler/parseTypes.go | 1 + src/core/Compile.go | 9 +- src/core/ParseType.go | 11 ++ src/core/checkResources.go | 34 +++++ src/core/compileAssign.go | 9 +- src/core/compileCondition.go | 4 +- src/core/compileIf.go | 16 ++- src/core/compileLoop.go | 56 +++----- src/core/decompose.go | 28 +++- src/core/define.go | 8 +- src/core/errors.go | 9 ++ src/core/errors_test.go | 6 + src/core/testdata/ResourceAlreadyConsumed.q | 10 ++ src/core/testdata/ResourceAlreadyConsumed2.q | 10 ++ src/core/testdata/ResourceAlreadyConsumed3.q | 9 ++ src/core/testdata/ResourceAlreadyConsumed4.q | 10 ++ src/core/testdata/ResourceNotConsumed.q | 7 + src/core/testdata/ResourceTypeMismatch.q | 9 ++ src/scanner/scanSignature.go | 12 +- src/ssa/Block.go | 128 +++++++++++-------- src/ssa/Block_test.go | 9 +- src/ssa/IR.go | 29 ++++- src/types/Is.go | 12 ++ src/types/Resource.go | 17 +++ tests/resource.q | 9 ++ tests/tests_test.go | 1 + 30 files changed, 422 insertions(+), 140 deletions(-) create mode 100644 src/core/checkResources.go create mode 100644 src/core/testdata/ResourceAlreadyConsumed.q create mode 100644 src/core/testdata/ResourceAlreadyConsumed2.q create mode 100644 src/core/testdata/ResourceAlreadyConsumed3.q create mode 100644 src/core/testdata/ResourceAlreadyConsumed4.q create mode 100644 src/core/testdata/ResourceNotConsumed.q create mode 100644 src/core/testdata/ResourceTypeMismatch.q create mode 100644 src/types/Resource.go create mode 100644 tests/resource.q diff --git a/docs/readme.md b/docs/readme.md index fa07386..b14492d 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -9,6 +9,7 @@ - High performance (comparable to C and Go) - Fast compilation (5x faster than most) - Tiny executables ("Hello World" is ~600 bytes) +- Resource safety (use-after-free is a compile error) - Multiple platforms (Linux, Mac and Windows) - Zero dependencies (no llvm, no libc) @@ -57,7 +58,8 @@ q build examples/hello --os [linux|mac|windows] --arch [x86|arm] ## News -- **2025-08-23**: Compile-time function polymorphism. +- **2025-08-25**: Resource safety. +- **2025-08-23**: Function overloading. - **2025-08-22**: General bugfixes. - **2025-08-19**: Performance improvements. - **2025-08-18**: Slices for strings. @@ -117,8 +119,91 @@ The following is a cheat sheet documenting the syntax. | Free memory | `mem.free(buffer)` | 🚧 Experimental | | Output a string | `io.write("Hello\n")` | ✔️ Stable | | Output an integer | `io.write(42)` | ✔️ Stable | +| Mark a type as a resource | `!int` | 🚧 Experimental | | Mark a parameter as unused | `_` | ✔️ Stable | +## Resources + +> [!WARNING] +> This feature is very new and still undergoing refinement. + +Resources are shared objects such as files, memory or database handles. The use of resource types prevents the following problems: + +- **Resource leaks** (forgetting to free a resource) +- **Use-after-free** (using a resource after it was freed) +- **Double-free** (freeing a resource twice) + +Any type, even integers, can be turned into a resource by prefixing the type with `!`. For example, consider these minimal functions: + +``` +acquire() -> !int { return 1 } +use(_ int) {} +free(_ !int) {} +``` + +With this, forgetting to call `free` becomes impossible: + +``` +main() { + x := acquire() + use(x) +} +``` + +``` + x := acquire() + ┬ + ╰─ Resource of type '!int' not consumed +``` + +Attempting a use-after-free is also rejected: + +``` +main() { + x := acquire() + use(x) + free(x) + use(x) +} +``` + +``` + free(x) + use(x) + ┬ + ╰─ Unknown identifier 'x' +``` + +Likewise, a double-free is disallowed: + +``` +main() { + x := acquire() + use(x) + free(x) + free(x) +} +``` + +``` + free(x) + free(x) + ┬ + ╰─ Unknown identifier 'x' +``` + +The compiler only accepts the correct usage order: + +``` +main() { + x := acquire() + use(x) + free(x) +} +``` + +The `!` prefix marks a type to be consumed exactly once. When a `!int` is passed to another `!int`, the original variable is invalidated in subsequent code. As an exception, converting `!int` to `int` bypasses this rule, allowing multiple uses. + ## Source The source code structure uses a flat layout without nesting: diff --git a/lib/fs/fs_linux.q b/lib/fs/fs_linux.q index 7d2b632..e75ce85 100644 --- a/lib/fs/fs_linux.q +++ b/lib/fs/fs_linux.q @@ -1,4 +1,4 @@ -open(path *byte, flags int, mode int) -> int { +open(path *byte, flags int, mode int) -> !int { return syscall(_openat, -100, path, flags, mode) } @@ -8,6 +8,6 @@ size(fd int) -> int { return stats.st_size } -close(fd int) -> int { +close(fd !int) -> int { return syscall(_close, fd) } \ No newline at end of file diff --git a/lib/fs/fs_mac.q b/lib/fs/fs_mac.q index 6657efa..6ef8926 100644 --- a/lib/fs/fs_mac.q +++ b/lib/fs/fs_mac.q @@ -1,4 +1,4 @@ -open(path *byte, flags int, mode int) -> int { +open(path *byte, flags int, mode int) -> !int { return syscall(_open, path, flags, mode) } @@ -8,6 +8,6 @@ size(fd int) -> int { return stats.st_size } -close(fd int) -> int { +close(fd !int) -> int { return syscall(_close, fd) } \ No newline at end of file diff --git a/lib/fs/fs_windows.q b/lib/fs/fs_windows.q index 678679d..d8333df 100644 --- a/lib/fs/fs_windows.q +++ b/lib/fs/fs_windows.q @@ -1,4 +1,4 @@ -open(path *byte, _flags int, _mode int) -> int { +open(path *byte, _flags int, _mode int) -> !int { return kernel32.CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) } @@ -8,7 +8,7 @@ size(fd int) -> int { return [fileSize] } -close(fd int) -> bool { +close(fd !int) -> bool { return kernel32.CloseHandle(fd) } diff --git a/src/compiler/parseTypes.go b/src/compiler/parseTypes.go index 14480ac..be7f9a5 100644 --- a/src/compiler/parseTypes.go +++ b/src/compiler/parseTypes.go @@ -32,6 +32,7 @@ func parseTypes(functions iter.Seq[*core.Function], env *core.Environment) error typeTokens := output.Tokens if len(output.Tokens) > 1 && output.Tokens[0].Kind == token.Identifier { + output.Name = output.Tokens[0].String(f.File.Bytes) typeTokens = typeTokens[1:] } diff --git a/src/core/Compile.go b/src/core/Compile.go index ccc7abc..2ef3c86 100644 --- a/src/core/Compile.go +++ b/src/core/Compile.go @@ -20,5 +20,12 @@ func (f *Function) Compile() { } f.Finalize() - f.Err = f.removeDeadCode() + err = f.removeDeadCode() + + if err != nil { + f.Err = err + return + } + + f.Err = f.checkResources() } \ No newline at end of file diff --git a/src/core/ParseType.go b/src/core/ParseType.go index 1b9c3b9..22033a4 100644 --- a/src/core/ParseType.go +++ b/src/core/ParseType.go @@ -11,6 +11,17 @@ func ParseType[T ~[]token.Token](tokens T, source []byte, env *Environment) type return nil } + if tokens[0].Kind == token.Not { + to := tokens[1:] + typ := ParseType(to, source, env) + + if typ == nil { + return nil + } + + return &types.Resource{Of: typ} + } + if tokens[0].Kind == token.Mul { to := tokens[1:] typ := ParseType(to, source, env) diff --git a/src/core/checkResources.go b/src/core/checkResources.go new file mode 100644 index 0000000..d2a13c2 --- /dev/null +++ b/src/core/checkResources.go @@ -0,0 +1,34 @@ +package core + +import ( + "git.urbach.dev/cli/q/src/errors" + "git.urbach.dev/cli/q/src/ssa" + "git.urbach.dev/cli/q/src/types" +) + +// checkResources checks for resources that were not deconstructed. +func (f *Function) checkResources() error { + for exitBlock := range f.ExitBlocks { + for _, value := range exitBlock.Identifiers { + if value == nil { + continue + } + + _, isParam := value.(*ssa.Parameter) + + if isParam { + continue + } + + resource, isResource := value.Type().(*types.Resource) + + if !isResource { + continue + } + + return errors.New(&ResourceNotConsumed{TypeName: resource.Name()}, f.File, value.(ssa.HasSource).Start()) + } + } + + return nil +} \ No newline at end of file diff --git a/src/core/compileAssign.go b/src/core/compileAssign.go index 45bc80f..14fed2a 100644 --- a/src/core/compileAssign.go +++ b/src/core/compileAssign.go @@ -5,6 +5,7 @@ import ( "git.urbach.dev/cli/q/src/errors" "git.urbach.dev/cli/q/src/ssa" "git.urbach.dev/cli/q/src/token" + "git.urbach.dev/cli/q/src/types" ) // compileAssign compiles an assignment. @@ -34,7 +35,13 @@ func (f *Function) compileAssign(node *ast.Assign) error { } if f.IsIdentified(rightValue) { - rightValue = f.copy(rightValue, right.Source()) + _, isResource := rightValue.Type().(*types.Resource) + + if isResource { + f.Block().Unidentify(rightValue) + } else { + rightValue = f.copy(rightValue, right.Source()) + } } if node.Expression.Token.Kind == token.Assign { diff --git a/src/core/compileCondition.go b/src/core/compileCondition.go index 726a5e0..1cc5415 100644 --- a/src/core/compileCondition.go +++ b/src/core/compileCondition.go @@ -15,7 +15,6 @@ func (f *Function) compileCondition(condition *expression.Expression, thenBlock f.Count.SubBranch++ leftFailLabel := f.CreateLabel("or", f.Count.SubBranch) leftFail := ssa.NewBlock(leftFailLabel) - f.Block().AddSuccessor(leftFail) // Left left := condition.Children[0] @@ -25,6 +24,7 @@ func (f *Function) compileCondition(condition *expression.Expression, thenBlock return err } + f.Block().AddSuccessor(leftFail) f.AddBlock(leftFail) // Right @@ -36,7 +36,6 @@ func (f *Function) compileCondition(condition *expression.Expression, thenBlock f.Count.SubBranch++ leftSuccessLabel := f.CreateLabel("and", f.Count.SubBranch) leftSuccess := ssa.NewBlock(leftSuccessLabel) - f.Block().AddSuccessor(leftSuccess) // Left left := condition.Children[0] @@ -46,6 +45,7 @@ func (f *Function) compileCondition(condition *expression.Expression, thenBlock return err } + f.Block().AddSuccessor(leftSuccess) f.AddBlock(leftSuccess) // Right diff --git a/src/core/compileIf.go b/src/core/compileIf.go index 9f79fe8..c3b409d 100644 --- a/src/core/compileIf.go +++ b/src/core/compileIf.go @@ -31,8 +31,12 @@ func (f *Function) compileIf(branch *ast.If) error { return err } - f.Block().AddSuccessor(exitBlock) - f.Append(&ssa.Jump{To: exitBlock}) + _, isReturn := f.Block().Last().(*ssa.Return) + + if !isReturn { + f.Block().AddSuccessor(exitBlock) + f.Append(&ssa.Jump{To: exitBlock}) + } } else { elseLabel := f.CreateLabel("if.else", f.Count.Branch) elseBlock := ssa.NewBlock(elseLabel) @@ -51,8 +55,12 @@ func (f *Function) compileIf(branch *ast.If) error { return err } - f.Block().AddSuccessor(exitBlock) - f.Append(&ssa.Jump{To: exitBlock}) + _, isReturn := f.Block().Last().(*ssa.Return) + + if !isReturn { + f.Block().AddSuccessor(exitBlock) + f.Append(&ssa.Jump{To: exitBlock}) + } // Append the if.else block f.AddBlock(elseBlock) diff --git a/src/core/compileLoop.go b/src/core/compileLoop.go index 0ac3b02..0def8ac 100644 --- a/src/core/compileLoop.go +++ b/src/core/compileLoop.go @@ -3,7 +3,6 @@ package core import ( "git.urbach.dev/cli/q/src/ast" "git.urbach.dev/cli/q/src/errors" - "git.urbach.dev/cli/q/src/set" "git.urbach.dev/cli/q/src/ssa" "git.urbach.dev/cli/q/src/token" ) @@ -38,8 +37,8 @@ func (f *Function) compileLoop(loop *ast.Loop) error { } beforeLoop.Identify(name, fromValue) - beforeLoop.Append(&ssa.Jump{To: loopHead}) beforeLoop.AddSuccessor(loopHead) + beforeLoop.Append(&ssa.Jump{To: loopHead}) // Loop starts, this is the jump target for new iterations. // The upper limit is recalculated on every iteration. @@ -68,7 +67,6 @@ func (f *Function) compileLoop(loop *ast.Loop) error { }) loopHead.AddSuccessor(bodyBlock) - loopHead.AddSuccessor(loopExit) // Loop condition is true from now on so we'll // execute the code inside the loop body. @@ -102,13 +100,18 @@ func (f *Function) compileLoop(loop *ast.Loop) error { } } + // Jump back to the loop head. + f.Append(&ssa.Jump{To: loopHead}) + f.Block().AddSuccessor(loopHead) + // The initial compilation of the loop body does not know // that the code is repeated in a loop. Therefore, we need // to find identifiers that were both defined outside the loop // and modified within the loop. For these identifiers, - // we create phi functions at the top of the loop head. + // we created Phi functions at the top of the loop head. + // All that's left to do is to replace all the occurrences + // of the old values with their new Phi in the loop blocks. loopBlocks := f.Blocks[loopBlockIndex:len(f.Blocks)] - modified := set.Ordered[string]{} for _, block := range loopBlocks { if block.Loop != nil { @@ -117,48 +120,21 @@ func (f *Function) compileLoop(loop *ast.Loop) error { block.Loop = loopHead - for name := range block.Identifiers { - _, existedBeforeLoop := beforeLoop.FindIdentifier(name) + for phi := range loopHead.Phis { + for _, instr := range block.Instructions { + if instr == phi { + continue + } - if existedBeforeLoop { - modified.Add(name) + instr.Replace(phi.Arguments[0], phi) } } } - // Insert phi functions that capture both the value - // outside of the loop and the modification within it. - // We initially only knew about the value outside of the loop, - // so we need to replace all of its occurrences in the loop blocks - // with the new phi function. - replacements := make(map[ssa.Value]*ssa.Phi, modified.Count()) - - for identifier := range modified.All() { - oldValue, _ := beforeLoop.FindIdentifier(identifier) - newValue, _ := f.Block().FindIdentifier(identifier) - phi := &ssa.Phi{Arguments: []ssa.Value{oldValue, newValue}, Typ: oldValue.Type()} - replacement, exists := replacements[oldValue] - - if exists && replacement.Equals(phi) { - loopHead.Identify(identifier, replacement) - continue - } - - replacements[oldValue] = phi - - for _, block := range loopBlocks { - for _, instr := range block.Instructions { - instr.Replace(oldValue, phi) - } - } - - loopHead.InsertAt(phi, 0) - loopHead.Identify(identifier, phi) + if loop.Head != nil { + loopHead.AddSuccessor(loopExit) } - // Jump back to the loop head. - f.Append(&ssa.Jump{To: loopHead}) - f.Block().AddSuccessor(loopHead) f.AddBlock(loopExit) return nil } \ No newline at end of file diff --git a/src/core/decompose.go b/src/core/decompose.go index bc08e1c..3ff92ab 100644 --- a/src/core/decompose.go +++ b/src/core/decompose.go @@ -19,13 +19,27 @@ func (f *Function) decompose(nodes []*expression.Expression, typeCheck []*ssa.Pa return nil, err } - if typeCheck != nil && !types.Is(value.Type(), typeCheck[i].Typ) { - return nil, errors.New(&TypeMismatch{ - Encountered: value.Type().Name(), - Expected: typeCheck[i].Typ.Name(), - ParameterName: typeCheck[i].Name, - IsReturn: isReturn, - }, f.File, node.Source().StartPos) + if typeCheck != nil { + valueType := value.Type() + expectedType := typeCheck[i].Typ + + _, valueIsResource := valueType.(*types.Resource) + expectedResource, expectedIsResource := expectedType.(*types.Resource) + + if valueIsResource && expectedIsResource { + f.Block().Unidentify(value) + } + + if isReturn && expectedIsResource && types.Is(valueType, expectedResource.Of) { + // pass type check. + } else if !types.Is(valueType, expectedType) { + return nil, errors.New(&TypeMismatch{ + Encountered: value.Type().Name(), + Expected: typeCheck[i].Typ.Name(), + ParameterName: typeCheck[i].Name, + IsReturn: isReturn, + }, f.File, node.Source().StartPos) + } } structure, isStruct := value.(*ssa.Struct) diff --git a/src/core/define.go b/src/core/define.go index 666a03d..7227b36 100644 --- a/src/core/define.go +++ b/src/core/define.go @@ -24,7 +24,13 @@ func (f *Function) define(identifier *expression.Expression, value ssa.Value) er // another named variable instead of using the cached value itself // because it could lead to incorrect optimizations. if f.IsIdentified(value) { - value = f.copy(value, identifier.Source()) + _, isResource := value.Type().(*types.Resource) + + if isResource { + f.Block().Unidentify(value) + } else { + value = f.copy(value, identifier.Source()) + } } _, isCall := value.(*ssa.Call) diff --git a/src/core/errors.go b/src/core/errors.go index e1f47cd..68a1ffa 100644 --- a/src/core/errors.go +++ b/src/core/errors.go @@ -58,6 +58,15 @@ func (err *ParameterCountMismatch) Error() string { return fmt.Sprintf("Not enough parameters in '%s' function call", err.Function) } +// ResourceNotConsumed error is created when a resource has not been consumed in an exit block. +type ResourceNotConsumed struct { + TypeName string +} + +func (err *ResourceNotConsumed) Error() string { + return fmt.Sprintf("Resource of type '%s' not consumed", err.TypeName) +} + // ReturnCountMismatch error is created when the number of returned values doesn't match the return type. type ReturnCountMismatch struct { Count int diff --git a/src/core/errors_test.go b/src/core/errors_test.go index 258bcae..02718bb 100644 --- a/src/core/errors_test.go +++ b/src/core/errors_test.go @@ -32,6 +32,12 @@ var errs = []struct { {"NoMatchingFunction.q", &core.NoMatchingFunction{Function: "main.f"}}, {"ParameterCountMismatch.q", &core.ParameterCountMismatch{Function: "main.f", Count: 0, ExpectedCount: 1}}, {"ParameterCountMismatch2.q", &core.ParameterCountMismatch{Function: "main.f", Count: 2, ExpectedCount: 1}}, + {"ResourceAlreadyConsumed.q", &core.UnknownIdentifier{Name: "x"}}, + {"ResourceAlreadyConsumed2.q", &core.UnknownIdentifier{Name: "x"}}, + {"ResourceAlreadyConsumed3.q", &core.UnknownIdentifier{Name: "x"}}, + {"ResourceAlreadyConsumed4.q", &core.UnknownIdentifier{Name: "x"}}, + {"ResourceNotConsumed.q", &core.ResourceNotConsumed{TypeName: "!int64"}}, + {"ResourceTypeMismatch.q", &core.TypeMismatch{Encountered: "int64", Expected: "!int64", ParameterName: "_", IsReturn: false}}, {"ReturnCountMismatch.q", &core.ReturnCountMismatch{Count: 1, ExpectedCount: 0}}, {"ReturnCountMismatch2.q", &core.ReturnCountMismatch{Count: 1, ExpectedCount: 2}}, {"TypeMismatch.q", &core.TypeMismatch{Encountered: "string", Expected: "int64", ParameterName: "x", IsReturn: false}}, diff --git a/src/core/testdata/ResourceAlreadyConsumed.q b/src/core/testdata/ResourceAlreadyConsumed.q new file mode 100644 index 0000000..19d837d --- /dev/null +++ b/src/core/testdata/ResourceAlreadyConsumed.q @@ -0,0 +1,10 @@ +main() { + x := acquire() + use(x) + free(x) + free(x) +} + +acquire() -> !int { return 1 } +use(_ int) {} +free(_ !int) {} \ No newline at end of file diff --git a/src/core/testdata/ResourceAlreadyConsumed2.q b/src/core/testdata/ResourceAlreadyConsumed2.q new file mode 100644 index 0000000..aa8292f --- /dev/null +++ b/src/core/testdata/ResourceAlreadyConsumed2.q @@ -0,0 +1,10 @@ +main() { + x := acquire() + use(x) + free(x) + use(x) +} + +acquire() -> !int { return 1 } +use(_ int) {} +free(_ !int) {} \ No newline at end of file diff --git a/src/core/testdata/ResourceAlreadyConsumed3.q b/src/core/testdata/ResourceAlreadyConsumed3.q new file mode 100644 index 0000000..77fe6b0 --- /dev/null +++ b/src/core/testdata/ResourceAlreadyConsumed3.q @@ -0,0 +1,9 @@ +main() { + x := acquire() + y := x + free(x) + free(y) +} + +acquire() -> !int { return 1 } +free(_ !int) {} \ No newline at end of file diff --git a/src/core/testdata/ResourceAlreadyConsumed4.q b/src/core/testdata/ResourceAlreadyConsumed4.q new file mode 100644 index 0000000..c20c827 --- /dev/null +++ b/src/core/testdata/ResourceAlreadyConsumed4.q @@ -0,0 +1,10 @@ +main() { + x := acquire() + y := acquire() + y = x + free(x) + free(y) +} + +acquire() -> !int { return 1 } +free(_ !int) {} \ No newline at end of file diff --git a/src/core/testdata/ResourceNotConsumed.q b/src/core/testdata/ResourceNotConsumed.q new file mode 100644 index 0000000..a7e7592 --- /dev/null +++ b/src/core/testdata/ResourceNotConsumed.q @@ -0,0 +1,7 @@ +main() { + x := acquire() + use(x) +} + +acquire() -> !int { return 1 } +use(_ int) {} \ No newline at end of file diff --git a/src/core/testdata/ResourceTypeMismatch.q b/src/core/testdata/ResourceTypeMismatch.q new file mode 100644 index 0000000..c8bc3c2 --- /dev/null +++ b/src/core/testdata/ResourceTypeMismatch.q @@ -0,0 +1,9 @@ +main() { + x := acquire() + use(x) + free(x) +} + +acquire() -> !int { return 1 } +use(a int) { free(a) } +free(_ !int) {} \ No newline at end of file diff --git a/src/scanner/scanSignature.go b/src/scanner/scanSignature.go index 86d0eaf..ccd6157 100644 --- a/src/scanner/scanSignature.go +++ b/src/scanner/scanSignature.go @@ -130,17 +130,7 @@ func scanSignature(file *fs.File, pkg string, tokens token.List, i int, delimite return nil, i, errors.New(MissingParameter, file, position) } - if len(param) == 1 { - function.Output = append(function.Output, &ssa.Parameter{ - Name: "", - Tokens: param, - }) - } else { - function.Output = append(function.Output, &ssa.Parameter{ - Name: param[0].String(file.Bytes), - Tokens: param, - }) - } + function.Output = append(function.Output, &ssa.Parameter{Tokens: param}) } return function, i, nil diff --git a/src/ssa/Block.go b/src/ssa/Block.go index 8997c9a..0e84849 100644 --- a/src/ssa/Block.go +++ b/src/ssa/Block.go @@ -2,6 +2,7 @@ package ssa import ( "iter" + "maps" "slices" ) @@ -25,6 +26,50 @@ func NewBlock(label string) *Block { // AddSuccessor adds the given block as a successor. func (b *Block) AddSuccessor(successor *Block) { successor.Predecessors = append(successor.Predecessors, b) + + if b.Identifiers == nil { + return + } + + if successor.Identifiers == nil { + successor.Identifiers = make(map[string]Value, len(b.Identifiers)) + + if len(successor.Predecessors) == 1 { + maps.Copy(successor.Identifiers, b.Identifiers) + return + } + } + + for name, oldValue := range successor.Identifiers { + newValue, exists := b.Identifiers[name] + + if !exists { + delete(successor.Identifiers, name) + continue + } + + if oldValue == newValue { + continue + } + + phi, isPhi := oldValue.(*Phi) + + if !isPhi || successor.Index(phi) == -1 { + phi = &Phi{ + Arguments: make([]Value, len(successor.Predecessors)-1, len(successor.Predecessors)), + Typ: oldValue.Type(), + } + + for i := range phi.Arguments { + phi.Arguments[i] = oldValue + } + + successor.InsertAt(phi, 0) + successor.Identifiers[name] = phi + } + + phi.Arguments = append(phi.Arguments, newValue) + } } // Append adds a new value to the block. @@ -88,62 +133,8 @@ func (b *Block) FindExisting(instr Value) Value { // FindIdentifier searches for all the possible values the identifier // can have and combines them to a phi instruction if necessary. func (b *Block) FindIdentifier(name string) (value Value, exists bool) { - return b.findIdentifier(name, make(map[*Block]Value)) -} - -// findIdentifier searches for all the possible values the identifier -// can have and combines them to a phi instruction if necessary. -func (b *Block) findIdentifier(name string, traversed map[*Block]Value) (Value, bool) { - if cached, isTraversed := traversed[b]; isTraversed { - return cached, cached != nil - } - - if value, exists := b.Identifiers[name]; exists { - traversed[b] = value - return value, true - } - - traversed[b] = nil - - switch len(b.Predecessors) { - case 0: - return nil, false - case 1: - value, exists := b.Predecessors[0].findIdentifier(name, traversed) - - if exists { - traversed[b] = value - } - - return value, exists - default: - var values []Value - - for _, pre := range b.Predecessors { - value, exists := pre.findIdentifier(name, traversed) - - if !exists { - return nil, false - } - - values = append(values, value) - traversed[b] = value - } - - if len(values) == 0 { - return nil, false - } - - if allSame(values) { - return values[0], true - } - - phi := &Phi{Arguments: values, Typ: values[0].Type()} - b.InsertAt(phi, 0) - b.Identify(name, phi) - traversed[b] = phi - return phi, true - } + value, exists = b.Identifiers[name] + return } // IdentifiersFor returns an iterator for all the identifiers pointing to the given value. @@ -197,9 +188,24 @@ func (b *Block) InsertAt(value Value, index int) { // Last returns the last value. func (b *Block) Last() Value { + if len(b.Instructions) == 0 { + return nil + } + return b.Instructions[len(b.Instructions)-1] } +// Phis is an iterator for all phis at the top of the block. +func (b *Block) Phis(yield func(*Phi) bool) { + for _, instr := range b.Instructions { + phi, isPhi := instr.(*Phi) + + if !isPhi || !yield(phi) { + return + } + } +} + // RemoveAt sets the value at the given index to nil. func (b *Block) RemoveAt(index int) { value := b.Instructions[index] @@ -228,4 +234,14 @@ func (b *Block) ReplaceAllUses(old Value, new Value) { // String returns the block label. func (b *Block) String() string { return b.Label +} + +// Unidentify deletes the identifier for the given value. +func (b *Block) Unidentify(value Value) { + for name, existing := range b.Identifiers { + if existing == value { + delete(b.Identifiers, name) + return + } + } } \ No newline at end of file diff --git a/src/ssa/Block_test.go b/src/ssa/Block_test.go index 6d7adae..3dd8a99 100644 --- a/src/ssa/Block_test.go +++ b/src/ssa/Block_test.go @@ -99,25 +99,24 @@ func TestBlockFindIdentifier(t *testing.T) { elseBlock := ssa.NewBlock("if.else") mergeBlock := ssa.NewBlock("merge") - branch.AddSuccessor(thenBlock) - branch.AddSuccessor(elseBlock) - thenBlock.AddSuccessor(mergeBlock) - elseBlock.AddSuccessor(mergeBlock) - branchValue := ssa.Value(&ssa.Int{Int: 1}) branch.Append(branchValue) branch.Identify("branch", branchValue) branch.Identify("x", branchValue) + branch.AddSuccessor(thenBlock) + branch.AddSuccessor(elseBlock) thenValue := ssa.Value(&ssa.Int{Int: 2}) thenBlock.Append(thenValue) thenBlock.Identify("then", thenValue) thenBlock.Identify("x", thenValue) + thenBlock.AddSuccessor(mergeBlock) elseValue := ssa.Value(&ssa.Int{Int: 3}) elseBlock.Append(elseValue) elseBlock.Identify("else", elseValue) elseBlock.Identify("x", elseValue) + elseBlock.AddSuccessor(mergeBlock) mergeValue := ssa.Value(&ssa.Int{Int: 4}) mergeBlock.Append(mergeValue) diff --git a/src/ssa/IR.go b/src/ssa/IR.go index edcf12d..e08d466 100644 --- a/src/ssa/IR.go +++ b/src/ssa/IR.go @@ -38,14 +38,33 @@ func (ir *IR) CountValues() int { return count } +// ExitBlocks is an iterator for all exit blocks. +func (ir *IR) ExitBlocks(yield func(*Block) bool) { + for i, block := range ir.Blocks { + if i < len(ir.Blocks)-1 { + last := block.Last() + _, isReturn := last.(*Return) + + if !isReturn { + continue + } + } + + if !yield(block) { + return + } + } +} + // Finalize creates the list of users for each value. func (ir *IR) Finalize() { - ir.Values(func(_ int, value Value) bool { - for _, input := range value.Inputs() { - input.AddUser(value) + for _, block := range ir.Blocks { + for _, value := range block.Instructions { + for _, input := range value.Inputs() { + input.AddUser(value) + } } - return true - }) + } } // IsIdentified returns true if the value can be obtained from one of the identifiers. diff --git a/src/types/Is.go b/src/types/Is.go index 6465681..ab89b9e 100644 --- a/src/types/Is.go +++ b/src/types/Is.go @@ -18,6 +18,18 @@ func Is(a Type, b Type) bool { return true } + aResource, aIsResource := a.(*Resource) + + if aIsResource && Is(b, aResource.Of) { + return true + } + + bResource, bIsResource := b.(*Resource) + + if aIsResource && bIsResource { + return aResource.Of == bResource.Of + } + aArray, aIsArray := a.(*Array) if aIsArray && bIsPointer && (bPointer.To == Any || aArray.Of == bPointer.To) { diff --git a/src/types/Resource.go b/src/types/Resource.go new file mode 100644 index 0000000..42587b1 --- /dev/null +++ b/src/types/Resource.go @@ -0,0 +1,17 @@ +package types + +// Resource represents a shared resource that must be +// constructed, used and deconstructed, in that order. +type Resource struct { + Of Type +} + +// Name returns the type name. +func (r *Resource) Name() string { + return "!" + r.Of.Name() +} + +// Size returns the total size in bytes. +func (r *Resource) Size() int { + return r.Of.Size() +} \ No newline at end of file diff --git a/tests/resource.q b/tests/resource.q new file mode 100644 index 0000000..b818278 --- /dev/null +++ b/tests/resource.q @@ -0,0 +1,9 @@ +main() { + x := acquire() + use(x) + free(x) +} + +acquire() -> !int { return 1 } +use(_ int) {} +free(_ !int) {} \ No newline at end of file diff --git a/tests/tests_test.go b/tests/tests_test.go index 0aa7a23..1b44ff1 100644 --- a/tests/tests_test.go +++ b/tests/tests_test.go @@ -78,6 +78,7 @@ var tests = []run{ {"reorder-add", "", "", 0}, {"free-registers", "", "", 0}, {"overload", "", "", 0}, + {"resource", "", "", 0}, } func TestTests(t *testing.T) { -- 2.51.2