Assignment U4#

Applies to:COMP1110
Released by:Monday, 21 April 2025, 09:00
Base assignment deadline:Friday, 02 May 2025, 15:00
Code Walk registration deadline:Friday, 02 May 2025, 18:00
Code Walk registration link:CWAC
Code Walks:
Thursday, 08 May 2025
Friday, 09 May 2025
GitLab template repository:(link)
Minimum Library Version:2025S1-7
Last Updated:Saturday, 26 April 2025, 13:30

This assignment uses regular Java 23, and JUnit Jupiter 5.9.0. You may optionally import the Functional Java Standard Libraries. Do not import other libraries into your project. Your code may not:

  • Explicitly throw exceptions, or annotate methods with a throws clause, except where explicitly permitted
  • Catch exceptions
  • Use concurrency (if you do not know what that is, you will not be using it so long as you write the code on your own).
  • Use reflection (if you do not know what that is, you will not be using it so long as you write the code on your own).
  • Interact with the environment
  • Write to or read from files

In everything you do, you must follow the Design Recipe. The minor adjustments to the Design Recipe for class definitions are available in Slides of Workshop 6B To see roughly how this assignment will be marked, check out the Skeleton Rubric.

You still need to follow the Design Recipe and write appropriate documentation and tests. In particular, the (minor) adjustments to the Design Recipe for class definitions were covered in workshop 6B. Do not write code that is overly complicated.

Formalities#

This assignment is submitted via GitLab.

Access to Your Repository#

In order for us to be able to collect and mark it, you need to satisfy the following conditions:

  • Your repository’s name must be comp1110-2025s1-u4 (exactly), placed directly in your user namespace (the default). That is, if your uid is u1234567, the address of your GitLab project should read as https://gitlab.cecs.anu.edu.au/u1234567/comp1110-2025s1-u4.
  • Your repository must have our marker bot (comp1110-2025-s1-marker) added as a Maintainer.

You can achieve both these points by creating your project through forking our template repository and not changing the name, or by just using the correct name when creating the project and then manually adding the bot.

Notebooks and Push Requirement#

You need to keep a notebook file as explained on the Git and Notebooks page. You need to commit and push the current state of your files:

  • at the end of every session where you work on the assignment
  • at least once per hour (our checks implement this that no two pushes during sessions recorded in the notebook are more than 70 minutes apart).
  • at least once per major part of the assignment (i.e. for this assignment, at least four times). You are absolutely free to commit and/or push more often than that.

You need to write informative commit messages, and informative notes for each session in your notebook, which describe what it is that you did for this particular commit or session, respectively.

Statement of Originality#

Your submission (and therefore your git repository) needs to include a signed statement of originality. This must be a text file named soo.txt. That file must contain the following text, with the placeholders [your name here] and [your UID here] replaced with your name and UID (remove the brackets!), respectively:

I declare that this work upholds the principles of academic
integrity, as defined in the University Academic Misconduct
Rule; is entirely my own work; is produced for the purposes
of this assessment task and has not been submitted for
assessment in any other context, except where authorised in
writing by the course convener; gives appropriate
acknowledgement of the ideas, scholarship and intellectual
property of others insofar as these have been used; in no part
involves copying, cheating, collusion, fabrication, plagiarism
or recycling.

I declare that I have not used any form of generative AI in
preparing this work.

I declare that I have the rights to use and submit any assets
included in this work.

[your name here]
[your UID here]

Files Summary#

Your repository must be accessible to us and must have the following structure:

  • notebook.yml - your Notebook
  • soo.txt - your signed Statement of Originality
  • src - a folder containing your source files:
    • Expression.java - containing the Expression interface as given here
    • EvaluationIterator.java - containing the EvaluationIterator interface as given here
    • Any Java source files you need to fully implement the above interfaces
  • tests - a folder containing your test files:
    • ExpressionTests.java - containing higher-level tests for Expression/Iterator functionality
    • test files for your other classes

Project Configuration and Running Programs#

We recommend that you use IntelliJ IDEA for this assignment. However, you do not have to. If you do, the standard settings that result from following the IntelliJ Project Setup Guide should make your project work with our testing environment.

If you do not want to (or cannot) use IntelliJ IDEA, or if you want to make sure you can run your program like our automated testing framework, then you can follow the guide available here. This will allow you to trigger your tests directly from the console, in a similar way we did in the first half of the course with the functional Java library support for tests.

Info: Java Standard Libraries#

