Skip to content
David Loscutoff edited this page Oct 22, 2022 · 2 revisions

Expressions and operators are at the core of programming in Pip. The operators in Pip are designed to work much like the operators in other programming languages, with a few quirks. Here's an overview.

Properties of operators

Each operator has three important properties: precedence, arity, and associativity.

Precedence

We've already seen how operator precedence works: higher-precedence operators are applied before lower-precedence operators in an expression. The main difference between Pip and other languages is the sheer number of operators available in Pip. For example, the C++ and Python operator precedence charts each have fewer than 20 precedence levels, but the Pip precedence chart has over 40. That sounds like a lot (and you'll want to keep the precedence chart handy—I still have to check it somewhat often), but the upside is that the precedence levels are designed to be as sensible and intuitive as possible. For example, all string operators are lower precedence than all arithmetic operators, as we saw in last chapter's example program.

Arity

Arity refers to how many arguments an operator takes. A unary operator takes one argument, a binary operator takes two, and a ternary operator takes three. We've already seen examples of unary and binary operators: the length operator # is unary and is placed to the left of its argument (#a); the multiplication operator * is binary and is placed between its two arguments (a*b).

Ternary operators will be familiar to users of C and its derivatives, which have the ?: conditional operator. Pip has a similar operator, ?, the difference being that no separator is used between the second and third arguments: a?bc is the Pip equivalent of a?b:c in a C-like language. Pip also has several other ternary operators; a common one is the replace operator R:

aR" ""..."

This program replaces every space in the input with three dots. Notice how R has three operands: a, " ", and "...". (ATO)

One more important point about arity: some operators have two different arities—either a unary and a binary version, or a unary and a ternary version. Take -, for example. Its binary version is the subtraction operator: 5-3 gives 2. But it also has a unary version, the negation operator: -3 gives, unsurprisingly, negative 3. The same concept extends to many other operators. Ternary R is replace, but unary R is reverse:

R"Hello, World!"

(ATO)

Frequently, though not always, the two arities of an operator do related things. For example, the binary operator TB converts its left argument to the base given by its right argument:

60TB16

converts 60 to hexadecimal, resulting in "3C". (ATO) The unary version of TB also performs base conversion, specifically to base 2:

TB60

converts 60 to its binary equivalent 111100. (ATO)

Associativity

Associativity, for binary and ternary operators, functions as a tiebreaker between operators at the same precedence level. For example, the binary operators + and - have the same precedence, so should an expression like 10-3+1 evaluate as (10-3)+1 or 10-(3+1)? Since these operators are left-associative, the leftmost operator in the expression takes precedence, so the correct result is (10-3)+1 or 8. (ATO)

Some operators, like the exponentiation operator E, are right-associative: 4E3E2 evaluates as 4E(3E2) (i.e. 49 or 262144), not (4E3)E2 (i.e. 642 or 4096). (ATO)

Comparison operators in Pip are neither left-associative nor right-associative. They have a special type of associativity called chaining associativity, inspired by Python's comparison operators:

0<a<=5

is true if and only if both 0<a and a<=5 are true. Try changing the argument to different values and see how the output changes. (ATO)

The Scalar type

Pip has several data types, but so far we have only seen one, the Scalar type.

Note: By convention, the names of Pip data types are capitalized.

Scalars encompass both strings and numbers. This feature, borrowed from Perl and PHP, makes it easy to perform string operations on numbers and numeric operations on strings. We've already concatenated numbers and strings together in last chapter's example program. Other string operations also work on numbers; for instance, this program:

aR0 8

replaces all zeros in the input with eights. (ATO)

In fact, the string "123" and the number 123 are completely interchangeable. Internally, each Scalar is stored as a string, which is converted to a number if it is the argument of an operator that expects a number. The details of the conversion are a bit involved, but the simple version is that any string that starts with a number is treated as that number in a numeric context. For example:

"123abc"*2

treats the string as if it were 123, returning 246. (ATO)

If the beginning of a string cannot be interpreted as a number, the string is treated as 0 in numeric contexts:

"abc"*2

returns 0. (ATO)

Scalars as truth values

Scalars are also used as truth values in Pip. Any operator that returns a truth value (comparison operators, for example) returns 1 for true and 0 for false.

On the other hand, any operator that takes a truth value as one of its arguments (the conditional operator ?, for example) can take any value, not just 0 and 1. In such boolean contexts, 0 and the empty string "" are treated as false; all nonzero, nonempty Scalars are treated as true. Try some different arguments: ATO

Numeric literals

Numeric literals come in two kinds: integer and floating point. An integer literal is simply any run of digits. A floating point literal is a run of digits, a . decimal point, and another run of digits. There must be digits both before and after the decimal point: 0.3, not .3.

A numeric literal with extra leading zeros, like 007, is perfectly valid. In a numeric context, it behaves like the number 7; in a string context, it behaves like the string "007". Pip does not have octal or hex literals.

A number like -3 is not a numeric literal; it is the unary negation operator - applied to the numeric literal 3.

String literals

