From 16055902bd286864bfb35ab63790050f6b69ceb6 Mon Sep 17 00:00:00 2001 From: mertcandav Date: Tue, 14 Jan 2025 21:34:49 +0300 Subject: [PATCH] sema: fix mutability and comparable state analysis of types --- std/jule/README.md | 62 +++++++++++++++++++++++++++++------ std/jule/sema/sema.jule | 58 +++++++++++++++++++++++++++------ std/jule/sema/struct.jule | 12 +++++-- std/jule/sema/type.jule | 68 ++++++++++++++++++++++++++++++++++----- 4 files changed, 172 insertions(+), 28 deletions(-) diff --git a/std/jule/README.md b/std/jule/README.md index 88816c966..73a1ed37f 100644 --- a/std/jule/README.md +++ b/std/jule/README.md @@ -29,14 +29,14 @@ It is also used by the official reference compiler JuleC and is developed in par For example: ``` struct Test[T1, T2] {} - + impl Test { - static fn new(): Test[T1, T2] { - type Y: T1 - ret Test[T1, T2]{} - ret Test[T2, Y]{} - ret Test[Test[T1, T2], T2]{} - } + static fn new(): Test[T1, T2] { + type Y: T1 + ret Test[T1, T2]{} + ret Test[T2, Y]{} + ret Test[Test[T1, T2], T2]{} + } } ``` In this example above `Test[T]` and `Test[Y]` declaring same thing which is good. Also `Test[T2, Y]` (which is declares different generic instance for owner structure) is valid because there is plain usage of it's own generics. But latest return statement declares `Test[Test[T1, T2], T2]` type which is causes cycle. `T2` is plain usage, but `Test[T1, T2]` is nested usage not plain, causes cycle. Same rules are works for slices, pointers or etc. Plain usage is just the generic, not else. @@ -57,11 +57,55 @@ Typical uses are things like capturing or tracing private behavior. For example, - **(9)** Jule can handle supported types bidirectionally for binary expressions (`s == nil || nil == s` or etc.). However, when creating CAST, some types in binary expressions must always be left operand. These types are; `any`, type enums, enums, smart pointers, raw pointers and traits. - **(9.1)** For these special types, the type that is the left operand can normally be left or right operand. It is only guaranteed if the expression of the relevant type is in the left operand. There may be a shift in the original order. - **(9.2)** In the case of a `nil` comparison, the right operand should always be `nil`. -**(10)** The `Scope` field of iteration or match expressions must be the first one. Accordingly, coverage data of the relevant type can be obtained by reinterpreting types such as `uintptr` with Unsafe Jule. -**(11)** Strict type aliases behave very similar to structs. For this reason, they are treated as a struct on CAST. They always have an instance. The data structure that represents a structure instance provides source type data that essentially contains what type it refers to. This data is only set if the structure was created by a strict type alias. + +- **(10)** The `Scope` field of iteration or match expressions must be the first one. Accordingly, coverage data of the relevant type can be obtained by reinterpreting types such as `uintptr` with Unsafe Jule. + +- **(11)** Strict type aliases behave very similar to structs. For this reason, they are treated as a struct on CAST. They always have an instance. The data structure that represents a structure instance provides source type data that essentially contains what type it refers to. This data is only set if the structure was created by a strict type alias. - **(11.1)** If a struct instance is created by a strict type alias (easily identified by looking at the source type data) and declared binded, the binded indicates that it was created by a strict type alias defined for a type. If a structure does not have source type data and the declaration is described as binded, this is a ordinary binded struct declaration. - **(11.2)** To ensure that the created structure instance can be used consistently, the type should be checked using a type alias for the instance's type. If a strict type alias is used in the type check, the source type of the created structure instance should be assigned as the source to the structure instance encapsulated by the type alias. While this type alias is being checked, it provides the same struct instance to those referencing it, even though the analysis has not yet been completed. The type is distributed consistently, duplication is prevented, and type errors are avoided. +- **(12)** During type analysis, it is not always possible to determine the mutability and comparability of all types because their primary attributes might not yet be fully known. As a result, incorrect evaluations can occur during the analysis phase. To prevent this, preconditions should be assessed. For example, when evaluating a type for a struct instance, even if the exact details of that type are unknown, it is still possible to infer whether it is mutable or comparable. For instance, if it is determined that the type is a function, but the specific parameters or additional details about the function are unknown, it can still be concluded that the type is not comparable because function types are inherently non-comparable. \ +\ +An example of a faulty analysis scenario: + ``` + type Func: fn(): (a: int, b: FuncTest) + + struct FuncTest { + f: Func + } + ``` + In the example above, while evaluating the `Func` type, it depends on the `FuncTest` type. Similarly, when evaluating `FuncTest`, it refers to the `Func` type. Since the exact nature of the `Func` type is not yet known, it might incorrectly be considered comparable. To prevent this, if it is established beforehand that `Func` is a function type, it can be marked as non-comparable. Consequently, when `FuncTest` references `Func`, it will inherit this information and correctly determine that `Func` is not comparable. + - **(12.1)** There should be no risk in cyclic cases, as types that inherently carry cyclic risks will already result in errors due to their cyclic nature. For types that are interdependent but do not result in a cycle, they must operate in harmony with each other. This is achievable through continuous deep evaluation of the mutability and comparability states of potentially dependent types. By ensuring that each type appropriately handles its dependencies, the system can maintain consistency and avoid incorrect assumptions during type analysis.\ +\ + For example: + ``` + type Test: chan FuncTest + + struct FuncTest { + f: Test + x: &int + } + ``` + In the example above, the `Test` function defines a channel type that uses the `FuncTest` structure. Within itself, `FuncTest` references the `Test` type. A channel type is considered mutable if its element type is mutable. However, since `FuncTest` has not yet been fully analyzed, it is impossible to determine whether the exact type is mutable. + + To resolve this, when analyzing `FuncTest`, the `Test` type is also analyzed, and no special static data is maintained for the channel type's mutability. Instead, a reference to `FuncTest` is used. Once the analysis of the `FuncTest` structure is complete, it will be determined as mutable due to the `&int` type. While checking the mutability state of the `Test` structure, it refers back to `FuncTest` and uses its mutability status, thereby ensuring mutual communication and consistency between the two types. + + - **(12.2)** Each structure instance should be initialized as comparable and non-mutable by default. If it contains a type that prevents it from being comparable, this state should be recorded. Similarly, if it uses a type that makes it mutable, this data should also be updated. + + By following this approach, as outlined in **(12)**, preliminary analyses can easily shape this information. This method ensures that the mutability and comparability of structures are accurately determined during the type analysis phase, even when complete information about dependent types is not yet available. + + - **(12.2.1)** During type analysis, particularly when dealing with interdependent types, determining mutability and comparability may not always be feasible during the preliminary analysis. Therefore, after the type checks, the final type should also be verified during the structure analysis phase. + + For example: + ``` + type Test: &FuncTest + + struct FuncTest { + f: Test + } + ``` + In the example above, when FuncTest is analyzed, it is necessary to also analyze Test. During the analysis of Test, the mutability status determined in the preliminary analysis is recorded in the implicit structure instance underlying the Test type. Since Test is a strict type alias, it creates its own structure internally. In such cases, the mutability and comparability status may not be reflected in dependent types like FuncTest. Therefore, during the structure analysis phase, a check is performed on the base type as well. Since the f field returns a Test type that is marked as mutable, FuncTest also inherits the mutable type information through this check. + ### Implicit Imports Implicit imports are as described in developer reference (9). This section addresses which package is supported and what special behaviors it has. diff --git a/std/jule/sema/sema.jule b/std/jule/sema/sema.jule index fee9fcce5..e4f2963d1 100644 --- a/std/jule/sema/sema.jule +++ b/std/jule/sema/sema.jule @@ -1086,11 +1086,15 @@ impl sema { self.setCurrentFile(file) } } - ok = self.checkTypeSymWithRefers(ta.TypeSym, l, nil, &referencer{ + mut referencer := &referencer{ ident: ta.Ident, owner: ta, refs: &ta.Refers, - }) + } + if ta.Strict { + referencer.ins = ta.TypeSym.Type.softStruct() + } + ok = self.checkTypeSymWithRefers(ta.TypeSym, l, nil, referencer) if ok && ta.TypeSym.Type.Array() != nil && ta.TypeSym.Type.Array().Auto { self.pushErr(ta.TypeSym.Decl.Token, build::LogMsg.ArrayAutoSized) ok = false @@ -1109,7 +1113,7 @@ impl sema { // Type alias is strict, make strict type alias analysis. // See developer reference (11). if ta.Strict { - mut s := initNewStructType(ta.Ident, nil) + mut s := initNewStructType(ta.Ident, pseudoSource) s.Decl.sema = self s.Decl.Binded = ta.Binded s.Decl.Token = ta.Token @@ -2262,6 +2266,11 @@ impl sema { // Which is contains fields and generic-type constraints. // If generic instance will be check, errorToken should be passed. fn checkStructEnv(mut &self, mut &s: &StructIns, mut errorToken: &token::Token): (ok: bool) { + // If source is not nil, do not check. + // This is unnecessary process for strict type alias structure instances. + if s.Source != nil { + ret true + } mut tc := typeChecker{ s: s.Decl.sema, rootLookup: s.Decl.sema, @@ -2325,6 +2334,14 @@ impl sema { mut eval := self.eval(self) s.Comparable = !s.Decl.Binded for (_, mut f) in s.Fields { + // Set instance for referencer if field declaration is not mutable. + // But field declared as mutable, do not set. + // It basically for the `!f.Decl.Mutable && f.Type.Mutable()` condition. + if !f.Decl.Mutable { + tc.referencer.ins = s + } else { + tc.referencer.ins = nil + } mut kind := tc.checkDecl(f.Decl.TypeSym.Decl) ok = kind != nil && ok if kind == nil { @@ -2335,6 +2352,10 @@ impl sema { continue } f.Type = kind + // We have to check mutable and comparable conditions again. + // Because type analysis is a simple precondition checker. + // For the actual type, we have to check. + // See developer reference (12.2.1). s.Mutable = s.Mutable || (!f.Decl.Mutable && f.Type.Mutable()) s.Comparable = s.Comparable && f.Type.Comparable() @@ -2364,7 +2385,13 @@ impl sema { ret } - fn precheckStructIns(mut &self, mut &s: &StructIns, mut errorToken: &token::Token): (ok: bool) { + fn precheckStructIns(mut &self, mut s: &StructIns, mut errorToken: &token::Token): (ok: bool) { + // Return is source type is still pseudoSource of a strict type alias instance. + // Following analysis must be done with real source type. + if s.Source == pseudoSource { + s.Checked = false + ret true + } ok = self.checkStructEnv(s, errorToken) if ok { // See implicit imports reference (1). @@ -2373,19 +2400,32 @@ impl sema { // push instance for runtime function. if s.Comparable && self.meta.runtime != nil { mut decl := runtimeFindFunc(self.meta.runtime, "arrayCmp") - for (_, mut field) in s.Fields { - mut arr := field.Type.Array() + pushArr := fn(mut t: Kind, mut token: &token::Token) { + match type t { + | &Array: + break + |: + ret + } + mut arr := (&Array)(t) if arr == nil { - continue + ret } mut f := decl.instanceForce() f.Generics = append(f.Generics, &InsGeneric{Type: arr.Elem}) - gok, _ := self.checkGenericFunc(f, field.Decl.Token) - if !gok { + ok, _ = self.checkGenericFunc(f, token) + if !ok { panic("sema: arrayCmp evaluation failed, this is an implementation mistake") } s.Refers.Push(f) } + if s.Source == nil { + for (_, mut field) in s.Fields { + pushArr(field.Type.ActualKind(), field.Decl.Token) + } + } else { + pushArr(s.Source.ActualKind(), s.Decl.Token) + } } } diff --git a/std/jule/sema/struct.jule b/std/jule/sema/struct.jule index 203c153ad..6e41fe13c 100644 --- a/std/jule/sema/struct.jule +++ b/std/jule/sema/struct.jule @@ -91,6 +91,10 @@ impl Struct { Refers: ReferenceStack.new(), } + // See developer reference (12). + ins.Mutable = false + ins.Comparable = true + for (_, mut s) in self.Statics { ins.Statics = append(ins.Statics, new(Var, *s)) } @@ -285,8 +289,12 @@ impl StructIns { ret } self.Source = t - self.Comparable = t.Comparable() - self.Mutable = t.Mutable() + // We have to check mutable and comparable conditions again. + // Because type analysis is a simple precondition checker. + // For the actual type, we have to check. + // See developer reference (12.2.1). + self.Comparable = self.Comparable || t.Comparable() + self.Mutable = self.Mutable || t.Mutable() } // Reports whether instances are same. diff --git a/std/jule/sema/type.jule b/std/jule/sema/type.jule index a396062e0..5e6d417b2 100644 --- a/std/jule/sema/type.jule +++ b/std/jule/sema/type.jule @@ -143,8 +143,8 @@ impl Type { fn Comparable(self): bool { unsafe { mut _self := &self - if _self.Struct() != nil { - ret _self.Struct().Comparable + if _self.softStruct() != nil { + ret _self.softStruct().Comparable } if _self.Array() != nil { ret _self.Array().Elem.Comparable() @@ -157,8 +157,8 @@ impl Type { fn Mutable(self): bool { unsafe { mut _self := &self - if _self.Struct() != nil { - ret _self.Struct().Mutable + if _self.softStruct() != nil { + ret _self.softStruct().Mutable } if _self.Chan() != nil { ret _self.Chan().Elem.Mutable() @@ -865,12 +865,20 @@ impl Ptr { struct referencer { ident: str owner: any + ins: &StructIns // If owner is a struct, this is a checker instance. refs: *[]any } +// Cycle state flags. const cycleErrEnable = 0b01 // Enables error logging for cycle analysis. const cycleErrFail = 0b10 // Means the last cycle analysis reports cycles exist. +// Type analysis tags. +// See developer reference (12). +const taNA = 0 << 0 +const taNotComparable = 1 << 0 +const taMutable = 1 << 1 + // Checks type and builds result as kind. // Removes kind if error occurs, // so type is not reports true for checked state. @@ -950,6 +958,16 @@ impl typeChecker { self.disBuiltin = true } + // Push type analysis flag to owner structure instance if exist. + // See developer reference (12). + fn pushTA(mut self, tags: int) { + if self.referencer != nil && self.referencer.ins != nil { + mut s := self.referencer.ins + s.Comparable = s.Comparable && tags&taNotComparable != taNotComparable + s.Mutable = s.Mutable || tags&taMutable == taMutable + } + } + fn pushReference[T](mut self, mut &t: T) { if self.refers == nil { ret @@ -1151,8 +1169,9 @@ impl typeChecker { if ta.Strict && self.s.step&stepFlag.ImplsImplemented == stepFlag.ImplsImplemented { // Check strict type structure if impl statements are implemented. + // But do not check if source type is nil, it means type type alias is still checking. mut s := ta.TypeSym.Type.softStruct() - if !s.Checked { + if s.Source != nil && !s.Checked { ok := self.checkStructIns(s, decl.Token) if !ok { ret nil @@ -1207,6 +1226,7 @@ impl typeChecker { self.pushErr(decl.Token, build::LogMsg.TypeNotSupportsGenerics, decl.Ident) ret nil } + self.pushTA(taMutable) self.pushReference[&Trait](t) ret t } @@ -1221,7 +1241,6 @@ impl typeChecker { if self.referencer != nil && self.referencer.owner == ins.Decl { ret true } - if !self.s.precheckStructIns(ins, errorToken) { ret false } @@ -1344,7 +1363,16 @@ impl typeChecker { ret nil } - ret self.fromStructIns(ins, decl.Token) + ins = self.fromStructIns(ins, decl.Token) + if ins != nil { + if !ins.Comparable { + self.pushTA(taNotComparable) + } + if ins.Mutable { + self.pushTA(taMutable) + } + } + ret ins } // Returns identifier if found. Also checks founded identifier. @@ -1406,6 +1434,9 @@ impl typeChecker { mut ta := self.lookup.FindTypeAlias(decl.Ident, decl.Binded) if ta == nil && !self.disBuiltin { ta = findBuiltinTypeAlias(decl.Ident) + if ta != nil && ta.Ident == types::Kind.Any { + self.pushTA(taMutable) + } } if ta != nil { ret self.fromTypeAlias(decl, ta) @@ -1472,6 +1503,8 @@ impl typeChecker { self.cycleRisk = false defer { self.cycleRisk = cycleRisk } + self.pushTA(taMutable) + mut elem := self.checkDecl(decl.Elem) ret self.buildSptrFromType(elem) } @@ -1492,6 +1525,9 @@ impl typeChecker { ins.Generics = [&InsGeneric{Type: elem}] _ = self.fromStructIns(ins, decl.Elem.Token) } + if elem.Mutable() { + self.pushTA(taMutable) + } ret &Chan{ Recv: decl.Recv, Send: decl.Send, @@ -1519,6 +1555,8 @@ impl typeChecker { self.cycleRisk = false defer { self.cycleRisk = cycleRisk } + self.pushTA(taMutable) + mut elem := (&Type)(nil) if !decl.IsUnsafe() { @@ -1536,6 +1574,8 @@ impl typeChecker { self.cycleRisk = false defer { self.cycleRisk = cycleRisk } + self.pushTA(taNotComparable | taMutable) + mut elem := self.checkDecl(decl.Elem) // Check special cases. @@ -1554,7 +1594,6 @@ impl typeChecker { fn buildArray(mut self, mut decl: &ast::ArrayType): &Array { mut n := 0 - if !decl.AutoSized() { mut size := self.s.eval(self.lookup).evalExpr(decl.Size) if size == nil { @@ -1594,6 +1633,13 @@ impl typeChecker { ret nil } + if !elem.Comparable() { + self.pushTA(taNotComparable) + } + if elem.Mutable() { + self.pushTA(taMutable) + } + ret &Array{ Auto: decl.AutoSized(), N: n, @@ -1602,6 +1648,7 @@ impl typeChecker { } fn buildMap(mut self, mut decl: &ast::MapType): &Map { + self.pushTA(taNotComparable) // Evaluate key type with cycle risk, but do not log errors about it. // We have to do this for checking correct map type dependencies, // cycle types are prevents valid map key types. So, caught them but @@ -1705,6 +1752,8 @@ impl typeChecker { self.cycleRisk = false defer { self.cycleRisk = cycleRisk } + self.pushTA(taNotComparable) + if len(decl.Generics) > 0 { self.pushErr(decl.Token, build::LogMsg.GenericedFuncAsAnonFunc) ret nil @@ -2013,6 +2062,9 @@ fn validTypeForXof(mut &t: &Type): bool { ret !t.Void() && t.Func() == nil && t.Tuple() == nil && !t.comptime() } +// Pseudo source type for strict type alias structure instances. +static mut pseudoSource = new(Type) + // Initializes new structure for strict types. // See developer reference (11) for details. fn initNewStructType(ident: str, mut source: &Type): &StructIns {