Part of this assignment asks you to do things for which there are operations in the Java Standard Libraries. Consider particularly looking at the following pages:

Tasks#

In this assignment, we revisit the expressions from Assignment U2, but this time with interfaces, classes, generics, and iterators.

This will result in a fair number of classes for you to implement. Reduce your overall workload by recognizing opportunities for abstraction, while respecting the Liskov Substitution Principle. The rubric will contain deductions for missed opportunities for abstraction, and severe deductions for violating the Liskov Substitution Principle.

In general, we recommend that you proceed by writing individual classes right away, and once you recognize that you are doing something very similar to what you have done in another class previously, introduce abstraction at that point. Don’t repeat yourself!

Expressions#

This assignment is based on implementing two interfaces (one is only needed for distinction-level content), which you can also find in the template repository. You may not change the given interfaces, except where explicitly instructed to do so, or to provide default method implementations.

The main interface is the Expression interface:

import java.util.List;
import java.util.Map;

/**
 * Represents expressions that can be evaluated to a value
 * of type T, potentially via several steps, whose intermediate
 * results are captured by an iterator.
 * @apiNote You MAY NOT add additional members to this interface,
 *   except where explicitly instructed to do so.
 *   We may test your class implementations of the interface with
 *   our own class implementations of the interface. In order to
 *   have a proper integration of your classes with our classes, 
 *   the interface provided here should remain unchanged. 
 *   You MAY (but do not have to) provide default implementations
 *   for any or all of the interface's instance methods.
 * @param <T> The type of the value that evaluating this expression
 *           produces
 */
