idk-lang 3

Forms

It's important to note that in idk-lang, every syntactically valid construct is known as a form. There are two types of forms, there are statements and there are expressions. Statements do not return a value. Expressions return a value.

Assignment & Blocks

Assignment comes in two flavors: a single-line assignment statement and a multiline assignment statement. Single-line assignment statements are (unsurprisingly) assignment occuring entirely on a single line. Multiline assignment statements must be started by a newline, and indented some amount of spaces to form a block. The last line of the block must be an expression and that value is what will be used for the assignment. Here are two valid assignment statements

x: 1          # `x` will be assigned `1`

y_plus_z:     # `y_plus_z` will be assigned `3`
  y: 1        # `y` will be assigned `1`
  z: 2        # `z` will be assigned `2`
  y + z       # `y + z` returns `3`

Assignment is a statement, and thus cannot be used as a value. For example, 1 + (x: 1) would be a syntax error because x: 1 does not return a value.

Also, in an attempt to have unified syntax, wherever a colon : is syntactically valid, it can be followed by a single-line form or a multiline list of forms. A multiline list of forms is known as a block and must be indented with some amount of tabs or spaces (but not both!).

Expression Forms

The following forms (if, when, and match) are all expressions. Therefore, they can be used on the right-hand side of a single-line assignment and as the expression in the last line of the block of an assignment.

If Expression

The general syntax for if expressions look like this, (very similar to Python's syntax)

if a_boolean:
  return_value_if_true
else:
  return_value_if_false

Remember, since if is an expression, it returns a value. The last value in the if or else block is what is returned. For example,

if x >= 0:
  x
else:
  negative_x: -x
  negative_x

This expression (rather verbosely) evaluates to the absolute value of x.

When Expression

when expressions are a generalized version of if expressions. They can take a list of boolean expressions and code to execute. It's easier to explain with an example,

when:
  x = y: "equal"
  abs(x - y) < 1: "close"
  else: "far"

This mostly reads like english: When x and y are equal: return "equal". When the absolute value of their difference is less than one: return "close". Otherwise: return "far".

Notice, the first true boolean that is ecountered is the one whose branch will be executed. For example,

when:
  True: "which"
  True: "branch"
  True: "will"
  True: "execute"
  True: "first?"

will return "which".

Parameterized When Expression

when expressions are more than just multiple if expressions, they can also pass paremeters to functions for a more expressive piece of code. For example,

when x, y:
  (<): "less than"
  (>): "greater than"
  (=): "equal"

Here, x and y are passed to the functions in each branch, until one evaluates to True. If a when expression is missing an else branch, the compiler will complain if it can't figure out that the when expression is exhaustive. That is, if the compiler can't tell that at least one of the branches of the when expression will return True, it will raise a compilation error.

Parameterized when expressions can take any number of parameters. Here's one with one parameter using operator sections that evaluates to the sign of x

when x:
  (< 0): -1
  (= 0): 0
  (> 0): 1

Match Expressions

The last expression form is a match expression. It's similar to the pattern matching facilities in other languages, but maybe slightly more advanced. A match expression is made up of a value and several patterns to test the value against. Here's an example match statement that computes the boolean expression x or y,

match x, y:
  (_, True): True
  (True, _): True
  else: False

Here, the match expression tests the tuple x, y against possible patterns. The underscore _, or any other variable name for that matter, matches against any object. Thus, the pattern (_, True) matches against all 2-tuples that have their second element as True.

match expressions also have disjunctive patterns meaning that multiple patterns can be matched in one branch. For example, this expression returns True is one of the elements is 0,

match x, y:
  (_, 0) or (0, _): True
  else: False

match expressions also have if and when guards. These are useful for finer grained logic against matched values. For example, let's say we have a tuple person with type (String, Int) representing the name and age of an individual. Then we can have the following match expression.

match person:
  (name, age) if age < 20: f"{name} is a teenager"
  (name, age) if age < 75: f"{name} is an adult"
  (name, age): f"{name} is a senior"

As you can hopefully see, if guards are pretty useful. However, as you can also hopefully see, multiple if guards begin to get pretty verbose, even if they are doing conditionals on the same portion of the pattern. There has to be a better way! In the same way that you can use when expressions as multiple if expressions, you can use when guards as multiple if guards! Here's a cleaner version of the code above,

match person:
  (name, age) when age:
    (< 20): f"{name} is a teenager"
    (< 75): f"{name} is an adult"
    else: f"{name} is a senior"

Note: the when guards need not be exhaustive. If a when guard fails to find a branch that evaluates to True, the entire pattern matching branch fails and moves onto the next branch. Because of this, the else clause in the example above could be on either the when guard block or as the final clause in the match expression.

The last feature of match expressions is being able to have multiple instances of the same variable in one pattern. Here's an example of a match expression that evaluates to True if the 2-tuple tup contains two elements that are equal to eachother,

match tup:
  (x, x): True
  else: False

This is exactly equivalent to

match tup:
  (x, y) if x = y: True
  else: False

Thus the additional constraints on tup having two elements that are of the same type and that inherit from Equatable will apply.

The next section is on loops and lambdas.