Annotations
Cangjie provides several built-in compilation markers to support special case handling.
Built-in Compilation Markers for Integer Overflow Handling Strategies
Cangjie offers three built-in compilation markers to control integer overflow handling strategies: @OverflowThrowing, @OverflowWrapping, and @OverflowSaturating. These markers can currently only be applied to function declarations and affect integer operations and type conversions within the function. They correspond to the following three overflow handling strategies:
1. Throwing Exceptions (throwing): Throws an exception when integer overflow occurs.
Note: For scenarios where integer overflow behavior is set to throwing, if the overflow can be detected at compile time, the compiler will directly report an error.
2. Wrapping (wrapping): When the result of an integer operation exceeds the representable range of the receiving memory space, the excess bits are truncated.
3. Saturating (saturating): When integer overflow occurs, the result is set to the extreme value of the corresponding fixed precision.
By default (i.e., when no such built-in compilation marker is applied), the throwing exception (@OverflowThrowing) strategy is used.
In practice, the overflow strategy should be chosen based on business requirements. For example, to implement a secure operation on Int32 where the calculation result must mathematically match the computation process, the throwing exception strategy should be used.
This incorrect example uses the wrapping overflow strategy. For instance, when the parameters a and b are large enough to cause overflow, the result will be truncated, leading to a mismatch between the function's return value and the mathematical expression a + b.
This correct example uses the throwing exception strategy. When the parameters a and b cause integer overflow, the operation function throws an exception.
The following table summarizes mathematical operators that may cause integer overflow.
| Operator | Overflow | Operator | Overflow | Operator | Overflow | Operator | Overflow |
|:----:|:--:|:--------------------:|:--:|:-------------------:|:--:|:----:|:--:|
| + | Y | -= | Y | << | N | < | N |
| - | Y | = | Y | >> | N | > | N |
| | Y | /= | Y | & | N | >= | N |
| / | Y | %= | N | | | N | <= | N |
| % | N | <<= | N | ^ | N | == | N |
| ++ | Y | >>= | N | = | Y | | |
| -- | Y | &= | N | ! | N | | |
| = | N | |= | N | != | N | | |
| += | Y | ^= | N | | Y | | |
// The result is truncated
@OverflowWrapping
func operation(a: Int32, b: Int32): Int32 {
a + b // No exception will be thrown when overflow occurs
}// Secure
@OverflowThrowing
func operation(a: Int32, b: Int32): Int32 {
a + b
}
main(): Int64 {
try {
operation(Int32.Max, 1)
} catch (e: ArithmeticException) {
println(e.message)
//Handle error
}
0
}Test Framework Built-in Compilation Markers
When using mocks in tests, if the mocked object involves static or top-level declarations, the test framework's built-in compilation marker @EnsurePreparedToMock must be used to instruct the compiler to prepare these declarations for mocking.
This marker can only be applied to lambda expressions where the last expression calls a static or top-level declaration. The compiler will then prepare this declaration for mocking.
Example:
In this example, ConfigureMock.stubFunction registers a stub for the function test, and returns sets the stub's return value.
> Note:
>
> Typically, the standard library's mock interfaces should be used to define mock declarations. Direct use of @EnsurePreparedToMock is discouraged unless necessary. Standard library functions internally use this marker when needed.
Constraints for using @EnsurePreparedToMock:
- Only allowed when compiling with test and mock-related options (--test/--test-only and --mock=on/--mock=runtime-error).
- Can only be applied to lambdas with a suitable last expression.
- The lambda's last expression must be a call, member access, or reference expression involving:
- Top-level functions or variables;
- Static functions, properties, or fields;
- Foreign declarations;
- Not local functions or variables;
- Non-private declarations;
- Not const expressions or declarations;
- Must be from a package built in mock mode.
package prod
public func test(a: String, b: String): String {
a + b
}package test
import prod.*
import std.unittest.mock.*
@Test
public class TestA {
@TestCase
func case1(): Unit {
{ =>
let matcher0 = Matchers.eq("z")
let matcher1 = Matchers.eq("y")
let stubCall = @EnsurePreparedToMock { => return(test(matcher0.value(), matcher1.value())) }
ConfigureMock.stubFunction(stubCall,[matcher0.withDescription(#"eq("z")"#), matcher1.withDescription(#"eq("y")"#)], Option<String>.None, "test", #"test("z", "y")"#, 15)
}().returns("mocked value")
println(test("z", "y")) // prints "mocked value"
}
}Custom Annotations
Custom annotations allow reflection (see Reflection Chapter) to retrieve additional metadata beyond type information, enabling more complex logic.
Developers can create custom annotations by marking a class with @Annotation. The class must not be abstract, open, or sealed, and must provide at least one const init function; otherwise, the compiler will report an error.
The following example defines a custom annotation @Version and applies it to classes A, B, and C. In main, reflection is used to retrieve and print the @Version annotation information.
Compiling and running this code outputs:
Annotation information must be generated at compile time and bound to the type. Custom annotations must be instantiated using const init with valid arguments. The annotation declaration syntax is similar to macro declarations, where the [] brackets must contain const expressions in order or named parameter rules (see Constant Evaluation Chapter). For annotation types with a no-argument const init, the brackets can be omitted.
The following example defines a custom annotation @Marked with a no-argument const init. Both @Marked and @Marked[] are valid usages.
Compiling and running this code outputs:
The same annotation class cannot be applied multiple times to the same target (i.e., no duplicate annotations).
When compiling and executing the above code, the output is:
Custom annotations can be applied to type declarations (class, struct, enum, interface), parameters in member functions/constructors, constructor declarations, member function declarations, member variable declarations, and member property declarations. They can also restrict their applicable locations to prevent misuse by developers. Such annotations need to specify the target parameter when declaring @Annotation, with the parameter type being Array. Here, AnnotationKind is an enum defined in the standard library. When no target is specified, the custom annotation can be used in all the aforementioned locations. When targets are specified, it can only be used in the declared list.
In the following example, a custom annotation is restricted via target to only be applicable to member functions. Using it in other locations will cause a compilation error.
package pkg
import std.reflect.TypeInfo
@Annotation
public class Version {
let code: String
const init(code: String) {
this.code = code
}
}
@Version["1.0"]
class A {}
@Version["1.1"]
class B {}
main() {
let objects = [A(), B()]
for (obj in objects) {
let annOpt = TypeInfo.of(obj).findAnnotation<Version>()
if (let Some(ann) <- annOpt) {
println(ann.code)
}
}
}1.0
1.1package pkg
import std.reflect.TypeInfo
@Annotation
public class Marked {
const init() {}
}
@Marked
class A {}
@Marked[]
class B {}
main() {
if (TypeInfo.of(A()).findAnnotation<Marked>().isSome()) {
println("A is Marked")
}
if (TypeInfo.of(B()).findAnnotation<Marked>().isSome()) {
println("B is Marked")
}
}A is Marked
B is Marked@Marked
@Marked // Error
class A {}
````Annotation` is not inherited, therefore a type's annotation metadata only comes from the annotations declared during its definition. If annotation metadata from a parent type is needed, developers must manually query it using reflection interfaces.
In the following example, `A` is annotated with `@Marked`, `B` inherits from `A`, but `B` does not inherit `A`'s annotation.
<!-- verify -->
```cangjie
package pkg
import std.reflect.TypeInfo
@Annotation
public class Marked {
const init() {}
}
@Marked
open class A {}
class B <: A {}
main() {
if (TypeInfo.of(A()).findAnnotation<Marked>().isSome()) {
println("A is Marked")
}
if (TypeInfo.of(B()).findAnnotation<Marked>().isSome()) {
println("B is Marked")
}
}A is Markedpublic enum AnnotationKind {
| Type
| Parameter
| Init
| MemberProperty
| MemberFunction
| MemberVariable
}@Annotation[target: [MemberFunction]]
public class Marked {
const init() {}
}
class A {
@Marked // OK, member function
func marked() {}
}
@Marked // Error, type
class B {}