public interface Expression<T> 
  extends Iterable<Expression<? extends T>> {

    /**
     * Evaluates the expression given a dictionary of variable
     * assignments. Evaluation may encounter an error, if
     * a variable in the expression has gotten no assignment
     * by the time it is evaluated, or if the assigned value
     * does not have the type that is expected.
     * In this case, the evaluation aborts and returns null.
     * Effects: evaluating the expression may add new variable
     *          assignments and change existing ones in the
     *          given map
     * @param variableAssignments a dictionary from variable
     *                            names to values, representing
     *                            current variable assignments.
     *                            As variables may now have
     *                            different types (at least
     *                            Integer and Boolean),
     *                            the type of values is Object.
     * @return the final result of evaluating the expression,
     *         null if evaluation encountered an error
     */
    T evaluate(Map<String, Object> variableAssignments);

    /**
     * Creates a new EvaluationIterator with no
     * existing variable assignments
     * @return the new EvaluationIterator
     * @apiNote DISTINCTION-LEVEL task
     */
    EvaluationIterator<T> iterator();

    /**
     * Creates a new EvaluationIterator with
     * the given existing variable assignments
     * @param variableAssignments a map from variable
     *                            names to values
     * @return the new EvaluationIterator
     * @apiNote DISTINCTION-LEVEL task
     */
    EvaluationIterator<T> iterator(
      Map<String, Object> variableAssignments);

    // TESTING INTERFACE:
    // if code for your methods below is only of the form
    // return new [ClassName][TypeArguments-where appropriate]
    //    ([arguments]...);
    // and you do not use those methods in your own code or tests,
    // then you do not have to provide examples, a design strategy,
    // or tests

    /**
     * Creates a new expression representing a constant integer value
     * @param x The constant integer value
     * @return An expression representing x
     */
    static Expression<Integer> makeIntegerConstant(int x) {
        return null;
    }

    /**
     * Creates a new expression representing a constant boolean value
     * @param x The constant boolean value
     * @return An expression representing x
     */
    static Expression<Boolean> makeBooleanConstant(boolean x) {
        return null;
    }

    /**
     * Creates a new expression representing an erroneous computation
     * of some given type
     * @param <T> the type for which the error expression fails
     *            to produce a value
     * @return An error expression that will always evaluate to null
     */
    static <T> Expression<T> makeErrorExpression() {
        return null;
    }

    /**
     * Creates a new addition expression
     * @param left an expression producing the left operand
     * @param right an expression producing the right operand
     * @return an expression representing the addition of the
     *         two given operands
     */
    static Expression<Integer> makeAdditionExpression(
      Expression<Integer> left,
      Expression<Integer> right) {
        return null;
    }

    /**
     * Creates a new subtraction expression
     * @param left an expression producing the left operand
     * @param right an expression producing the right operand
     * @return an expression representing the subtraction of the
     *         second operand from the first
     */
    static Expression<Integer> makeSubtractionExpression(
      Expression<Integer> left,
      Expression<Integer> right) {
        return null;
    }

    /**
     * Creates a new multiplication expression
     * @param left an expression producing the left operand
     * @param right an expression producing the right operand
     * @return an expression representing the multiplication of the
     *         two given operands
     */
    static Expression<Integer> makeMultiplicationExpression(
      Expression<Integer> left,
      Expression<Integer> right) {
        return null;
    }

    /**
     * Creates a new integer negation expression
     * @param expr an expression producing an integer
     * @return an expression representing the negated result
     *         of the inner expression
     */
    static Expression<Integer> makeNegationExpression(
      Expression<Integer> expr) {
        return null;
    }

    /**
     * Creates a new equality expression on integers
     * @param left an expression producing the left operand
     * @param right an expression producing the right operand
     * @return an expression representing the equality of the
     *         two given operands
     */
    static Expression<Boolean> makeIntegerEqualsExpression(
      Expression<Integer> left,
      Expression<Integer> right) {
        return null;
    }

    /**
     * Creates a new integer less-than expression
     * @param left an expression producing the left operand
     * @param right an expression producing the right operand
     * @return an expression representing the less-than
     *         comparison between the two operands
     */
    static Expression<Boolean> makeIntegerLessThan(
      Expression<Integer> left, 
      Expression<Integer> right) {
        return null;
    }

    /**
     * Creates a new equality expression on booleans
     * @param left an expression producing the left operand
     * @param right an expression producing the right operand
     * @return an expression representing the equality of the
     *         two given operands
     */
    static Expression<Boolean> makeBooleanEqualsExpression(
      Expression<Boolean> left, 
      Expression<Boolean> right) {
        return null;
    }

    /**
     * Creates a new and expression
     * @param left an expression producing the left operand
     * @param right an expression producing the right operand
     * @return an expression representing the conjunction of the
     *         two given operands
     */
    static Expression<Boolean> makeAndExpression(
      Expression<Boolean> left, 
      Expression<Boolean> right) {
        return null;
    }

    /**
     * Creates a new or expression
     * @param left an expression producing the left operand
     * @param right an expression producing the right operand
     * @return an expression representing the disjunction of the
     *         two given operands
     */
    static Expression<Boolean> makeOrExpression(
      Expression<Boolean> left, 
      Expression<Boolean> right) {
        return null;
    }

    /**
     * Creates a new boolean negation expression
     * @param expr an expression producing a boolean
     * @return an expression representing the negated result
     *         of the inner expression
     */
    static Expression<Boolean> makeNotExpression(
      Expression<Boolean> expr) {
        return null;
    }

    /**
     * Creates a new if-then-else expression
     * @param condition A boolean condition expression determining
     *                  the branch to be evaluated
     * @param thenExpr An expression to be evaluated if the
     *                 condition evaluates to true
     * @param elseExpr An expression to be evaluated if the
     *      *          condition evaluates to false
     * @return a new if-then-else expression
     * @param <T> the type of the branch expressions and thus
     *           of the ultimate result of the expression
     */
    static <T> Expression<T> makeIfThenElseExpression(
      Expression<Boolean> condition, 
      Expression<T> thenExpr, 
      Expression<T> elseExpr) {
        return null;
    }

    /**
     * Creates a new sequencing expression, which
     * has at least one final expression, whose
     * type argument must match the sequence
     * expression's type argument
     * @param exprs A list of other expressions
     *              to be evaluated in sequence before
     *              the final expression. Since their
     *              results are going to be ignored,
     *              the type argument is the unbounded 
     *              wildcard (i.e. ?)
     * @param finalExpr the final expression in
     *                  the sequence, whose result
     *                  will be the result of the
     *                  sequence expression
     *
     * @return a new sequence expression
     * @param <T> The type of the ultimate result
     *           of evaluating the expression
     */
    static <T> Expression<T> makeSequenceExpression(
      List<Expression<?>> exprs,
      Expression<T> finalExpr) {
        return null;
    }

    /**
     * Creates a new integer variable expression
     * @param varName the name of the variable
     * @return a new integer variable expression
     */
    static Expression<Integer> makeIntegerVariableExpression(
      String varName) {
        return null;
    }

    /**
     * Creates a new boolean variable expression
     * @param varName the name of the variable
     * @return a new boolean variable expression
     */
    static Expression<Boolean> makeBooleanVariableExpression(
      String varName) {
        return null;
    }

    /**
     * Creates a new assignment expression
     * @param varName the name of the variable to be assigned
     * @param expr the expression whose value should be assigned
     *             to the variable
     * @return a new assignment expression
     * @param <T> the type of the variable and thus of the
     *           overall expression
     */
    static <T> Expression<T> makeAssignmentExpression(
      String varName, 
      Expression<T> expr) {
        return null;
    }

}

