Expressions

From MZXWiki
Revision as of 03:00, 9 August 2013 by Old-Sckool (talk | contribs) (→‎Bitwise Operators)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Expressions in MZX are specially formatted strings that are mathematically and programatically evaluated when the program runs. They have two primary functions: First, expressions allow MZX to perform advanced, inline arithmetic on counters in the same way as most other programming languages. That is, to express a = b + c, instead of having to go through the more cumbersome process of performing each step individually (set "a" to "b"; inc "a" by "c"), this can be done in one simple, easy to read line (set "a" to "('b' + 'c')"). Second, expressions can be used to recursively nest counter interpolations, while ampersand interpolation cannot. That is, a('b('c')'), where c = 3, b3 = 2, and a2 = 1, resolves to 1. By contrast, a&b&c&& is ambiguous and ultimately resolves to a0c&, which is almost certainly not set to anything.

MZX interprets anything in a string between matching parentheses as a potential expression, and attempts to evaluate it. Within the expression string, counter values can be interpolated between pairs of ampersands (&) or single quotes ('). Expressions can also be nested. Strings between parentheses are evaluated in order from innermost to outermost. It is also possible to use ampersands within a quoted counter to perform an interpolation, or to nest an expression within a pair of ampersands. Finally, if the expression is not properly formed and cannot be evaluated, the program behavior falls through and interprets the string as is. Depending on the context this can have different results, but in the case where the ultimate target value is a counter, this will almost certainly result in a non-existent counter lookup and resolve to 0. Pay very close attention to this, as typos in expressions and the resultant silent fallthrough are the source of many MZX bugs.

An In-depth Look At Expressions

Expressions consist of a series of symbols joined by operators. These symbols can be either numeric literals (i.e. 1, 2, 3, 4...), counters (which can be interpolated and nested to any degree), or expressions themselves (e.g. "((1+2)*(3+4))"). As explained before, expressions and counter interpolations are evaluated from innermost to outermost until the entire expression is resolved. It is important to note that expressions ALWAYS resolve to numeric values, which is a crucial difference from the behavior of ampersand interpolation. That is to say, they cannot be used to return the value of strings, ('$string') will not function as expected and will return 0. &$string& must be used instead.

Expressions can accept a number of different operators, which can be divided into three groups. One final note before we dive into them, however: operators in expressions have no concept of precedence. They are always executed in order from left to right, regardless of category or what you learned in math class. With that in mind, be sure to enclose any parts of the expression that require a different order of operations within parentheses.

Mathematical Operators

  • + Addition. Add the first value to the second and return the result.
  • - Subtraction. Subtract the second value from the first and return the result. This is also the unary negative; in the case where there is no left-hand value, this negates the right-hand value. This will take precedence over the preceding operator (if any), and is one of the exceptions to the rule of no precedence.
  • * Multiplication. Multiply the first and second values and return the result.
  • / Division. Divide the first value by the second and return the integer part of the result (MZX has no floating point numbers).
  • % Modulo. Most simply understood as dividing the first value by the second and returning the remainder. However, this is a formal mathematical modulo operation, and works differently from the modulo command originally provided by MZX. "(q%m)" always returns a value between 0 and m-1, inclusive, for positive values of m. For negative values of m, the return is regular but more complicated. q%m = -(-q%-m), except where the result would be 0, in which case it is normalized to m. If this doesn't make sense to you, then don't use a negative modulus, there's very little reason to ever do so.
  • ^ Power. Raise the first value to the power of the second value and return the result.

Comparison Operators

All comparison operators perform a logical comparison on their parameters, and return 0 for false and 1 for true.

  • = Equality. Compare the first value to the second and return true if and only if they are equal. This is NOT an assignment operator, expressions by themselves have no operators that cause side-effects.
  • != Inequality. Compare the first value to the second and return true if and only if they are not equal.
  • < Less-than. Compare the first and second values and return true if and only if the first is less than the second.
  • <= Less-than-or-equal. Compare the first and second values and return true if and only if the first is less than or equal to the second.
  • > Greater-than. Compare the first and second values and return true if and only if the first is greater than the second.
  • >= Greater-than-or-equal. Compare the first and second values and return true if and only if the first is greater than or equal to the second.

Bitwise Operators

MZX expressions have no logical, boolean operators like many other languages do. There is no logical and, or logical not. This is primarily because expressions have no concept of boolean values; all symbols must resolve to integers, and lack of precedence means that all values must be interoperable. As a result, while it is possible to simulate logical operations using 0 to represent false and 1 to represent true (as the comparison operators do), return values for these operators are not limited to those two options.

  • a Bitwise and. AND each respective bit in the first value with each respective bit in the second to obtain a new value, and return the result. (42 a 27) = b101010 & b011011 = b001010 = 10.
  • o Bitwise or. OR each respective bit in the first value with each respective bit in the second to obtain a new value, and return the result. (42 o 27) = b101010 | b011011 = b111011 = 59.
  • x Bitwise xor. XOR each respective bit in the first value with each respective bit in the second to obtain a new value, and return the result. (42 x 27) = b101010 ^ b011011 = b110001 = 49.
  • ~ Bitwise not. NOT (i.e. one's complement) the right-hand value and return the result. This is a unary operator and takes precedence, like the unary negative (two's complement). ~42 = ~b101010 = b...11010101 = -43. Be especially careful with your assumptions about this operator when trying to use it as a logical not. ~0 is NOT 1, but -1. ~1 is NOT 0, but -2. You will have to modify the result of a comparison accordingly if you want to logically negate it. This CAN be done by chaining unary operators like so: -~-(42 < 27) returns 1. However, it is highly recommended that you construct comparisons to avoid this confusing syntax (i.e. simply (42 >= 27) is much clearer).
  • << Left-shift. Bitshift the first value left by the number of bits specified in the second value. This is effectively the same as a*(2^b), but incurs much less computational overhead. (42 << 2) = b101010 << 2 = b10101000 = 168.
  • >> Right-shift. Bitshift the first value right by the number of bits specified in the second value. This is effectively the same as a/(2^b) for all positive numbers, but incurs much less computational overhead. (42 >> 2) = b101010 >> 2 = b1010 = 10.

See the article on bitwise math for more information on how to use these operators.

  • >>> Arithmetic Right-shift. Bitshift the first value right by the number of bits specified in the second value, while preserving the sign bit. This is effectively the same as a/(2^b) for both positive and negative numbers, but incurs much less computational overhead. (42 >> 2) = b101010 >> 2 = b1010 = 10.

See the article on bitwise math for more information on how to use these operators.

Advanced Applications

Advanced users of MZX often employ expressions to circumvent the limitations and clunkiness of robotic. We've already shown, for instance, how expressions can be used to condense a long string of mathematical operations (inc, dec, multiply, divide, etc.) into a single line (e.g. set "a" to "('b'+'c'/('d'-'f'))"). Here are some more complicated tricks.

Multiple Conditions in a Single If

If support has always been very limited in MZX. Not only does the lack of codeblocks require each if statement to branch to another label, but the fact that only one condition can be evaluated per command forces a complicated spaghetti structure of gotos and labels when more than one condition must be checked simultaneously. At least, it did before expressions. Now, through the use of expressions, a series of comparisons can evaluated as a block, with boolean logic applied, all in a single statement. For example, testing whether the player is within a certain defined rectangle on the board can now be done in a single statement:

if "(('playerx'>='zone_min_x')a('playerx'<='zone_max_x')a('playery'>='zone_min_y')a('playery'<='zone_max_y'))" = 1 then "in_zone"

The external "= 1" is the raw robotic conditional test for whether the expression evaluates to true or not. We can use bitwise operators on comparisons as if they were logical operators, because the values of 0 and 1 differ in their representation by only a single bit, and all operations will return acceptable, expected values as if they were true booleans.

Ad-hoc If-Else Statements

Very often in more fully featured languages, there are cases where a variable is set to one value if a condition evaluates one way, and another value if not. For example:

if (x > 0) {
  y = 42;
} else {
  y = 27;
}

MZX possesses no conceptually easy way to perform this simple control-flow operation. Historically this was managed with a construct like this:

if "x" > 0 then "S"
set "y" to 27
goto "S2"
: "S"
set "y" to 42
: "S2"
...

This is clunky, and pollutes the label namespace, as new labels must be created for each such situation in the robot. Expressions provide an alternative solution, which while not any less clunky (in fact it is significantly more convoluted and difficult to understand), does not require any labels and fits on a single line. It works like this:

set "y" to "( (('x' > 0) * 42) + (('x' <= 0) * 27) )"

Or, as it is often stripped of any unnecessary spacing and clarifying parentheses:

set "y" to "(('x'>0*42)+('x'<=0*27))"

This works because the comparison operators return integers, not booleans, and in particular return 0 or 1. They can therefore be inserted into a mathematical expression to cause certain values to become 0 and other values to remain, depending on the condition. Here, when x > 0, the expression evaluates to ((1*42) + (0*27)). When x <= 0, the expression becomes ((0*42) + (1*27)). This principle can be extended to incorporate as many cases as will fit in a single line. We can also take advantage of the zero return to abbreviate the expression in cases where zero is the desired result. For example:

set "x" to "('x'-5>0*('x'-5))"

This will decrease x by 5, but not past 0. If the value would be less than zero, or if x is already less than zero, then it will become zero.

Important Notes

  • All expressions return integer values. If you try to interpolate a string value within an expression, MZX will attempt to numerically evaluate that string. If the string represents a number (e.g. set "$string" to "12345"), then the result will be that number, but usually the result will simply be 0. You CAN interpolate a string as part of a counter name, such as set "get" to "('&$anarray&_&anindex&')", but the expression has to evaluate to a number.
  • Double check your expressions to make sure that all parentheses and quotes have matching pairs. If they are mismatched, the expression will almost certainly fail to evaluate, and will be treated as a raw string. This usually results in the entire string evaluating as 0, and causes code to simply not work as expected, but in subtle and hard to track ways. Generally, a value that is supposed to be changing will not change. MZX gives you no help in syntax highlighting, since expressions are just grafted on to strings. So, mistyped expressions should be one of the first things you check for in non-working code.
  • When an expression is used in a parameter that would usually accept a counter or value, and the entire string provided for that parameter is an expression, then the result of the evaluated expression will be treated as a literal value, not as a counter name, even though it is contained inside of a string. Or as an example, if the counter named "four" is set to 4, then set "ctr" to "('four')" will be the same as set "ctr" to 4, and NOT set "ctr" to "4". This should seem like common sense, because it generally is the natural expectation when you want to evaluate some complicated expression and assign it to a counter. It is noteworthy because it is another difference in the behavior of expressions versus ampersand interpolation. set "ctr" to "&four&" WILL try to set counter to some counter named "4", which probably doesn't exist. Of course, the correct way to do something like the previous example is to not use expressions or ampersands at all, but it is simply intended to demonstrate a key difference in expression behavior.
  • Mathematical counters like sin#, abs#, and sqrt# can be used freely in expressions, but remember that they are counters and not operators, and must be properly enclosed in quotes to function.