A string literal starts with " and ends with ". Anything between the quotation marks is part of the string. That's it: no escape sequences, no interpolation, no problem including literal newlines or nonprinting characters or whatever. (ATO) The only character you can't put in a string literal is ". (There is another type of string literal that can include ", but we'll get to those later.)

To create a single-character string, you can instead use a character literal, which consists of ' followed by any one character. Again, there are no escape sequences, and literally any character is allowed, including " and '. (ATO)

Common Scalar operators

Here are some commonly encountered operators when working with Scalars. They are given in increasing order of precedence: that is, lowest-precedence first.

Logic

Operator Arity Associativity Description
? 3 Right Conditional operator (if-then-else)
| 2 Left Logical OR
& 2 Left Logical AND
! 1 n/a Logical NOT

Comparison

Operator Arity Associativity Description
< > = <= >= != LT GT Q LE GE NE 2 Chaining Numeric and string comparison

There are two sets of comparison operators. The symbolic operators perform numeric comparison, treating both operands as numbers and comparing their values. The alphabetical operators perform string comparison, treating both operands as strings and comparing them lexicographically. Some examples:

  • "abc"="xyz" is true because both strings are treated as 0 in a numeric context (ATO), while "abc"Q"xyz" is false because the two strings are not the same (ATO).
  • 123<45 is false because the number 123 is larger than the number 45 (ATO), while 123LT45 is true because a string starting with the character 1 is lexicographically earlier than a string starting with the character 4 (ATO).

String

Operator Arity Associativity Description
N NI 2 Left Count occurrences of substring in larger string (N); true if substring is not in larger string (NI)
H S 2 Left Get first N (H = "Head") or last N (S = "Suffix") characters of string
H S R 1 n/a Get all but last (H) or all but first (S) character of string; reverse
R 3 Left Replace
. 2 Left Concatenate
X 2 Left Repeat string

Arithmetic

Operator Arity Associativity Description
TB 2 Left Convert to base
TB 1 n/a Convert to binary
+ - 2 Left Addition; subtraction
* / % // 2 Left Multiplication; division; mod; integer division
- / % 1 n/a Negation; reciprocal; parity (mod 2)
E RT 2 Right Exponentiation; nth root
E RT SQ 1 n/a Power of 2; square root; square

Other high-precedence operators

Operator Arity Associativity Description
FB 2 Left Convert from base
# A C FB 1 n/a Length; to charcode (A = "ASCII"); from charcode (C = "chr"); convert from binary
@ @< @> 2 Left Character at index; slice left of index; slice right of index
@ 1 n/a Character at index 0 (first character)

Pip uses 0-based indexing. Out of bounds indices are converted to valid indices by taking them modulo the length of the string; for example, "abc"@4 and "abc"@-8 give the same result as "abc"@1. (ATO)

Some notes about parsing

You've noticed by now that most of our Pip programs don't have spaces in them. Pip's parser can tell that 60TB16 is a numeric literal 60, an operator TB, and another numeric literal 16. However, we did need a space in the program aR0 8, because without a space 08 would be treated as a single numeric literal, not two separate numbers 0 and 8.

Lowercase letters in Pip serve as variables (more on that next chapter). Two lowercase letters next to each other parse as separate variables.

With uppercase letters, it's more complicated. Each uppercase letter by itself has meaning (most of them are operators), but uppercase letters can also have meaning in pairs (also mostly operators). How does the parser know whether you meant SQ or S followed by Q? The rule is that any run of uppercase letters is broken up into pairs, and if there's an odd letter out, it's the first one. So SQFB is SQ FB, while ESQFB is E SQ FB. If you need to have two single-letter operators in a row (like X E), or a two-letter operator followed by a single-letter operator (like TB E), you'll have to separate them with a space.

Symbolic operators don't need to be separated with spaces unless there's an ambiguity. This can happen sometimes, but it's fairly rare. For example, if you wanted to divide something by the reciprocal of something else, you'd have to write a/ /b to avoid ambiguity with the integer division operator //... but there shouldn't be any reason to do that, since it's mathematically the same as just multiplying a*b.

Example challenge

Let's tackle another challenge: Is the binary representation of the input number a palindrome?

Challenge description

Given a nonnegative integer n, output YES if its binary representation is a palindrome, or NO if it is not.

Input Binary Output
0 0 YES
1 1 YES
2 10 NO
3 11 YES
4 100 NO
5 101 YES

Solving the challenge

Let's build up to a solution in stages. First, we'll need to convert the input number to binary. Unary TB does exactly that:

TBa

(ATO)

Next, we want to check whether the result is a palindrome. First, we reverse it:

RTBa

Remember that the sequence RTB parses as R TB because the odd letter out comes at the beginning. (ATO)

Then we test whether the reversed binary representation is equal to the binary representation. Since the result of TB only contains the digits 0 and 1, we can use numeric comparison:

RTBa=TBa

(ATO)

The comparison results in a 0 or 1, but we want a string NO or YES, so we use the ternary conditional operator:

RTBa=TBa?"YES""NO"

(ATO)