This interface covers both the integer and boolean expressions from Assignment 2. We instead use a generic type parameter to distinguish between expressions of different types, so a boolean expression has type Expression<Boolean>, and an integer expression has type Expression<Integer>.

In contrast to Assignment 2, there can now also be boolean variables, and boolean expressions can form the branches of an if-expression, as well be part of assignment expressions and sequencing expressions.

The other change to assignment 2 is that you may now be given expressions that don’t evaluate correctly. The three possible reasons for errors are:

  • An explicit error expression.
  • A variable has not been assigned at the point where it is evaluated.
  • A variable has been assigned at the point where it is evaluated, but the value has the wrong type. If either of these three scenarios happens, an expression should abort its execution and return null as the value it evaluates to.

This means that in an expression like x + (y = 5), if x has not been assigned a variable, then the result of evaluating x is null, and thus the addition operation should abort its evaluation and return null itself. This implies in particular that the assignment y = 5 never gets evaluated.

The second interface, EvaluationIterator, is described as part of the distinction-level tasks below.

The assignment proceeds in stages, but if you feel comfortable with iterators, you may simply implement iterators and use such implementations to implement evaluate. Otherwise, we recommend that you start by implementing evaluate with good old recursion.

Part 1#

In appropriate Java files in the src folder, with tests in appropriate files in the tests folder

Part 1 is a regular part of the assignment and will be included in marking it. However, it is designed as an on-ramp. Tutors may help you with the code here if you ask for help - though you should still try to do as much as possible yourself.

Implement the relevant classes and corresponding static methods in Expression<T> for:

  • integer constants
  • error expressions
  • addition expressions
  • subtraction expressions
  • multiplication expressions

For each class, implement the evaluate method and override the toString method, such that the expressions are turned into strings as follows:

  • integer constants just become a string representation of their integer, e.g. 5
  • error expressions are rendered as ERROR
  • addition expressions are rendered as ([left] + [right])
  • subtraction expressions are rendered as ([left] - [right])
  • multiplication expressions are rendered as ([left] * [right])

For binary expressions, make sure to include the parentheses and the spaces around the operators (do not put spaces between the parentheses and the operands).

The behavior of evaluate is as follows:

  • integer constants return their integer value
  • error constants return null
  • for compound expressions, evaluation proceeds mostly left-to-right, inside-out. The key change to the behavior from assignment 2 is that if any step in evaluating sub-expressions from left to right returns null, the result of the overall expression is null, and further sub-expressions do not get evaluated at all. If their sub-expressions return non-null results, addition, subtraction, and multiplication expressions apply the corresponding mathematical operator to their results.

The iterator methods may simply return null.

Follow the Design recipe!

Part 2#

In appropriate Java files in the src folder, with tests in appropriate files in the tests folder

Implement the relevant classes and corresponding static methods in Expression for:

  • boolean constants
  • integer negation expressions
  • boolean negation expressions
  • and expressions
  • or expressions
  • integer equality expressions
  • integer less-than expressions
  • boolean equality expressions
  • if-then-else expressions

For each class, implement the evaluate method and override the toString method, such that the expressions are turned into strings as follows:

  • boolean constants just become a string representation of their boolean value, i.e. true or false
  • integer negation expressions are rendered as (- [expr])
  • boolean negation expressions are rendered as (! [expr])
  • and expressions are rendered as ([left] && [right])
  • or expressions are rendered as ([left] || [right])
  • integer equality expressions ([left] = [right])
  • integer less-than expressions ([left] < [right])
  • boolean equality expressions ([left] = [right])
  • if-then-else expressions ([condition] ? [thenExpr] : [elseExpr])

For non-constant expressions, make sure to include the parentheses and the spaces around the operators (do NOT put spaces between the parentheses and the operands, or between the opening parenthesis and the unary operator).

