Expressions
An expression consists of one or more operands. Multiple operands are connected by operators. Each expression has a type. The process of calculating the expression value is called evaluation.
In the Cangjie programming language, expressions are almost ubiquitous. There are expressions that represent various calculations (such as arithmetic expressions and logical expressions), and expressions that represent branches and loops (such as if expressions and loop expressions). For expressions that contain multiple operators, the precedence, associativity, and operand evaluation sequence of each operator must be specified. The precedence and associativity specify the combination mode of operands and operators. The evaluation sequence of operands specifies the evaluation sequence of operands of binary and ternary operators. Both of them affect the value of an expression.
The following describes expressions in Cangjie in sequence.
Note: The operand types of each operator in this chapter are specified on the premise that the operator is not overloaded.
Literals
A literal is an expression with a fixed syntax. For a literal that does not contain other expressions (see [Literals]), its value is the value of the literal itself, and its type can be determined by its syntax or context.When the literal type cannot be determined, an integer literal is of the Int64
type, and a floating-point literal is of the Float64
type. For a set literal or a tuple literal (see [Tuple]) that can contain other expressions internally, its value is equal to the value of the literal obtained after all expressions inside it are evaluated.Its type is determined by its syntax.
Example of literals:
main(): Int64 {
10u8 // UInt8 literal
10i16 // Int16 literal
1024u32 // UInt32 literal
1024 // Int64 literal
1024.512_f32 // Float32 literal
1024.512 // Float64 literal
'a' // Rune literal
true // Bool literal
"Cangjie" // String literal
() // Unit literal
[1, 2, 3] // Array<Int64> literal
(1, 2, 3) // (Int64, Int64, Int64) literal
return 0
}
Variable Names and Function Names
Variable names and function names (including variables or functions pointed to by package names) are also expressions. For a variable name, its value is equal to the evaluated value of the variable, and its type is that of the variable. For a function name, its value is a closure (see [Closures]), and its type is that of the function.
Examples of variable names and function names:
let intNum: Int64 = 100 // 'intNum' is the name of a variable, whose value and type are '100' and 'Int64', respectively.
/* 'add' is the name of a function, whose value and type are '(p1: Int64, p2: Int64) => {p1 + p2}' and '(Int64, Int64) -> Int64', respectively. */
func add(p1: Int64, p2: Int64) {
p1 + p2
}
let value = p1.x // x is a variable defined in package p1.
For variable names, the variables declared by var
are always mutable. A value can be assigned to a variable declared by let
only once (during or after declaration). The variable is mutable before the value is assigned and immutable after the value is assigned.
Generic Function Name as Expression
In Cangjie, functions (see [Functions]) can be used as the first member, and generic functions (see [Generics]) that declare type parameters are also supported.For a generic function, the type argument of the generic function must be provided when the function name is used as an expression. Example:
func identity<T>(a: T) { // identity is a generic function
return a
}
var b = identity // error: generic function 'identity' needs type arguments
var c = identity<Int32> // ok: Int32 is given as a type argument
identity
is a generic function. Therefore, identity
is not a valid expression. Only identity<Int32>
that provides the type argument is a valid expression.
If a function is overloaded in the current scope and contains multiple optional functions with complete types, it is ambiguous to directly use the name of this type as an expression. For example:
interface Add<T> {
operator func +(a: T): T
}
func add<T>(i: T, j: Int64): Int64 where T <: Add<Int64> { // f1
return i + j;
}
func add<T>(i: T, j: T): T where T <: Add<T> { // f2
return i + j;
}
main(): Int64 {
var plus = add<Int64> // error: ambiguous use of 'add'
return 0
}
Condition Expressions
A conditional expression is an if expression. You can determine the code branch to be executed based on whether the conditions are met to implement the branch control logic.
The syntax of if expressions is defined as follows:
ifExpression
: 'if' '(' ('let' deconstructPattern '<-')? expression ')' block ('else' ( ifExpression | block))?
;
if
is the keyword, followed by an expression enclosed in parentheses, a block, and an optional else
branch in sequence. The else
branch starts with the else
keyword and is followed by a new if
expression or a block.
The following is an example of if
expressions:
main(): Int64 {
let x = 100
// if expression without else branch
if (x > 0) {
print("x is larger than 0")
}
// if expression with else branch
if (x > 0) {
print("x is larger than 0")
} else {
print("x is not larger than 0")
}
// if expression with nested if expression
if (x > 0) {
print("x is larger than 0")
} else if (x < 0) {
print("x is lesser than 0")
} else {
print("x equals to 0")
}
return 0
}
The expression following if
(the expression type must be Bool
) is evaluated first. If the value of the expression is true
, the block following the expression is executed. Otherwise, the if
expression or block (if any) following else
is executed. The value of the if
expression is equal to the value of the expression in the execution branch.
An if
expression that contains let
is called an if-let
expression. You can use the if-let
expression to perform some simple deconstruction operations.
The following is an example of a basic if-let
expression:
main(): Int64 {
let x: Option<Int64> = Option<Int64>.Some(100)
// if-let expression without else branch
if (let Some(v) <- x) {
print("x has value")
}
// if-let expression with else branch
if (let Some(v) <- x) {
print("x has value")
} else {
print("x has not value")
}
return 0
}
In the if-let
expression, the expression following <-
(the expression type can be any type) is first evaluated. If the value of the expression matches the pattern following let
, the block following the expression is executed. Otherwise, the if
expression or block (if any) following else
is executed. The value of the if-let
expression is equal to the value of the expression in the execution branch.
The pattern following let
supports the constant, wildcard, binding, tuple, and enum patterns.
Types of the if
expression
For an if
expression without an else
branch, its type is Unit
, and its value is ()
, because if expr1 {expr2}
is the syntactic sugar of if expr1 {expr2; ()} else {()}
.
For an if
expression that contains an else
branch:
-
If the value of the
if
expression is not read or returned, the type of theif
expression is Unit, and the two branches do not need to have a common supertype. Otherwise, see the following rules. -
If there is no specific type requirement in the context and the two branch types of
if
are set toT1
andT2
, the type of theif
expression isT
, the minimum common supertype ofT1
andT2
. If the minimum common supertypeT
does not exist, an error is reported during compilation. -
If the context specifies type requirements, this supertype is the type of the
if
expression. In this case, the types of the two branches ofif
must be the subtypes of the type required by the context.
For example:
struct S1 { }
struct S2 { }
interface I1 {}
class C1 <: I1 {}
class C2 <: I1 {}
interface I2{}
class D1 <: I1 & I2 {}
class D2 <: I1 & I2 {}
func test12() {
if (true) { // OK. The type of this if expression is Unit.
S1()
} else {
S2()
}
if (true) { // OK. The type of this if expression is Unit.
C1()
} else {
C2()
}
return if (true) { // Error. The `if` expression is returned. There is no least common supertype of `D1` and `D2`.
D1()
} else {
D2()
}
}
Note: To normalize the code format, improve the code maintainability, and avoid the dangling else problem, Cangjie requires that the execution part (even if there is only one expression) in each if
branch and else
branch use a pair of curly braces to enclose a block. (The dangling else problem refers to the problem that whether else expr2
in the code such as if cond1 if cond2 expr1 else expr2
belongs to the inner or the outer if
cannot be determined. If else expr2
belongs to the inner if
, the code should be interpreted as if cond1 (if cond2 expr1 else expr2)
. If it belongs to the outer if
, the code should be interpreted as if cond1 (if cond2 expr1)expr2
. However, if the branch is forced to use braces, this problem does not occur.)
Pattern Matching Expression
In Cangjie, match
expressions can be used to implement pattern matching
, and developers are allowed to use simpler code to describe complex branch control logic. Intuitively, a pattern is a structure that defines a set of instances that match the pattern. Pattern matching is to determine whether a given instance belongs to the instance set defined by the pattern. Obviously, there are only two matching results: success and failure. match
expressions are classified into two types: match
expressions with and without selector
.
The syntax of match
expressions is defined as follows:
matchExpression
: 'match' '(' expression ')' '{' matchCase+ '}'
| 'match' '{' ('case' (expression | '_') '=>' expressionOrDeclaration+)+ '}'
;
matchCase
: 'case' pattern ('|' pattern)* patternGuard? '=>' expressionOrDeclaration+
;
patternGuard
: 'where' expression
;
For a match
expression with selector
, expression
following the keyword match
is the selector
to be matched. Multiple matchCase
s can be defined in {}
after selector
. Each matchCase
starts with the keyword case
, followed by a pattern
or multiple pattern
s of the same type separated by |
(For details about the definitions of different pattern
s, see [Patterns]), an optional pattern guard
, a fat arrow =>
, and a series of (at least one) declarations or expressions (separated by semicolons or newline characters).
During the execution of the match
expression, a selector
matches a pattern
in the sequence defined by case
. Once a selector
matches the current pattern
(and meets the pattern guard
requirement), the code following the =>
is executed and the selector
does not need to match the next pattern
. If the selector
does not match the current pattern
, the system continues to match the next pattern
. The same rule applies to the rest. The following example shows how to use constant patterns for branch control in a match
expression:
let score: Int64 = 90
var scoreResult: String = match (score) {
case 0 => "zero"
case 10 | 20 | 30 | 40 | 50 => "fail"
case 60 => "pass"
case 70 | 80 => "good"
case 90 | 100 => "excellent" // matched
case _ => "not a valid score"
}
To ensure security and completeness, Cangjie requires that the combination of all pattern
s defined in case
expressions and the corresponding patternGuard
s (if any) must be exhaustive, covering all possible values of selector
. If the compiler determines that the combinations are not exhaustive, an error is reported. To implement complete coverage, you can use the wildcard _
in the last case
to process the values that other case
s fail to cover. In addition, it is not required that space defined by each pattern
is mutually exclusive, that is, space covered by different pattern
s may overlap.
For a match
expression without selector
, the keyword match
is not followed by expression
. In each case
within {}
, only one expression of the Bool
type (or the wildcard _
, indicating that the value is always true
) can be used between the keyword case
and =>
.
During the execution, the values of the expressions following case
are checked in sequence. If the value of an expression is true
, the code following =>
is executed, and all case
s following =>
do not need to be checked. A match
expression without selector
is a concise expression for a series of nested if-else if
expressions.
Similarly, the match
expression without selector
must be exhaustive (that is, at least one case
is met in any case). The compiler performs exhaustive check if possible. If the check fails, an error is reported and the system prompts you to add case _
.
let score = 80
var result: String = match {
case score < 60 => "fail"
case score < 70 => "pass"
case score < 90 => "good" // matched
case _ => "excellent"
}
Similar to an if
expression, the type of a match
expression complies with the following rules regardless of whether the match
expression contains selector
:
-
If the value of the
match
expression is not read or returned, the type of thematch
expression is Unit, and all branches do not need to have a common supertype. Otherwise, see the following rules. -
If there is no specific type requirement in the context and all branch types of
match
areT1, ..., Tn
, the type of thematch
expression isT
, the minimum common supertype ofT1, ..., Tn
. If the minimum common supertypeT
does not exist, an error is reported. -
If the context has specific type requirements, this supertype is the type of the
match
expression. In this case, the type of the expression following=>
in eachcase
must be the subtype of the type required by the context.
Patterns
Changjie provides various patterns, including:
-
Constant patterns
-
Wildcard patterns
-
Binding patterns
-
Tuple patterns
-
Type patterns
-
Enum patterns
The syntax of patterns is defined as follows:
pattern
: constantPattern
| wildcardPattern
| varBindingPattern
| tuplePattern
| typePattern
| enumPattern
;
Constant Patterns
Constant patterns can be integer literals, byte literals, floating-point literals, Rune literals, Boolean literals, string literals (string interpolation is not supported), and unit literals. The type of a literal in the constant pattern must be the same as that of selector
. The condition for successful matching between selector
and a constant pattern is that selector
is the same as the literal in the constant pattern.
The syntax of constant patterns is defined as follows:
constantPattern
: literalConstant
;
The following is an example of using constant patterns:
func f() {
let score: Int64 = 90
var scoreResult: String = match (score) {
case 0 => "zero"
case 10 | 20 | 30 | 40 | 50 => "fail"
case 70 | 80 => "good"
case 90 => "excellent" // matched
case _ => "not a valid score"
}
}
Note that floating-point literal matching complies with the floating-point equality comparison rules in constant patterns, which has the same precision problem as those encountered in other equality comparisons.
Wildcard Patterns
A wildcard pattern is represented by an underscore (_
), which can match any value. It is usually used for partial matching (for example, as a placeholder) or as the last pattern
of a match
expression to match values that are not covered by other case
s.
The syntax of wildcard patterns is defined as follows:
wildcardPattern
: '_'
;
The following is an example of using wildcard patterns:
let score: Int64 = 90
var scoreResult: String = match (score) {
case 60 => "pass"
case 70 | 80 => "good"
case 90 | 100 => "excellent" // matched
case _ => "fail" // wildcard pattern: used for default case
}
Binding Patterns
A binding pattern can also match any value. However, different from a wildcard pattern, a binding pattern binds the matched value to the variable defined in binding pattern
so that the value can be accessed in the expression following =>
.
The syntax of binding patterns is defined as follows:
varBindingPattern
: identifier
;
Variables defined in a binding pattern are immutable.
The following is an example of using binding patterns:
let score: Int64 = 90
var scoreResult: String = match (score) {
case 60 => "pass"
case 70 | 80 => "good"
case 90 | 100 => "excellent" // matched
case failScore => // binding pattern
let passNeed = 60 - failScore
"failed with ${failScore}, and ${passNeed} need to pass"
}
For a variable defined in the binding pattern, its scope starts from the position where the variable appears for the first time and ends before the next case. Note that |
is used to connect multiple patterns, the binding pattern cannot be used and cannot be nested in other patterns. Otherwise, an error is reported.
let opt = Some(0)
match (opt) {
case x | x => {} // error: variable cannot be introduced in patterns connected by '|'
case Some(x) | Some(x) => {} // error: variable cannot be introduced in patterns connected by '|'
case x: Int64 | x: String => {} // error: variable cannot be introduced in patterns connected by '|'
}
Tuple Patterns
A tuple pattern
is used to match the values of Tuple
. It is defined as multiple pattern
s enclosed in parentheses and separated by commas (,): (pattern_1, pattern_2, ... pattern_k)
. For example, (x, y, z)
is a tuple pattern
including three binding pattern
s, and (1, 0, 0)
is a tuple pattern
including three constant pattern
s. The number of sub-pattern
s in a tuple pattern
must be the same as the dimension of selector
. If the sub-pattern
s are constant pattern
s or enum pattern
s, their type must be the same as the type of selector
dimension.
The syntax of tuple patterns is defined as follows:
tuplePattern
: '(' pattern (',' pattern)+ ')'
;
The following is an example of using tuple patterns:
let scoreTuple = ("Allen", 90)
var scoreResult: String = match (scoreTuple) {
case ("Bob", 90) => "Bob got 90"
case ("Allen", score) => "Allen got ${score}" // matched
case ("Allen", 100) | ("Bob", 100) => "Allen or Bob got 100"
case (_, _) => ""
}
Type Patterns
The type check
and type cast
can be easily implemented using type patterns. The syntax of type patterns is defined as follows:
typePattern
: (wildcardPattern | varBindingPattern) ':' type
;
For the type pattern varBindingPattern : type
(or wildcardPattern : type
): Check whether the runtime type of the value to be matched is the type defined by type
on the right of :
or its subtype. If the type is matched successfully, convert the value type to the defined type
, and then bind the value of the new type to varBindingPattern
on the left of :
. (There is no binding for wildcardPattern : type
.) The matching is successful only when the type is matched. Otherwise, the matching fails. Therefore, varBindingPattern : type
(or wildcardPattern : type
) can implement both type test
and type cast
.
The following is an example of type pattern matching:
open class Point {
var x: Int32 = 1
var y: Int32 = 2
init(x: Int32, y: Int32) {
this.x = x
this.y = y
}
}
class ColoredPoint <: Point {
var color: String = "green"
init(x: Int32, y: Int32, color: String) {
super(x, y)
this.color = color
}
}
let normalPt = Point(5,10)
let colorPt = ColoredPoint(8,24,"red")
var rectangleArea1: Int32 = match (normalPt) {
case _: Point => normalPt.x * normalPt.y // matched
case _ => 0
}
var rectangleArea2: Int32 = match (colorPt) {
case cpt: Point => cpt.x * cpt.y // matched
case _ => 0
}
Enum Patterns
An enum
pattern is used together with the enum
type.
An enum pattern
is used to match the enum constructor. The format is constructorName
(parameterless constructor) or constructorName(pattern_1, pattern_2, ..., pattern_k)
(parameterized constructor). Multiple pattern
s, separated by commas (,) in parentheses, match each parameter in sequence. The pattern
s can be of any other type and can be nested.
The syntax of enum patterns is defined as follows:
enumPattern
: (userType '.')? identifier enumPatternParameters?
;
enumPatternParameters
: '(' pattern (',' pattern)* ')'
;
The following is an example of enum
pattern matching:
enum TimeUnit {
| Year(Float32)
| Month(Float32, Float32)
| Day(Float32, Float32, Float32)
| Hour(Float32, Float32, Float32, Float32)
}
let oneYear = TimeUnit.Year(1.0)
var howManyHours: Float32 = match (oneYear) {
case Year(y) => y * Float32(365 * 24) // matched
case Month(y, m) => y * Float32(365 * 24) + m * Float32(30 * 24)
case Day(y, m, d) => y * Float32(365 * 24) + m * Float32(30 * 24) + d * Float32(24)
case Hour(y, m, d, h) => y * Float32(365 * 24) + m * Float32(30 * 24) + d * Float32(24) + h
}
let twoYear = TimeUnit.Year(2.0)
var howManyYears: Float32 = match (twoYear) {
case Year(y) | Month(y, m) => y // error: variable cannot be introduced in patterns connected by '|'
case Year(y) | Month(x, _) => y // error: variable cannot be introduced in patterns connected by '|'
case Year(y) | Month(y, _) => y // error: variable cannot be introduced in patterns connected by '|'
...
}
In the scope of pattern matching, if the identifier in a pattern is an enum constructor, the identifier is always matched as an enum pattern. Otherwise, the identifier is matched as a binding pattern.
enum Foo {
A | B | C
}
func f() {
let x = Foo.A
match (x) {
case A => 0 // enum pattern
case B => 1 // enum pattern
case C => 2 // enum pattern
case D => 3 // binding pattern
}
}
Note that the type of the enum pattern must be the same as that of the selector during matching. In addition, the compiler checks whether each constructor (including the values of the constructor parameters) of the enum type is completely overridden. If not, the compiler reports an error.
enum TimeUnit {
| Year(Float32)
| Month(Float32, Float32)
| Day(Float32, Float32, Float32)
| Hour(Float32, Float32, Float32, Float32)
}
let oneYear = TimeUnit.Year(1.0)
var howManyHours: Float32 = match (oneYear) { // error: match must be exhaustive
case Year(y) => y * Float32(365 * 24)
case Month(y, m) => y * Float32(365 * 24) + m * Float32(30 * 24)
}
Classification of Patterns
Generally, if a pattern does not match the value to be matched, the pattern is called refutable pattern. If a pattern always matches the value to be matched, the pattern is called irrefutable pattern. For the preceding patterns, the following rules are defined:
- Constant patterns are always refutable patterns.
- Wildcard patterns are always irrefutable patterns.
- Binding patterns are always irrefutable patterns.
- A tuple pattern is an irrefutable pattern only when all of its patterns are irrefutable patterns.
- Type patterns are always refutable patterns.
- An enum pattern is an irrefutable pattern only when the corresponding enum type has only one parameterized constructor, and other patterns (if any) contained in the enum pattern are irrefutable patterns.
Matching Rules of Character Strings, Bytes, and Rune
When the target of pattern matching is a value whose static type is Rune
, both the Rune literal and the single-character string literal can be used as constant patterns representing Rune
literals.
When the target of pattern matching is a value whose static type is Byte
, a string literal representing an ASCII character can be used as a constant pattern representing the Byte
literal.
Pattern Guards
To further determine the matched value, Cangjie supports pattern guard
. pattern guard
can be used in match expressions or for-in expressions. This section describes how to use pattern guard
in match expressions. For details about how to use pattern guard
in for-in expressions, see [for-in Expressions].
In match expressions, pattern guard
is supported to provide a more powerful and accurate matching pattern. That is, where boolExpression
is added between pattern
and =>
(boolExpression
is an expression whose value is of the Boolean type). The matching is successful only when the value matches the pattern
and the boolExpression
following the where
is met. Otherwise, the matching fails.
The syntax of pattern guard
is defined as follows:
patternGuard
: 'where' expression
;
The following is an example of using pattern guards
:
let oneYear = Year(1.0)
var howManyHours: Float32 = match (oneYear) {
case Year(y) where y > 0.0f32 => y * Float32(365 * 24) // matched
case Year(y) where y <= 0.0f32 => 0.0
case Month(y, m) where y > 0.0f32 && m > 0.0f32 => y * Float32(365 * 24) + m * Float32(30 * 24)
case Day(y, m, d) where y > 0.0f32 && m > 0.0f32 && d > 0.0f32 => y * Float32(365 * 24) + m * Float32(30 * 24) + d * Float32(24)
case Hour(y, m, d, h) where y > 0.0f32 && m > 0.0f32 && d > 0.0f32 && h > 0.0f32 => y * Float32(365 * 24) + m * Float32(30 * 24) + d * Float32(24) + h
case _ => 0.0
}
Loop Expressions
Cangjie supports three types of loop expressions: for-in
, while
, and do-while
.
The syntax of loop expressions is defined as follows:
loopExpression
: forInExpression
| whileExpression
| doWhileExpression
;
for-in Expressions
A complete for-in expression has the following form:
for (p in e where c) {
s
}
pattern guard where c
is optional. Therefore, a simpler format of for-in expressions is as follows:
for (p in e) {
s
}
The syntax of for-in expressions is defined as follows:
forInExpression
: 'for' '(' patternsMaybeIrrefutable 'in' expression patternGuard? ')' block
;
patternsMaybeIrrefutable
: wildcardPattern
| varBindingPattern
| tuplePattern
| enumPattern
;
patternGuard
: 'where' expression
;
In the preceding syntax definition, the keyword for
can be followed only by patterns that must or may be irrefutable (see [Classification of Patterns]). In semantic check, the system checks whether the patterns following for
are irrefutable. If not, an error is reported during compilation. In addition, if the patterns after for
contain any binding pattern, it is equivalent to declaring one (or more) let
variables. The scope of each variable starts from the position where the variable appears for the first time to the end of the loop body.
for-in
evaluates expression
and then calls its iterator()
function to obtain a value of the Iterator<T>
type. The program starts to execute the loop by calling the next()
function of Iterator<T>
. You can use pattern
s to match the iteration elements. If the matching is successful (if patternGuard
exists, its conditions must also be met), the loop body block
is executed, and then next()
is called again at the beginning to continue the loop, when next()
returns None
, the loop ends.
You can query Iterable<T>
and Iterator<T>
in the standard library.
main(): Int64 {
let intArray: Array<Int32> = [0, 1, 2, 3, 4]
for (item in intArray) {
print(item) // output: 01234
}
let intRange = 0..5
for (number in intRange where number > 2) {
print(number) // output: 34
}
return 0
}
while Expressions
The syntax of while expressions is defined as follows:
whileExpression
: 'while' '(' ('let' deconstructPattern '<-')? expression ')' block
;
The syntax includes the keyword while
, parentheses that enclose an expression or a deconstruction match declared by let, and a block.
The following is an example of a basic while
expression:
main(): Int64 {
var hundred = 0
while (hundred < 100) { // until hundred = 100
hundred++
}
return 0
}
In the while
expression, the expression following while
(the expression type must be Bool
) is first evaluated. If the value of the expression is true
, the block following the expression is executed. Then, the expression value is recalculated and whether to execute the loop again is determined. If the value of the expression is false
, the loop is terminated.
A while
expression that contains let
is called a while-let
expression. You can use the while-let
expression to perform some simple deconstruction operations.
The following is an example of a basic while-let
expression:
main(): Int64 {
var x: Option<Int64> = Option<Int64>.Some(100)
// while-let expression
while (let Some(v) <- x) {
print("x has value")
x = ...
}
return 0
}
In the while-let
expression, the expression following <-
(the expression type can be any type) is first evaluated. If the value of the expression matches the pattern following let
, the block following the expression is executed. Then, the value of the expression is recalculated and matched again to determine whether to execute a loop again. If the matching fails, the current while
loop is terminated.
The patterns following let
can be constant, wildcard, binding, tuple, or enum patterns.
do-while Expressions
The syntax of do-while expressions is defined as follows:
doWhileExpression
: 'do' block 'while' '(' expression ')'
;
Different from the while
expression, if the value of expression
is false
during the first loop iteration of the while
expression, the loop body is not executed. However, for the do-while
expression, during the first loop iteration, the loop body block
is executed first, and then whether to execute the loop body again is determined based on the value of expression
. That is, the loop body in the do-while
expression is executed at least once. Example:
main(): Int64 {
var hundred = 0
do {
hundred++
} while (hundred < 100)
return 0
}
Summary of Loop Expressions
The expression capabilities of the for-in
, while
, and do-while
expressions are equivalent. Generally, if the number of loops is known or all elements in a sequence are traversed, for-in
expressions are used. If the number of loops is unknown but the loop termination condition is known, while
or do-while
expressions are used.
The three loop expressions are of the Unit
type.
The break
and continue
expressions must have their loop bodies. Therefore, for the three types of loop expressions, break
or continue
in the loop condition is bound to the nearest outer loop. If the outer loop does not exist, an error is reported. For example:
while (true) {
println("outer") // printed once
do {
println("inner") // printed once
} while (break) // stop the execution of the outer loop
println("unreached") // not printed
}
try Expressions
The try expressions are classified into ordinary try expressions that do not involve automatic resource management and try-with-resources expressions that involve automatic resource management.
The syntax of try expressions is as follows:
tryExpression
: 'try' block 'finally' block
| 'try' block ('catch' '(' catchPattern ')' block)+ ('finally' block)?
| 'try' '(' resourceSpecifications ')' block ('catch' '(' catchPattern ')' block)* ('finally' block)?
;
Ordinary try expressions are used for error handling. For details, see chapter [Exceptions].
The try-with-resources
expression is used to automatically release non-memory resources. For details, see chapter [Exceptions].
Control Transfer Expressions
Control transfer expressions change the execution sequence of programs. The type of control transfer expressions is Nothing
, which is a subtype of any type. Cangjie provides the following control transfer expressions:
break
continue
return
throw
Like any other expression, a control transfer expression can be used as a subexpression to become a part of a complex expression, but may cause unreachable code (a compilation alarm will be generated for the unreachable part).
main(): Int64 {
return return 1 // warning: the left return expression is unreachable
}
In the control transfer expression, break
and continue
must have their surrounding loop bodies, and the loop bodies cannot cross the function boundary. return
must have its surrounding function body, and the function body cannot cross the function boundary. There is no requirement on throw
.
A surrounding loop body "cannot cross" the function boundary. In the following example, break
appears in the f
function, and the outer while loop body is not considered as the loop body that surrounds break
. continue
appears in the lambda expression, and the outer while loop body is not considered as the loop body that surrounds continue
.
while (true) {
func f() {
break // Error: break must be used directly inside a loop
}
let g = { =>
continue // Error: continue must be used directly inside a loop
}
}
The syntax of control transfer expressions is as follows:
jumpExpression
: 'break'
| 'continue'
| 'return' expression?
| 'throw' expression
;
break Expressions
A break
expression can be used only in the loop body of a loop expression, and the execute permission of the program is granted to the expression after the terminated loop expression. For example, the following code uses a break
expression in the while
loop body to calculate the least common multiple of 4
and 6
in the interval [1,49]
.
main(): Int64 {
var index: Int32 = 0
while (index < 50) {
index = index + 1
if ((index % 4 == 0) && (index % 6 == 0)) {
print("${index} is divisible by both 4 and 6") // output: 12
break
}
}
return 0
}
Note that when break
appears in a nested loop expression, only the loop expression that directly surrounds it can be terminated. The outer loop is not affected. For example, the following program outputs 12 is divisible by both 4 and 6
for five times and outputs the value of i
each time:
main(): Int64 {
var index: Int64 = 0
for (i in 0..5) {
index = i
while (index < 20) {
index = index + 1
if ((index % 4 == 0) && (index % 6 == 0)) {
print("${index} is divisible by both 4 and 6")
break
}
}
print("${i}th test")
}
return 0
}
continue Expressions
A continue
expression can be used only in the loop body of a loop expression. It ends the current iteration of the closest loop expression in advance and then start a new round of loop (the loop expression is not terminated). For example, in the following code output range [1,49]
, all numbers (12
, 24
, 36
, and 48
) that can be exactly divided by both 4
and 6
are output. Other numbers that do not meet the requirements are also explicitly output.
main(): Int64 {
var index: Int32 = 0
while (index < 50) {
index = index + 1
if ((index % 4 == 0) && (index % 6 == 0)) {
print("${index} is divisible by both 4 and 6")
continue
}
print("${index} is not what we want")
}
return 0
}
return Expressions
A return
expression can be used only in the function body. It can terminate the execution of a function at any position and return a value, transferring the control flow from the called function to the calling function. A return
statement can be in the following two formats: return
and return expr
(expr
indicates an expression).
- If an expression is in
return expr
format, the value ofexpr
is the return value of the function. Therefore, the type ofexpr
must be the same as the return type in the function definition.
// return expression
func larger(a: Int32, b: Int32): Int32 {
if (a >= b) {
return a
} else {
return b
}
}
- If an expression is in
return
format, it is considered as the syntactic sugar ofreturn()
. Therefore, the return type of the function must also beUnit
.
// return expression
func equal(a: Int32, b: Int32): Unit {
if (a == b) {
print("a is equal to b")
return
} else {
print("a is not equal to b")
}
}
It should be noted that, the type of the return
expression as a whole is not determined by its following expression (the expression following return
is ()
), but is of the Nothing
type.
throw Expressions
The throw
expressions throw exceptions. When a code block that contains a throw
expression is called, if the throw
expression is executed, the corresponding exception is thrown. The predefined exception handling logic captures and processes the exception, changing the program execution process.
In the following example, an arithmetic exception is thrown when the divisor is 0:
func div(a: Int32, b: Int32): Int32 {
if (b != 0) {
return a / b
} else {
throw ArithmeticException()
}
}
This section provides only the simplest examples of the return
and throw
expressions. For details, see chapters [Functions] and [Exceptions].
Numeric Type Conversion Expressions
A numeric type conversion expression implements conversion between numeric types. It adopts the value and the target type (the type of the original expression is not affected by the target type) after type conversion. For details about the conversion rules, see [Type Conversion].
The syntax of numeric type conversion expressions is defined as follows:
numericTypeConvExpr
: numericTypes '(' expression ')'
;
numericTypes
: 'Int8'
| 'Int16'
| 'Int32'
| 'Int64'
| 'UInt8'
| 'UInt16'
| 'UInt32'
| 'UInt64'
| 'Float16'
| 'Float32'
| 'Float64'
;
this and super Expressions
The this
and super
expressions are represented by this
and super
respectively. this
can appear in all instance member functions and constructors, indicating the current instance. super
can appear only in the class definitions, indicating the instance of the direct superclass of the currently defined type (For details, see [Classes]). Do not use an independent super
expression.
The syntax of this
and super
expressions is defined as follows:
thisSuperExpression
: 'this'
| 'super'
;
spawn Expressions
A spawn expression creates and starts a thread
. For details, see [Concurrency].
synchronized Expressions
The synchronized expressions are used in the synchronization mechanism. For details, see [Concurrency].
Parenthesized Expressions
A parenthesized expression is an expression enclosed in parentheses. The subexpression enclosed in parentheses is considered as a separate calculation unit and is preferentially calculated.
The syntax of parenthesized expressions is defined as follows:
parenthesizedExpression
: '(' expression ')'
;
Example of a parenthesized expression:
1 + 2 * 3 - 4 // The result is 3.
(1 + 2) * 3 - 4 // The result is 5.
Postfix Expressions
A postfix expression consists of an expression and a postfix operator. Based on operators, postfix expressions are classified into member access expressions, function call expressions, and index access expressions. When the optional ?
operator is used before their operators, the Option type can support these operators. For details about the ?
operator, see the following description.
The syntax of postfix expressions is defined as follows:
postfixExpression
: atomicExpression
| type '.' identifier
| postfixExpression '.' identifier typeArguments?
| postfixExpression callSuffix
| postfixExpression indexAccess
| postfixExpression '.' identifier callSuffix? trailingLambdaExpression
| identifier callSuffix? trailingLambdaExpression
| postfixExpression ('?' questSeperatedItems)+
;
Member Access Expressions
The syntax of member access expressions is defined as the third item of the preceding postfix expression syntax:
postfixExpression '.' identifier typeArguments?
Member access expressions can be used to access members of a class, interface, and struct.
A member access expression is in the format of T.a
. T
indicates a specific instance or type name, which is called the body of the member access expression. a
indicates the name of a member.
-
If
T
is an instantiated object of a class, non-static members in the class or interface can be accessed in this way. -
If
T
is an instance ofstruct
, non-static members instruct
can be accessed by instance name. -
If
T
is the name of a class, interface, orstruct
, its static members can be accessed directly by type name.
Note that the access subject of static members of the class, interface, and struct
can only be type names.
-
T
isthis
: In the scope of a class or interface, thethis
keyword can be used to access non-static members. -
T
issuper
: In the scope of a class or interface, thesuper
keyword can be used to access the non-static members of the superclass of the current class object.
For a member access expression e.a
, if e
is a type name:
- When
a
is a mutable static member variable ofe
,e.a
is mutable. In other cases,e.a
is immutable.
If e
is an expression (assuming that the type of e
is T
):
-
When
T
is of the reference type, ifa
is a mutable instance member variable ofT
,e.a
is mutable. Otherwise,e.a
is immutable. -
When
T
is of the value type, ife
is mutable anda
is a mutable instance member variable ofT
,e.a
is mutable. Otherwise,e.a
is immutable.
Function Call Expressions
The syntax of function call expressions is defined as the fourth item of the preceding postfix expression syntax. The syntax of callSuffix
and valueArgument
is defined as follows:
callSuffix
: '(' (valueArgument (',' valueArgument)*)? ')'
;
valueArgument
: identifier ':' expression
| expression
| refTransferExpression
;
refTransferExpression
: 'inout' (expression '.')? identifier
;
Function call expressions are used to call functions. For details about functions, see [Functions].
For the function call expression f(x)
, assume that the type of f
is T
. If T
is of the function type, the function named f
is called. Otherwise, if T
overloads the function call operator ()
, f(x)
calls its operator overloading function ()
(see [Operators That Can Be Overloaded]).
Index Access Expressions
The syntax of index access expressions is defined as the fifth item of the preceding postfix expression syntax. The syntax of indexAccess
is defined as follows:
indexAccess
: '[' (expression | rangeElement) ']'
;
rangeElement
: '..'
| ('..=' | '..' ) expression
| expression '..'
;
Index access expressions are used for types that support index access (including the Array
and Tuple
types) to access elements in specific locations through indexes. For details, see [Array] and [Tuple] in [Types].
For the index access expression e[a]
(assuming that the type of e
is T
):
-
If
T
is of the Tuple type,e[a]
is immutable. -
If
T
is not of the Tuple type and it overloads the[]
operator in set form (see [Operators That Can Be Overloaded]),e[a]
is mutable. Otherwise,e[a]
is immutable.
For the index access expression e1[e2]
, the Cangjie language always evaluates e1
to v1
, then evaluates e2
to v2
, and finally selects the corresponding value based on the index v2
or calls the corresponding overloaded []
operator.
Question Mark Operators
The question mark operator ?
is a unary postfix operator. It must be used together with and before the postfix operators .
, ()
, {}
, or []
described above to enable the Option
type to support these postfix operators, such as a?.b
, a?(b)
, and a?[b]
. ()
is used for function call. When the last argument of the function call is lambda, the tailing closure syntax a?{b}
can be used. An expression that contains ?.
, ?()
, ?{}
, or ?[]
is called an optional chaining expression.
The syntax of optional chaining expressions is defined as the last item of the preceding postfix expression syntax. The syntax of questSeperatedItems
is defined as follows:
questSeperatedItems
: questSeperatedItem+
;
questSeperatedItem
: itemAfterQuest (callSuffix | callSuffix? trailingLambdaExpression | indexAccess)?
;
itemAfterQuest
: '.' identifier typeArguments?
| callSuffix
| indexAccess
| trailingLambdaExpression
;
The rules for the optional chaining expressions are as follows:
-
For the expression
e
, delete all?
ine
, change the type of the expression right before?
fromOption<T>
toT
, and obtain the expressione1
. If the type ofe1
is Option, add?
betweene
and.
,()
,{}
, or[]
when using these operators aftere
. Otherwise, do not add?
. -
The type of an optional chaining expression is
Option<T>
(That is, no matter how many?
operators exist, the type has only one layer of Option). The typeT
is that of the last expression (variable or function name, function call expression, or index access expression) in the optional chaining expression. -
If the value of an expression of the Option type in the optional chaining expression is
None
, the value of the entire optional chaining expression isNone
. If the value of each expression of the Option type in the optional chaining expression is equal to aSome
value, the value of the entire expression isSome(v)
(the type ofv
is that of the last expression).
The following examples use the expressions a?.b
, c?(d)
, and e?[f]
:
-
The expression
a
must be of anOption<T1>
type, andT1
must contain the instance memberb
. The expressionc
must be of anOption<(T2)->U2>
type, and the type ofd
must beT2
. The expressione
must be of anOption<T3>
type, andT3
supports index operators. -
The types of expressions
a?.b
,c?(d)
, ande?[f]
areOption<U1>
,Option<U2>
, andOption<U3>
, whereU1
is the type of the instance memberb
inT1
,U2
is the return value type of the function type(T2)->U2
, andU3
is the return type of the index operation performed byT3
. -
When the values of
a
,c
, ande
areSome(v1)
,Some(v2)
, andSome(v3)
, respectively, the values ofa?.b
,c?(d)
, ande?[f]
areOption<U1>.Some(v1.b)
,Option<U2>.Some(v2(d))
, andOption<U3>.Some(v3[f])
, respectively. When the values ofa
,c
, ande
areNone
, the values ofa?.b
,c?(d)
, ande?[f]
areOption<U1>.None
,Option<U2>.None
, andOption<U3>.None
, respectively. (Note thatb
,d
, andf
are not evaluated.)
The expressions a?.b
, c?(d)
, and e?[f]
are equivalent to the following match
expressions respectively:
// a?.b is equivalent to the following match expression.
match (a) {
case Some(v) => Some(v.b)
case None => None<U1>
}
// c?(d) is equivalent to the following match expression.
match (c) {
case Some(v) => Some(v(d))
case None => None<U2>
}
// e?[f] is equivalent to the following match expression.
match (e) {
case Some(v) => Some(v[f])
case None => None<U3>
}
The next example contains multiple ?
in the multi-layer access expression a?.b.c?.d
(?.
is an example that can be applied in other operations in similar ways.)
-
The
a
expression must be of anOption<Ta>
type, andTa
must contain the instance memberb
. The type ofb
must contain the instance member variablec
, andc
must be of anOption<Tc>
type.Tc
must contain the instance memberd
. -
The type of the expression
a?.b.c?.d
isOption<Td>
, whereTd
is the type of the instance memberd
ofTc
. -
When the value of
a
is equal toSome(va)
and that ofva.b.c
is equal toSome(vc)
, the value ofa?.b.c?.d
is equal toOption<Td>.Some(vc.d)
. When the value ofa
is equal toSome(va)
and that ofva.b.c
is equal toNone
, the value ofa?.b.c?.d
is equal toOption<Td>.None
(d
is not evaluated). When the value ofa
is equal toNone
, the value ofa?.b.c?.d
is equal toOption<Td>.None
(b
,c
, andd
are not evaluated).
The expression a?.b.c?.d
is equivalent to the following match
expression:
// a?.b.c?.d is equivalent to the following match expression.
match (a) {
case Some(va) =>
let x = va.b.c
match (x) {
case Some(vc) => Some(vc.d)
case None => None<Td>
}
case None =>
None<Td>
}
Optional chaining expressions can also be used as lvalue expressions (see [Assignment Expressions]), such as a?.b = e1
, a?[b] = e2
, and a?.b.c?.d = e3
.
If an optional chaining expression is on the left of an assignment expression, the former must be mutable (For details, see [Assignment Expressions]). Because the function type is immutable, only ?.
and ?[]
need to be considered. Both of them fall into two basic scenarios: a?.b = c
and a?[b] = c
(assuming that the type of a
is Option<T>
). The rules are as follows:
-
a?.b
is mutable only whenT
is of the reference type andb
is mutable. In other cases,a?.b
is immutable. -
a?[b]
is mutable only whenT
is of the reference type and[]
in set mode is overloaded. In other cases,a?[b]
is immutable.
When a?.b
(or a?[b]
) is mutable, if the value of a
is equal to Option<T>.Some(v)
, assign the value of c
to v.b
(or v[b]
); if the value of a
is equal to Option<T>.None
, do not perform any operation (b
and c
are not evaluated).
Similarly, the expressions a?.b = e1
, a?[b] = e2
, and a?.b.c?.d = e3
are equivalent to the following match
expressions:
// a?.b = e1 is equivalent to the following match expression.
match (a) {
case Some(v) => v.b = e1
case None => ()
}
// a?[b] = e2 is equivalent to the following match expression.
match (a) {
case Some(v) => v[b] = e2
case None => ()
}
// a?.b.c?.d = e3 is equivalent to the following match expression.
match (a) {
case Some(va) =>
match (va.b.c) {
case Some(vc) => vc.d = e3
case None => ()
}
case None =>
()
}
The following example is how the ?
operator is used:
// The usuage of ?.
class C {
var item: Int64 = 100
}
let c = C()
let c1 = Option<C>.Some(c)
let c2 = Option<C>.None
let r1 = c1?.item // r1 = Option<Int64>.Some(100)
let r2 = c2?.item // r2 = Option<Int64>.None
func test1() {
c1?.item = 200 // c.item = 200
c2?.item = 300 // no effect
}
// The usuage of ?()
let foo = {i: Int64 => i + 1}
let f1 = Option<(Int64) -> Int64>.Some(foo)
let f2 = Option<(Int64) -> Int64>.None
let r3 = f1?(1) // r3 = Option<Int64>.Some(2)
let r4 = f2?(1) // r4 = Option<Int64>.None
// The usuage of ?[] for tuple access
let tuple = (1, 2, 3)
let t1 = Option<(Int64, Int64, Int64)>.Some(tuple)
let t2 = Option<(Int64, Int64, Int64)>.None
let r7 = t1?[0] // r7 = Option<Int64>.Some(1)
let r8 = t2?[0] // r8 = Option<Int64>.None
func test3() {
t1?[0] = 10 // error: 't1?[0]' is immutable
t2?[1] = 20 // error: 't2?[0]' is immutable
}
Increment and Decrement Expressions
Increment and decrement expressions are expressions that contain increment operators (++
) or decrement operators (--
). a++
and a--
are syntactic sugars of a+=1
and a-=1
, respectively. The increment and decrement operators add 1
to and subtract 1
from values, and can be used only as postfix operators. ++
and --
are non-associative. Therefore, it is (syntactically) forbidden that an expression contains two or more ++/--
operators, such as a++--
but does not use parentheses to specify the calculation sequence.
The syntax of increment and decrement expressions is defined as follows:
incAndDecExpression
: postfixExpression ('++' | '--' )
;
The rules for expr++
(or expr--
) expression are as follows:
-
The type of
expr
must be integer. -
Because
expr++
(orexpr--
) is the syntactic sugar ofexpr += 1
(orexpr -= 1
),expr
must also be assignable (see [Assignment Expressions]). -
The type of
expr++
(orexpr--
) isUnit
.
The following examples are the increment and decrement expressions:
var i: Int32 = 5
i++ // i = 6
i-- // i = 5
i--++ // syntax error
var j = 0
j = i-- // semantics error
Arithmetic Expressions
An arithmetic expression is an expression that contains arithmetic operators. Cangjie supports the following arithmetic operators: unary minus sign (-
), addition (+
), subtraction (-
), multiplication (*
), division (/
), modulo (%
), and exponentiation (**
). Except that the unary minus sign is a unary prefix operator, other operators are binary infix operators. Their precedence and associativity are described below.
The syntax of arithmetic expressions is defined as follows:
prefixUnaryExpression
: prefixUnaryOperator* incAndDecExpression
;
prefixUnaryOperator
: '-'
| ...
;
additiveExpression
: multiplicativeExpression (additiveOperator multiplicativeExpression)*
;
multiplicativeExpression
: exponentExpression (multiplicativeOperator exponentExpression)*
;
exponentExpression
: prefixUnaryExpression (exponentOperator prefixUnaryExpression)*
;
additiveOperator
: '+' | '-'
;
multiplicativeOperator
: '*' | '/' | '%'
;
exponentOperator
: '**'
;
The operand of the unary minus sign (-
) must be an expression of the numeric type. The value of the unary prefix minus sign expression is equal to the negated value of the operand. The type is the same as that of the operand.
let num1: Int64 = 8
let num2 = -num1 // num2 = -8, with 'Int64' type
let num3 = -(-num1) // num3 = 8, with 'Int64' type
For binary operators *
, /
, %
, +
, and -
, the two operands must be of the same type. The operand of %
can only be an integer. The operands of *
, /
, +
, and -
can be of any numeric type.
let a = 2 + 3 // add: 5
let b = 3 - 1 // sub: 2
let c = 3 * 4 // multi: 12
let d = 6.6 / 1.1 // division: 6
let e = 4 % 3 // mod: 1
In particular, when the operand of the division (/
) is an integer, the non-integer value is rounded to an integer, rounding toward 0. The value of the integer modulo operation a % b
is defined as a - b * (a / b)
.
let q1 = 7 / 3 // integer division: 2
let q2 = -7 / 3 // integer division: -2
let q3 = 7 / -3 // integer division: -2
let q4 = -7 / -3 // integer division: 2
let r1 = 7 % 3 // integer remainder: 1
let r2 = -7 % 3 // integer remainder: -1
let r3 = 7 % -3 // integer remainder: 1
let r4 = -7 % -3 // integer remainder: -1
**
indicates the exponentiation operation. For example, x**y
indicates that the y exponent of the base x is calculated. The left operand of **
must be of the Int64
or Float64
type.
-
When the left operand is of the
Int64
type, the right operand must be of theUInt64
type, and the expression type must beInt64
. -
When the left operand is of the
Float64
type, the right operand must be of theInt64
orFloat64
type, and the expression type must beFloat64
.
let p1 = 2 ** 3 // p1 = 8
let p2 = 2 ** UInt64(3 ** 2) // p2 = 512
let p3 = 2.0 ** 3 // p3 = 8.0
let p4 = 2.0 ** 3 ** 2 // p4 = 512.0
let p5 = 2.0 ** 3.0 // p5 = 8.0
let p6 = 2.0 ** 3.0 ** 2.0 // p6 = 512.0
When the left operand type is Float64
and the right operand type is Int64
, the value of the exponentiation expression needs to be specified in some special cases as follows:
x ** 0 = 1.0 // for any x
0.0 ** n = POSITIVE_INFINITY // for odd n < 0
-0.0 ** n = NEGATIVE_INFINITY // for odd n < 0
0.0 ** n = POSITIVE_INFINITY // for even n < 0
-0.0 ** n = POSITIVE_INFINITY // for even n < 0
0.0 ** n = 0.0 // for even n > 0
-0.0 ** n = 0.0 // for even n > 0
0.0 ** n = 0.0 // for odd n > 0
-0.0 ** n = -0.0 // for odd n > 0
POSITIVE_INFINITY ** n = POSITIVE_INFINITY // for n > 0
NEGATIVE_INFINITY ** n = NEGATIVE_INFINITY // for odd n > 0
NEGATIVE_INFINITY ** n = POSITIVE_INFINITY // for even n > 0
POSITIVE_INFINITY ** n = 0.0 // for n < 0
NEGATIVE_INFINITY ** n = -0.0 // for odd n < 0
NEGATIVE_INFINITY ** n = 0.0 // for even n < 0.
Note: In addition to the preceding special cases, when the value of the left operand is
NaN
, the value of the exponentiation expression is equal toNaN
regardless of the value of the right operand.
When the left operand type is Float64
and the right operand type is Float64
, the value of the exponentiation expression needs to be specified in some special cases as follows:
x ** 0.0 = 1.0 // for any x
x ** -0.0 = 1.0 // for any x
0.0 ** y = POSITIVE_INFINITY // for the value of y is equal to an odd integer < 0
-0.0 ** y = NEGATIVE_INFINITY // for the value of y is equal to an odd integer < 0
0.0 ** NEGATIVE_INFINITY = POSITIVE_INFINITY
-0.0 ** NEGATIVE_INFINITY = POSITIVE_INFINITY
0.0 ** POSITIVE_INFINITY = 0.0
-0.0 ** POSITIVE_INFINITY = 0.0
0.0 ** y = 0.0 // for finite y > 0.0 and its value is equal to an odd integer
-0.0 ** y = -0.0 // for finite y > 0.0 and its value is equal to an odd integer
-1.0 ** POSITIVE_INFINITY = 1.0
-1.0 ** NEGATIVE_INFINITY = 1.0
1.0 ** y = 1.0 // for any y
x ** POSITIVE_INFINITY = 0.0 // for -1.0 < x < 1.0
x ** POSITIVE_INFINITY = POSITIVE_INFINITY // for any x < -1.0 or for any x > 1.0
x ** NEGATIVE_INFINITY = POSITIVE_INFINITY // for -1.0 < x < 1.0
x ** NEGATIVE_INFINITY = 0.0 // for any x < -1.0 or for any x > 1.0
POSITIVE_INFINITY ** y = 0.0 // for y < 0.0
POSITIVE_INFINITY ** y = POSITIVE_INFINITY // for y > 0.0
NEGATIVE_INFINITY ** y = -0.0 // for finite y < 0.0 and its value is equal to an odd integer
NEGATIVE_INFINITY ** y = NEGATIVE_INFINITY // for finite y > 0.0 and its value is equal to an odd integer
NEGATIVE_INFINITY ** y = 0.0 // for finite y < 0.0 and its value is not equal to an odd integer
NEGATIVE_INFINITY ** y = POSITIVE_INFINITY // for finite y > 0.0 and its value is not equal to an odd integer
0.0 ** y = POSITIVE_INFINITY // for finite y < 0.0 and its value is not equal to an odd integer
-0.0 ** y = POSITIVE_INFINITY // for finite y < 0.0 and its value is not equal to an odd integer
0.0 ** y = 0.0 // for finite y > 0.0 and its value is not equal to an odd integer
-0.0 ** y = 0.0 // for finite y > 0.0 and its value is not equal to an odd integer
x ** y = NaN // for finite x < 0.0 and finite y whose value is not equal to an integer
Note: In addition to the special cases listed above, if the value of an operand is
NaN
, the value of the exponentiation expression is equal toNaN
.
If the result of an arithmetic expression is of the integer type, integer overflow may occur. Cangjie provides three attribute macros and compilation options to control the processing behavior of integer overflow (hereinafter referred to as behavior). They are shown in the following table.
attributes | Options | behavior | explaining for behavior |
---|---|---|---|
@OverflowThrowing | --int-overflow throwing | throwing | throwing an exception |
@OverflowWrapping | --int-overflow wrapping | wrapping | wrapping around at the numeric bounds of the type |
@OverflowSaturating | --int-overflow saturating | saturating | saturating at the numeric bounds of the type |
Note: 1. The default behavior is throwing. 2. If an attribute macro and a compilation option are used in a range at the same time and represent different behaviors, the behavior represented by the attribute macro prevails.
Behavior example:
@OverflowThrowing
func test1(x: Int8, y: Int8) { // if x equals to 127 and y equals to 3
let z = x + y // throwing OverflowException
}
@OverflowWrapping
func test2(x: Int8, y: Int8) { // if x equals to 127 and y equals to 3
let z = x + y // z equals to -126
}
@OverflowSaturating
func test3(x: Int8, y: Int8) { // if x equals to 127 and y equals to 3
let z = x + y // z equals to 127
}
The following is an example of the conflict between the scope of an attribute macro and that of a compilation option:
// Compile with cjc --int-overflow saturating test.cj
// this file's name is test.cj
@OverflowWrapping
func test2(x: Int8, y: Int8) {
let z = x + y // the behavior is wrapping
}
func test3(x: Int8, y: Int8) {
let z = x + y // the behavior is saturating
}
In particular, for INT_MIN * -1
, INT_MIN / -1
, and INT_MIN % -1
, the specified behaviors are as follows:
Expression | Throwing | Wrapping | Saturating |
---|---|---|---|
INT_MIN * -1 or -1 * INT_MIN | throwing OverflowException | INT_MIN | INT_MAX |
INT_MIN / -1 | throwing OverflowException | INT_MIN | INT_MAX |
INT_MIN % -1 | 0 | 0 | 0 |
Note that in the scenario where the integer overflow behavior is throwing, if the integer overflow can be detected in advance in the compilation phase, the compiler directly reports an error.
Relational Expressions
A relational expression is an expression that contains relational operators. There are six
relational operators: equality (==
), inequality (!=
), less than (<
), greater than (<=
), less than or equal to (>
), and greater than or equal to (>=
). Relational operators are binary operators, and the types of the two operands must be the same. A relational expression is of the Bool
type. That is, its value can only be true
or false
. Precedence and associativity of relational operators are described below.
The syntax of relational expressions is defined as follows:
equalityComparisonExpression
: comparisonOrTypeExpression (equalityOperator comparisonOrTypeExpression)?
;
comparisonOrTypeExpression
: shiftingExpression (comparisonOperator shiftingExpression)?
| ...
;
equalityOperator
: '!=' | '=='
;
comparisonOperator
: '<' | '>' | '<=' | '>='
;
The following examples are relational expressions:
main(): Int64 {
3 < 4 // return true
3 <= 3 // return true
3 > 4 // return false
3 >= 3 // return true
3.14 == 3.15 // return false
3.14 != 3.15 // return true
return 0
}
It should be noted that a relational operator is non-associative, that is, an expression similar to a < b < c
cannot be written.
main(): Int64 {
3 < 4 < 5 // error: `<` is non-associative
3 == 3 != 4 // error: `==` and `!=` are non-associative
return 0
}
type test and type cast Expressions
A type test expression is an expression that contains the operator is
, and a type cast expression is an expression that contains the operator as
. The precedence and associativity of is
and as
are described below.
The syntax of type test and type cast expressions is defined as follows:
comparisonOrTypeExpression
: ...
| shiftingExpression ('is' type)?
| shiftingExpression ('as' userType)?
;
is Operators
e is T
is an expression used for type checking. The type of e is T
is Bool
. e can be any type of expression, and T can be any type.
When the runtime type R of e is a subtype of T, the value of e is T
is true
; otherwise, the value is false
.
The following are examples of is
operators:
open class Base {
var name: String = "Alice"
}
class Derived1 <: Base {
var age: UInt8 = 18
}
class Derived2 <: Base {
var gender: String = "female"
}
main(): Int64 {
var testVT = 1 is Int64 // testVT = true
testVT = 1 is String // testVT = false
testVT = true is Int64 // testVT = false
testVT = [1, 2, 3] is Array<Int64> // testVT = true
let base1: Base = Base()
let base2: Base = Derived1()
let base3: Base = Derived2()
let derived1: Derived1 = Derived1()
let derived2: Derived2 = Derived2()
var test = base1 is Base // test = true
test = base1 is Derived1 // test = false
test = base1 is Derived2 // test = false
test = base2 is Base // test = true
test = base2 is Derived1 // test = true
test = base2 is Derived2 // test = false
test = base3 is Base // test = true
test = base3 is Derived1 // test = false
test = base3 is Derived2 // test = true
test = derived1 is Base // test = true
test = derived1 is Derived1 // test = true
test = derived1 is Derived2 // test = false
test = derived2 is Base // test = true
test = derived2 is Derived1 // test = false
test = derived2 is Derived2 // test = true
return 0
}
as Operators
e as T
is an expression used for type conversion. The type of e as T
is Option<T>
. e can be any type of expression, and T can be any specific type.
When the runtime type R of e is a subtype of T, the value of e as T
is Some(e)
; otherwise, the value is None
.
The following are examples of as
operators:
open class Base {
var name: String = "Alice"
}
class Derived1 <: Base {
var age: UInt8 = 18
}
class Derived2 <: Base {
var gender: String = "female"
}
main(): Int64 {
let base1: Base = Base()
let base2: Base = Derived1()
let base3: Base = Derived2()
let derived1: Derived1 = Derived1()
let derived2: Derived2 = Derived2()
let castOP1 = base1 as Base // castOP = Option<Base>.Some(base1)
let castOP2 = base1 as Derived1 // castOP = Option<Derived1>.None
let castOP3 = base1 as Derived2 // castOP = Option<Derived2>.None
let castOP4 = base2 as Base // castOP = Option<Base>.Some(base2)
let castOP5 = base2 as Derived1 // castOP = Option<Derived1>.Some(base2)
let castOP6 = base2 as Derived2 // castOP = Option<Derived2>.None
let castOP7 = base3 as Base // castOP = Option<Base>.Some(base3)
let castOP8 = base3 as Derived1 // castOP = Option<Derived1>.None
let castOP9 = base3 as Derived2 // castOP = Option<Derived2>.Some(base3)
let castOP10 = derived1 as Base // castOP = Option<Base>.Some(derived1)
let castOP11 = derived1 as Derived1 // castOP = Option<Derived1>.Some(derived1)
let castOP12 = derived1 as Derived2 // castOP = Option<Derived2>.None
let castOP13 = derived2 as Base // castOP = Option<Base>.Some(derived2)
let castOP14 = derived2 as Derived1 // castOP = Option<Derived1>.None
let castOP15 = derived2 as Derived2 // castOP = Option<Derived2>.Some(derived2)
return 0
}
Bitwise Operation Expression
A bitwise expression is an expression that contains bitwise operators. Cangjie supports one unary prefix bitwise operation operator bitwise NOT (!
) and five binary infix bitwise operation operators, including left shift (<<
), right shift (>>
), bitwise AND (&
), and bitwise XOR (^
), and bitwise OR (|
). The operands of bitwise operators must be of the integer type. The operands are considered as binary sequences, and then logical operations (0
is considered as false
and 1
is considered as true
) or shift operations are performed on each bit to implement bitwise operations. In operations with &
, ^
, and |
, logical operations are performed between bits (For details, see [Logical Expressions]). The precedence and associativity of bitwise operators are described below.
The syntax of bitwise operation expressions is defined as follows:
prefixUnaryExpression
: prefixUnaryOperator* incAndDecExpression
;
prefixUnaryOperator
: '!'
| ...
;
bitwiseDisjunctionExpression
: bitwiseXorExpression ( '|' bitwiseXorExpression)*
;
bitwiseXorExpression
: bitwiseConjunctionExpression ( '^' bitwiseConjunctionExpression)*
;
bitwiseConjunctionExpression
: equalityComparisonExpression ( '&' equalityComparisonExpression)*
;
shiftingExpression
: additiveExpression (shiftingOperator additiveExpression)*
;
shiftingOperator
: '<<' | '>>'
;
The following are example of bitwise operation expressions:
func foo(): Unit {
!10 // The result is -11
!20 // The result is -21
10 << 1 // The result is 20
10 << 1 << 1 // The result is 40
10 >> 1 // The result is 5
10 & 15 // The result is 10
10 ^ 15 // The result is 5
10 | 15 // The result is 15
1 ^ 8 & 15 | 24 // The result is 25
}
For a shift operator, its operand must be of the integer type (but the types of the two operands can be different). Regardless of whether the operator is left or right shift, the right operand cannot be a negative number. (If such an error is detected during compilation, an error is reported. If this error occurs during running, an exception is thrown.)
For a shift operation of an unsigned number, the shift and padding rules are as follows: left shift pads 0s for low bits and discards high bits, and right shift pads 0s for high bits and discards low bits. For a shift operation of a signed number, the shift and padding rules are as follows:
- The shift padding rules for positive numbers and unsigned numbers are the same.
- If negative numbers are shifted left, 0s are padded to the low bits and high bits are discarded.
- If negative numbers are shifted right, 1s are padded to the high bits and low bits are discarded.
let p: Int8 = -30
let q = p << 2 // q = -120
let r = p >> 2 // r = -8
let r = p >> -2 // error
let x: UInt8 = 30
let b = x << 3 // b = 240
let b = x >> 1 // b = 15
In addition, if the number of bits (right operand) shifted right or left is greater than or equal to the operand width, this is an overshift
. If the overshift can be detected during compilation, an error is reported. Otherwise, an exception is thrown during running.
let x1 : UInt8 = 30 // 0b00011110
let y1 = x1 >> 11 // compilation error
Range Expressions
A range expression is an expression that contains interval operators. A range expression is used to create Range
instances. The syntax of range expressions is defined as follows:
rangeExpression
: bitwiseDisjunctionExpression ('..=' | '..') bitwiseDisjunctionExpression (':' bitwiseDisjunctionExpression)?
| bitwiseDisjunctionExpression
;
There are two types of range operators: ..
and ..=
, which are used to create "left-closed and right-open" and "left-closed and right-closed" Range
instances respectively. For details, see [Range].
Logical Expressions
A logical expression is an expression that contains logical operators. The operand of a logical operator must be an expression of the Bool
type. Cangjie supports three logical operators: logical NOT (!
), logical AND (&&
), and logical OR (||
). Their precedence and associativity are described below.
The syntax of logical expressions is defined as follows:
prefixUnaryExpression
: prefixUnaryOperator* incAndDecExpression
;
prefixUnaryOperator
: '!'
| ...
;
logicDisjunctionExpression
: logicConjunctionExpression ( '||' logicConjunctionExpression)*
;
logicConjunctionExpression
: rangeExpression ( '&&' rangeExpression)*
;
The logical NOT (!
) is a unary operator, which is used to negate the Boolean value of its operand: The value of !false
is true
. The value !true
is false
.
The logical AND (&&
) and logical OR (||
) are binary operators. For the expression expr1 && expr2
, its value is equal to true
only when the values of expr1
and expr2
are equal to true
. For the expression expr1 || expr2
, its value is equal to false
only when the values of expr1
and expr2
are equal to false
.
Short-circuit evaluation is used for &&
and ||
. When expr1 && expr2
is calculated, expr2
does not need to be evaluated if expr1=false
, and the value of the entire expression is false
. When expr1 || expr2
is calculated, expr2
does not need to be evaluated if expr1=true
, and the value of the entire expression is true
.
main(): Int64 {
let expr1 = false
let expr2 = true
!true // Logical NOT, return false.
1 > 2 && expr1 // Logical AND, return false without computing the value of expr1.
1 < 2 || expr2 // Logical OR, return true without computing the value of expr2.
return 0
}
coalescing Expressions
The coalescing
expression is an expression that contains coalescing
operators. The coalescing
operator is a binary infix operator represented by ??
. Its precedence and associativity are described below.
The syntax of coalescing
expressions is defined as follows:
coalescingExpression
: logicDisjunctionExpression ('??' logicDisjunctionExpression)*
;
The coalescing
operator is used to deconstruct the Option
type. Assume that the type of the expression e1
is Option<T>
. For the expression e1 ?? e2
:
-
The expression
e2
has the typeT
. -
The expression
e1 ?? e2
has the typeT
. -
When the value of
e1
is equal toOption<T>.Some(v)
, the value ofe1 ?? e2
is equal tov
(in this case,e2
is not evaluated, that is, the short-circuit evaluation condition is met). When the value ofe1
is equal toOption<T>.None
, the value ofe1 ?? e2
is equal toe2
.
The expression e1 ?? e2
is the syntactic sugar of the following match
expression:
// when e1 is Option<T>
match (e1) {
case Some(v) => v
case None => e2
}
The following are examples of using the coalescing
expressions:
main(): Int64 {
let v1 = Option<Int64>.Some(100)
let v2 = Option<Int64>.None
let r1 = v1 ?? 0
let r2 = v2 ?? 0
print("${r1}") // output: 100
print("${r2}") // output: 0
return 0
}
Flow Expressions
A flow expression is an expression that contains flow operators. There are two types of flow operators: |>
infix operator (called pipeline
) that represents the data flow direction and ~>
infix operator (called composition
) that represents a combination of functions. The precedence of |>
is the same as that of ~>
, and is between ||
and =
. |>
and ~>
are both left-associative. For details, see the following information. The syntax of flow expressions is defined as follows:
flowExpression
: logicDisjunctionExpression (flowOperator logicDisjunctionExpression)*
;
flowOperator
: '|>' | '~>'
;
pipeline
Operators
The pipeline
expression is the syntactic sugar of a single-parameter function call. For example, e1 |> e2
is the syntactic sugar of let v = e1; e2(v)
(that is, e1
on the left of the |>
operator is evaluated first). e2
is an expression of the function type, and the type of e1
is a subtype of the parameter type of e2
. Alternatively, the type of e2
overloads the function call operator ()
(see [Operators That Can Be Overloaded]).
Note: The f
cannot be an init
or super
constructor.
func f(x: Int32): Int32 { x + 1 }
let a: Int32 = 1
var res = a |> f // ok
var res1 = a |> {x: Int32 => x + 1} // ok
func h(b: Bool) { b }
let res3 = a < 0 || a > 10 |> h // Equivalence: (a < 0 || a > 10) |> h
func g<T>(x: T): T { x }
var res4 = a |> g<Int32> // ok
class A {
let a: Int32
let b: Int32
init(x: Int32) {
a = x
b = 0
}
init(x: Int32, y: Int32) {
x |> init // error: `init` is not a valid expression
b = y
}
}
// PIPELINE with operator `()` overloading
class A {
operator func ()(x: Int32) {
x
}
}
let obj = A()
let a: Int32 = 1
let res = a |> obj // Equivalence: obj(a)
composition
Operators
A composition
expression indicates a combination of two single-parameter functions. For example, the composition
expression e1 ~> e2
is the syntactic sugar of let f = e1; let g = e2; {x = > g(f(x))}
(that is, e1
on the left of the ~>
operator is evaluated first). If f
and g
are expressions of the function type or their type overloads the function call operator ()
(see [Operators That Can Be Overridden]) of a single parameter, the following four situations may occur:
e1 ~> e2 | The lambda expression |
---|---|
e1 and e2 are function types, and the return value type of e1 is a subtype of the argument type of e2 | let f = e1; let g = e2; {x => g(f(x))} |
The type f implements the single-parameter operator () overloading function, and g is a function type, and the return value type of f.operator() is a subtype of the argument type of g | let f = e1; let g = e2; {x => g(f.operator()(x))} |
f is a function type, and the type of g implements the the single-parameter operator () overloading function, and the return value type of f is a subtype of the argument type of g.operator() . | let f = e1; let g = e2; {x => g.operator()(f(x))} |
The types of f and g both implement the single-parameter operator () overloading function, and the return value type of f.operator() is a subtype of the argument type of g.operator() | let f = e1; let g = e2; {x => g.operator()(f.operator()(x))} |
Note: The evaluated values of e1
and e2
cannot be init
or super
.
func f(x: Int32): Float32 { Float32(x) }
func g(x: Float32): Int32 { Int32(x) }
var fg = f ~> g // Equivalence: {x: Int32 => g(f(x))}
let lambdaComp = {x: Int32 => x} ~> f // ok
func h1<T>(x: T): T { x }
func h2<T>(x: T): T { x }
var hh = h1<Int32> ~> h2<Int32> // ok
// COMPOSITION with operator `()` overloading
class A {
operator func ()(x: Int32): Int32 {
x
}
}
class B {
operator func ()(x: Float32): Float32 {
x
}
}
let objA = A()
let objB = B()
let af = objA ~> f // ok
let fb = f ~> objB // ok
let aa = objA ~> objA // ok
Assignment Expressions
An assignment expression is an expression that contains assignment operators. It is used to change the value of a left operand to the value of a right operand. The type of the right operand must be a subtype of the type of the left operand. When an assignment expression is evaluated, the expression on the right of =
is evaluated first, and then the expression on the left of =
is evaluated.
When a compound assignment expression is used, the left value of the expression on the left of =
is calculated first, and then the right value is obtained based on the left value. Then, the right value is used to calculate the expression on the right of =
(if there is a short-circuit rule, the rule is followed). Finally, a value is assigned.
Except for the value that can be assigned as defined by the subtype, if the right operand is a string literal that contains only a single character and the type of the left operand is Byte
or Rune
, the type of the string value is forcibly converted to Byte
or Rune
and assigned to the left operand. If the type conversion fails, the compiler reports an error.
Assignment operators are classified into ordinary assignment operators and compound assignment operators. The syntax of assignment expressions is defined as follows:
assignmentExpression
: leftValueExpressionWithoutWildCard assignmentOperator flowExpression
| leftValueExpression '=' flowExpression
| tupleLeftValueExpression `=` flowExpression
| flowExpression
;
tupleLeftValueExpression
: `(` (leftValueExpression | tupleLeftValueExpression) (`,` (leftValueExpression | tupleLeftValueExpression))+ `,`? `)`
;
leftValueExpression
: leftValueExpressionWithoutWildCard
| '_'
;
leftValueExpressionWithoutWildCard
: identifier
| leftAuxExpression '?'? assignableSuffix
;
leftAuxExpression
: identifier typeArguments?
| type
| thisSuperExpression
| leftAuxExpression ('?')? '.' identifier typeArguments?
| leftAuxExpression ('?')? callSuffix
| leftAuxExpression ('?')? indexAccess
;
assignableSuffix
: fieldAccess
| indexAccess
;
fieldAccess
: '.' identifier
;
assignmentOperator
: '=' | '+=' | '-=' | '**=' | '*=' | '/=' | '%=' | '&&=' | '||='
| '&=' | '|=' | '^=' | '<<=' | '>>='
;
The expression that appears on the left of a (compound) assignment operator is called a lvalue expression (leftValueExpression
in the preceding definition).
Syntax: The lvalue expression can be an identifier
, _
, or leftAuxExpression
followed by a assignableSuffix
(including fieldAccess
and indexAccess
). There can be optional ?
operators (syntactic sugar for assigning values to instances of Option Type) between leftAuxExpression
and assignableSuffix
. The leftAuxExpression
may be in the following syntax form: 1. an identifier
that includes an optional type of argument (typeArguments
); 2. this
or super
; 3. a leftAuxExpression
is followed by a .
(there can be optional ?
operators between them) and an identifier
with optional type arguments; 4. a leftAuxExpression
is followed by a function call postfix callSuffix
or an index access postfix indexAccess
(there can be optional ?
operators before callSuffix
or indexAccess
).
The semantics of lvalue expression can only be one of the following:
-
Variables indicated by
identifier
(see [Variable Names and Function Names]). -
The wildcard _ indicates that the evaluation result of the expression on the right of = is ignored. (Wildcards are not allowed in compound assignment expressions.)
-
Member access expression
e1.a
ore2?.a
(see [Member Access Expressions]) -
Index access expression
e1[a]
ore2?[a]
(see [Index Access Expressions])
Note:
e1
ande2
must be expressions that comply with theleftAuxExpression
syntax.
Left value expression is valid only when Left value expression is mutable. For details about the variability of the preceding expressions, see the corresponding chapters.
The type of assignment expressions is Unit
, and the value is ()
. In this way, problems such as incorrectly using an assignment expression as a judgment expression can be avoided. In the following example, if (a = b)
is executed first, the return value is ()
. However, ()
cannot be displayed on the left of =
. Therefore, an error is reported when ()=0
is executed. Similarly, the expression following if
must be of the Bool
type. Therefore, an error is also reported for the if
expression in the following example. In addition, =
is non-associative. Therefore, an expression such as a = b = 0
that contains more than two =
's cannot pass the syntax check.
main(): Int64 {
var a = 1
var b = 1
a = (b = 0) // semantics error
if (a = 5) { // semantics error
}
a = b = 0 // syntax error
return 0
}
The compound assignment expression a op = b
cannot be simply regarded as a = a op b
, a combination of an assignment expression and other binary operators (op
can be any binary operator among arithmetic operators, logical operators, and bitwise operators, and the types of operands a
and b
are the types required by the operator op
). In Cangjie, a
in a op = b
is evaluated only once (the side effect occurs only once), and a
in a = a op b
is evaluated twice (the side effect occurs twice). A compound assignment expression is also an assignment expression, so compound assignment operators are also non-associative. A compound assignment expression also requires that the two operands be of the same type.
The following examples describe how to use compound assignment expressions:
a **= b
a *= b
a /= b
a %= b
a += b
a -= b
a <<= b
a >>= b
a &&= b
a ||= b
a &= b
a ^= b
a |= b
Finally, if you overload the **
, *
, /
, %
, +
, -
, <<
, >>
, &
, ^
, or |
operator, Cangjie provides the default implementation of the corresponding compound assignment operator **=
, *=
, /=
, %=
, +=
, -=
, <<=
, >>=
, &=
, ^=
, or |=
. However, some additional requirements should be met. Otherwise, the value assignment semantics cannot be provided for a = a op b
.
-
The return type of an overloaded operator must be the same as the type of a left operand or its subtype. That is,
a
,b
, andop
ina op = b
must pass the type check ofa = a op b
. For example, when there is a subtype relationshipA <: B <: C
, if a type of+
overloaded by the user is(B, Int64) -> B
or(B, Int64) -> A
, Cangjie can provide a default implementation. If a type of+
overloaded by the user is(B, Int64) -> C
, Cangjie does not provide a default implementation. -
The value of
a
ina op= b
must be assignable, for example, a variable.
A multiple assignment expression is a special assignment expression. In such expression, there must be a tuple on the left and right of the equal sign (=) respectively. All elements in the left tuple must be left values, and the type of each element in the right tuple must be the subtype of the lvalue type in the corresponding position. Note: If _
is displayed in the tuple on the left, the evaluation result at the position corresponding to the tuple on the right of the equal sign is ignored (that is, the type check at this position can always be passed).
The multiple assignment expression can assign the value of the right tuple type to the corresponding left value in the left tuple at a time, so that code for assigning values one by one is not needed.
main(): Int64 {
var a: Int64
var b: Int64
(a, b) = (1, 2) // a == 1, b == 2
(a, b) = (b, a) // swap, a == 2, b == 1
(a, _) = (3, 4) // a == 3
(_, _) = (5, 6) // no assignment
return 0
}
A multiple assignment expression can be considered as a syntactic sugar in the following form: The expression on the right of the assignment expression is evaluated first, and then the left value is assigned one by one from left to right.
main(): Int64 {
var a: Int64
var b: Int64
(a, b) = (1, 2)
// desugar
let temp = (1, 2)
a = temp[0]
b = temp[1]
return 0
}
Lambda Expressions
A Lambda expression is a value of the function type. For details, see [Functions].
Quote Expressions
Quote expressions are used to reference code and represent it as operable data objects. They are mainly used for metaprogramming. For details, see [Metaprogramming].
Macro Call Expressions
Macro call expressions call macros defined by Cangjie and are mainly used for metaprogramming. For details, see [Metaprogramming].
Reference Value Transfer Expressions
Reference value transfer expressions can be used only when CFunc
is called during C interoperability. For details, see the description of the inout
parameter in [Cross-Language Interoperability].
Precedence and Associativity of Operators
For an expression that contains two or more operators, its value is determined by the grouping and combination mode of the operators and operands. The grouping and combination mode depends on the precedence and associativity of the operators. To put it simply, the precedence specifies the evaluation sequence of different operators, and the associativity specifies the evaluation sequence of operators with the same precedence.
If an expression contains multiple operators with different precedence, its subexpressions with the operators of higher precedence is evaluated first, followed by those of lower precedence. If a subexpression contains multiple operators of the same precedence, the sequence depends on the associativity of the operators.
The following table lists the precedence, associativity, feature description, usage, and expression type of each operator. The closer to the top of the table, the higher the precedence of the operator.
Operator | Associativity | Description | Usage | Expression type |
---|---|---|---|---|
@ | Right associative | macro call expression | @expr1 @expr2 | Unit |
. | Left associative | Member access | Name.name | The type of name |
[] | Index access | varName[expr] | The type of the element of varName | |
() | Function call | funcName(expr) | Return type of funcName | |
++ | None | Postfix increment | varName++ | Unit |
-- | Postfix decrement | varName-- | Unit | |
? | Question mark | expr1?.expr2 etc. | Option<T> (T is the type of expr2) | |
! | Right associative | Bitwise Logic NOT | !expr | The type of expr |
- | Unary negative | -expr | ||
** | Right associative | Power | expr1 ** expr2 | The type of expr1 |
* | Left associative | Multiply | expr1 * expr2 | The type of expr1 or expr2 , since expr1 and expr2 have the same type |
/ | Divide | expr1 / expr2 | ||
% | Remainder | expr1 % expr2 | ||
+ | Left associative | Add | expr1 + expr2 | The type of expr1 or expr2 , since expr1 and expr2 have the same type |
- | Subtract | expr1 - expr2 | ||
<< | Left associative | Bitwise left shift | expr1 << expr2 | The type of expr1 , where expr1 and expr2 can have different types |
>> | Bitwise right shift | expr1 >> expr2 | ||
.. | None | Range operator | expr1..expr2:expr3 | Range type |
..= | expr1..=expr2:expr3 | |||
< | None | Less than | expr1 < expr2 | Except the type of 'expr as userType' is Option<userType> , other expressions have Bool type |
<= | Less than or equal | expr1 <= expr2 | ||
> | Greater than | expr1 > expr2 | ||
>= | Greater than or equal | expr1 >= expr2 | ||
is | Type check | expr is type | ||
as | Type cast | expr as userType | ||
== | None | Equal | expr1 == expr2 | Bool |
!= | Not equal | expr1 != expr2 | ||
& | Left associative | Bitwise AND | expr1 & expr2 | The type of expr1 or expr2 , since expr1 and expr2 have the same type |
^ | Left associative | Bitwise XOR | expr1 ^ expr2 | The type of expr1 or expr2 , since expr1 and expr2 have the same type |
` | ` | Left associative | Bitwise OR | `expr1 |
&& | Left associative | Logic AND | expr1 && expr2 | Bool |
` | ` | Left associative | Logic OR | |
?? | Right associative | coalescing | expr1 ?? expr2 | The type of expr2 |
` | >` | Left associative | Pipeline | `expr1 |
~> | Composition | expr1 ~> expr2 | The type of expr1 ~> expr2 is the type of the lambda expression {x=>expr2(expr1(x))} | |
= | None | Assignment | leftValue = expr | Unit |
**= | Compound assignment | leftValue **= expr | ||
*= | leftValue *= expr | |||
/= | leftValue /= expr | |||
%= | leftValue %= expr | |||
+= | leftValue += expr | |||
-= | leftValue -= expr | |||
<<= | leftValue <<= expr | |||
>>= | leftValue >>= expr | |||
&= | leftValue &= expr | |||
^= | leftValue ^= expr | |||
` | =` | `leftValue | ||
&&= | leftValue &&= expr | |||
` | =` |
Note: When
?
is used together with.
,()
,{}
, or[]
, it indicates a syntactic suger and evaluation does not strictly follow their inherent precedence and associativity. For details, see [Question Mark Operators].
Evaluation Sequence of Expressions
The evaluation sequence of expressions specifies the sequence in which the values of operands are calculated. Obviously, only expressions containing binary operators have the concept of evaluation sequence. The default evaluation sequence of Cangjie is as follows:
-
For an expression that contains logical AND (
&&
), logical OR (||
), andcoalescing
(??
), the right operand of an operator is evaluated only when it affects the value of the entire expression. Otherwise, only the left operand is evaluated. Therefore, the evaluation sequence of&&
,||
, and??
is as follows: The left operands are evaluated first, followed by the right operands. -
For an optional chaining expression, the
?
operators separate it into several subitems and the subitems are evaluated from left to right in sequence (based on the evaluation sequence of the used operators). -
Other expressions (such as arithmetic expressions, relational expressions, and bitwise expressions) are also evaluated from left to right.