The behavior of evaluate is as follows:

  • boolean constants return their boolean value
  • for compound expressions, evaluation proceeds mostly left-to-right, inside-out. The key change to the behavior from assignment 2 is that if any step in evaluating sub-expressions from left to right returns null, the result of the overall expression is null, and further sub-expressions do not get evaluated at all. If their sub-expressions return non-null results, integer and boolean negation, and and or expressions, integer and boolean equality expressions, and integer less-than expressions apply their respective operators to the results of their sub-expressions
  • if-then-else is an exception from left-to-right, inside-out: it first evaluates the boolean condition expression. If that expression evaluates to null, the whole if-then-else expression returns null. Otherwise, if the result is true, the result of the if-then-else expression becomes whatever is the result of evaluating the then-expression, and if the result is false, the result of the if-then-else expression becomes whatever is the result of evaluating the else-expression. In all cases, at least one of the then-expression or else-expression is not evaluated at all.

The behavior of boolean and- and or-expressions is a bit different from those in U2, where they were supposed to implement “short-circuiting” behavior, only evaluating the second expression if that makes a difference in the (boolean) result. This kind of behavior is in a sense still present when the first expression evaluates to null - this is uniform across all binary expressions. However, if the first expression evaluates to either true or false, the second expression should always be evaluated here.

The iterator methods may simply return null.

Follow the Design recipe!

Part 3#

In appropriate Java files in the src folder, with tests in appropriate files in the tests folder

Implement the relevant classes and corresponding static methods in Expression for:

  • integer variable expressions
  • boolean variable expressions
  • assignment expressions
  • sequencing expressions

For each class, implement the evaluate method and override the toString method, such that the expressions are turned into strings as follows:

  • integer variable expressions are rendered as their variable name, prefixed by i_, e.g. i_x for a variable named x
  • boolean variable expressions are rendered as their variable name, prefixed by b_, e.g. b_x for a variable named x
  • assignment expressions are rendered as ([varName] = [expr]) - in contrast to variables, no prefixing happens, e.g. (x = 5)
  • sequencing expressions are rendered as (e1; e2; ...; eN) for some N > 0. In contrast to the other operators, semicolons ; have no space in front of them.

For non-constant expressions, make sure to include the parentheses and the spaces around the operators (do not put spaces before semicolons ;).

The behavior of evaluate is as follows:

  • integer variable expressions look up their variable in the given map, by the variable name. If the map does not have such a key, the expression evaluates to null. If the map does have the key, we check whether the corresponding value is an instance of Integer. If so, the expression evaluates to that integer value. Otherwise, the expression evaluates to null.
  • boolean variable expressions look up their variable in the given map, by the variable name. If the map does not have such a key, the expression evaluates to null. If the map does have the key, we check whether the corresponding value is an instance of Boolean. If so, the expression evaluates to that boolean value. Otherwise, the expression evaluates to null.
  • assignment expressions and sequencing expressions proceed left-to-right, inside-out. As usual, if some intermediate result is null, the whole expression returns aborts execution and returns null.
    • Where an assignment expression evaluates its inner expression to a non-null value, it updates the given dictionary to associate its variable name with the returned value, and then returns the same value as its result.
    • Sequencing expressions evaluate their expressions in order, applying the relevant effects to the dictionary, but ignoring the non-null result values of all but the last expression, whose return value becomes the return value of the sequencing expression.

The iterator methods may simply return null.

Follow the Design recipe!

Part 4 (DISTINCTION-LEVEL)#

In appropriate Java files in the src folder, with tests in appropriate files in the tests folder

This part is based on the EvaluationIterator interface, defined as follows:

import java.util.Iterator;
import java.util.Map;

/**
 * Represents an iterator through intermediate states of
 * evaluating an expression. The iterator proceeds in
 * steps according to the assignment specification.
 * @apiNote You MAY NOT add additional members to this interface,
 *   except where explicitly instructed to do so.
 *   We may test your class implementations of the interface with
 *   our own class implementations of the interface. In order to
 *   have a proper integration of your classes with our classes, 
 *   the interface provided here should remain unchanged. 
 * @param <T> The type of the value that the expression
 *            should evaluate to.
 */
public interface EvaluationIterator<T>
        extends Iterator<Expression<? extends T>> {
    /**
     * Returns the result of the overall computation,
     * or null if there was an error.
     * If the iterator has not reached the end of
     * the computation yet, this method will
     * fast-forward the iterator to that point,
     * i.e. after calling this method, hasNext
     * should always return false.
     * Effects: in the course of evaluating the expression,
     *          variable assignments may be updated and
     *          new ones may be added
     * @return The final result of the overall computation,
     *         or null if there was an error
     */
    T finalResult();

    /**
     * Returns the current map of variable assignments,
     * based on any changes that may have happened
     * in evaluating part of the expression.
     * @return the current map of variable assignments
     */
    Map<String, Object> currentVariableAssignments();

    /**
     * Produces the next intermediate expression after taking
     * one more step in evaluating the expression.
     * Effects: in the course of evaluating the expression,
     *          variable assignments may be updated and
     *          new ones may be added
     * @return The next intermediate expression after taking
     *         one more step in evaluation
     * @throws java.util.NoSuchElementException may throw
     *         this exception if there is no next element for
     *         whatever reason
     */
    Expression<? extends T> next();
}

The point of the EvaluationIterator is to enable a “show your work” style evaluation process, where the iterator returns intermediate versions of the expression where already-evaluated sub-expressions are replaced with their result. For example, evaluating ((1 + 2) * (5 - 3)) proceeds in the following steps:

  • ((1 + 2) * (5 - 3))
  • (3 * (5 - 3))
  • (3 * 2)
  • 6

As part of this scheme, the iterator’s hasNext method should return true whenever there is another step that can be taken to refine the current state of evaluation towards a result, and next should take that step and return the new intermediate expression. In general, constant expressions have no more steps to take, and hasNext therefore should always return false (the original expression does not count as a step in the iterator). For all other expressions, a “step” is:

  • a step that can be taken in a sub-expression, according to evaluation order
  • a step that corresponds to the main purpose of an expression, once enough sub-expressions have evaluated:
    • for most expressions, that means that all inner expressions have been evaluated and the main operation of the expression can now be applied (this is the case for addition, subtraction, multiplication, both negations, both equality comparisons, the less-than comparison, and assignment)
    • for if-then-else, this means that the condition expression has been fully evaluated, and the expression now steps to one of its branches. For example,
      ((5 = 5) ? (1 + 2) : (3 - 4))
      (true ? (1 + 2) : (3 - 4))
      (1 + 2)
      3
      
    • for sequencing expressions, this occurs whenever the first remaining expressions has finished evaluating, and evaluation should move on to the next expression in the list. The “step” here effectively removes the evaluated expression from the list. For example:
      ((x = 5); (y = 3); (i_x + i_y))
      (5; (y = 3); (i_x + i_y))
      ((y = 3); (i_x + i_y))
      (3; (i_x + i_y))
      (i_x + i_y)
      (5 + i_y)
      (5 + 3)
      8
      

For the above examples, the lines should be the result of running (for given expression expr):

System.out.println(expr);
for(var intermediate : expr) {
   System.out.println(intermediate);
}

Follow the Design recipe!

Part 5 (DISTINCTION-LEVEL)#

In appropriate Java files in the src folder, with tests in appropriate files in the tests folder

Add the following kinds of expressions to your program, and add additional static methods in the Expression interface to create instances of them (figure out the necessary signatures):

  • a compare expression, which works on any combination of two expressions, so long as the type of the left expression implements the Comparable interface in a way that allows it to be compared with the type of the right expression. Returns the integer that is the result of the compareTo method. Is rendered as ([left] <?> [right]), and created with Expression.makeCompareExpression([left], [right]).
  • a number of expressions around lists, from which we only expect to read values (you may adjust your types accordingly):
    • a list constant expression, which represents a value of type List<T> for any T. Rendered using the toString method of List, and created with Expression.makeListConstant([list]).
    • a singleton expression, which takes the result of an expression of an arbitrary type T and produces a List<T> with the result of the inner expression as the single element. Rendered as S[[expr]] (the outer brackets are actually part of the syntax, e.g. S[5] is the singleton list expression of 5). Created with Expression.makeSingletonExpression([expr]).
    • a get-expression, which takes a list-producing expression and an integer expression, and either returns the element at that index in the list, or, if the index is invalid, null. Rendered as ([list] @ [index]). For evaluation order, the list is evaluated before the index value. Created with Expression.makeGetExpression([listExpr], [indexExpr]).
    • a length expression, which takes a list expression and returns the length of its result. Rendered as |[expr]|, and created with Expression.makeLengthExpression([expr]).
    • a concat-expression, which takes two lists and concatenates them. Arbitrary lists may be combined, which may result in a loss of type information, but where two lists of equal types are combined, as much type information as possible should be retained. Rendered as ([left] + [right]), and created with Expression.makeConcatExpression([left], [right]).

You may add and constrain generic type arguments to the new static Expression methods as appropriate.

Follow the Design Recipe!

Updates#

Saturday, 26 April 2025, 13:30Clarified non-short-circuiting behavior of boolean binary expressions
bars search caret-down plus minus arrow-right times