Expressions¶
Function bodies in PIE DSL consist entirely of expressions.
Every expression has a type and returns a value of that type, with the exception of fail
and return
.
Expressions can use brackets to override the default priority rules, for example a && (b || c)
.
Brackets have the following form: ($Exp)
This section describes expressions in the PIE DSL. Expressions can use declared values. These are described in this section of the functions documentation.
Syntactic priorities (disambiguation)¶
Nested expressions can be ambiguous, for example ! true && false
could either ! (true && false) // = true
or (!true) && false // = false
.
To solve these ambiguities, each expression has a priority.
Expressions with higher priories will be nested in expressions with lower priority.
The example above is parsed as (!true) && false
because not has a higher priority than logical and.
All expressions are left-associative, which means that if two expressions with the same priority are ambiguous, the leftmost expression will be nested in the rightmost expression.
For example, 3 - 2 + 1
is equivalent to (3 - 2) + 1
.
The following list lists expressions in order of descending priority. Expressions on the same number have the same priority. If an expression is not listed, it cannot be ambiguous (e.g. Blocks and list literals)
- Not
- Make nullable, Make non-nullable
- Addition
- Compare for (in)equality
- Logical and
- Logical or
- list, walk
- requires, generates
- read, exists
- Function calls, Method calls, Create supplier, Task supplier
- Filters
- Tuple literal, List literal
- List comprehension
- Value declaration
- return, fail
- if-else
- if
Quick overview¶
The following table gives a quick overview of all expressions in the PIE DSL.
name | syntax | example | description | type |
---|---|---|---|---|
Block | {$Exps} |
{val x = 4; x+7} |
A sequence of expressions | The type of the last expression |
Make nullable | $Exp? |
"nullable string"? |
Makes the type T of an expression nullable (T? ) |
When applied to an expression of type T , T? |
Make non-nullable | $Exp! |
input! |
Makes the type T? of an expression non-nullable (T ). Throws an exception if the value is null |
When applied to an expression of type T? , T |
Not | !$Exp |
!flag |
Turns false to true and vice versa |
bool |
Compare for (in)equality | $Exp == $Exp and Exp != $Exp |
result == null , errors != [] |
Compares two values for (in)equality | bool |
Logical or | $Exp || $Exp |
dryRun || input == null |
Is true unless both the values are false |
bool |
Logical and | $Exp && $Exp |
!dryRun && input != null |
Is true iff both values are true |
bool |
Addition | $Exp + $Exp |
x + y |
Adds or concatenates two values | Depends on the types of the expressions |
Value declaration | val $VALID TypeHint? = $Exp |
val num: int = 47 |
Declare a value by name | The type of the declared value |
Value reference | $VALID |
x |
Reference a value or parameter | The type of the referenced value |
if | if ($Exp) $Exp |
if (input == null) fail "Input is null" |
Evaluates the body if the condition evaluates to true |
unit |
if-else | if ($Exp) $Exp else $Exp |
if (name != null) name else default |
Evaluates the first branch if the condition is true , and the second branch otherwise |
The least upper bound of the types of the branches |
List comprehension | [$Exp | $Binder <- $Exp] |
["Key: $key; value: $value" | (key, value) <- pairs] |
Evaluate the expression for each element in a list | A list of the type of the expression |
Function calls | $ModuleList$FUNCID$TypeArgs($Exps) |
stdLib:okResult<string>("Hello world!") |
Call a function | The return type of the function |
Method calls | $Exp.$FUNCID$TypeArgs($Exps) |
file.replaceExtension("pp.pie") |
Call a method | The return type of the method |
Create supplier | supplier$TypeArgs($Exps) |
supplier(47) |
Create a supplier | A supplier of the provided type |
Task supplier | $ModuleList$FUNCID.supplier$TypeArgs($Exps) |
lang:java:parse.supplier(file) |
Create a supplier from a function | A supplier of the return type of the function |
requires | requires $Exp $FilterPart? $StamperPart? |
requires ./metaborg.yaml by hash |
Declare that the current task depends on the provided path | unit |
generates | generates $Exp $StamperPart? |
generates file by hash |
Declare that the current task generates on the provided path | unit |
list | list $Exp $FilterPart? |
list ./examples with extension "pie" |
Lists the direct children of the given directory. Note: for list literals, see further down this table. | A list of paths, i.e. path* |
walk | walk $Exp $FilterPart? |
walk ./examples with extension "pie" |
Recursively get all descendants of the given directory | A list of paths, i.e. path* |
exists | exists $Exp |
exists ./config.json |
Check if a file exists | bool |
read | read $Exp |
read ./config.json |
Returns the file contents as a string, or null if the file does not exist | A nullable string, i.e. string? |
return | return $Exp |
return false |
Returns the provided value from the current function | unit. Note: may get changed to bottom type |
fail | fail $Exp |
fail "input cannot be null" |
Throws an ExecException with the provided string as message | unit. Note: may get changed to bottom type |
Unit literal | unit |
unit |
Literal expression of the only value of the unit type | unit |
true | true |
true |
Literal expression for the boolean value true . |
bool |
false | false |
false |
Literal expression for the boolean value false . |
bool |
int literal | "-"? [0-9]+ |
0 , 23 , -4 |
A literal value of the int type | int |
null | null |
null |
Literal expression for the null value of nullable types. | null type |
Tuple literal | ($Exps) |
(1, "one", "une") |
A literal value of a tuple type | A tuple type of the types of the elements |
List literal | [$Exps] |
(1, 2, 3) |
A literal value of a list type. For the keyword list , see earlier in this table. |
A list of the least upper bound of the types of the elements |
String literal | "$StrParts" |
"Hello $name!" |
A literal value of the string type | string |
Path literal | $PathStart$PathParts |
./src/test/resources |
A literal value of the path type | path |
There is also a section on common lexical elements. This covers filters and stampers.
Block¶
Blocks are expressed as {$Exps}
, where $Exps
is a list of semi-colon separated expressions.
Its type and value are those of the final expressions in the block.
The final expression does not end with a semi-colon.
For example:
{
val name = read ./example/name.txt;
val greeting = read ./example/greeting.txt;
"$greeting $name"
}
Empty blocks (blocks without any expression) are allowed, their type and value is unit
.
Blocks introduce their own scope. Expressions in the block are evaluated in that scope. Values declared in the block are not allowed to shadow values or parameters outside the block. This means that it is not allowed to declare a value with a name that already exists. Values declared before the block are still visible inside the block. Values declared inside the block are not visible after the block.
Make nullable¶
Make nullable is expressed as a question mark after an expression.
As its name suggests, it makes a non-nullable expression nullable.
The value remains unchanged, but the type of the expression is now nullable.
Its syntax is $Exp?
, for example read ./name.txt == "Bob"?
.
It is an error to use this expression on an expression that was nullable already.
Make non-nullable¶
Make non-nullable is expressed as an exclamation mark after an expression.
As its name suggests, it makes a nullable expression non-nullable.
The value remains unchanged, but the type of the expression is now non-nullable.
Its syntax is $Exp!
, for example read file!
.
It is an error to use this expression on an expression that was non-nullable already.
Not¶
Logical negation.
It takes a boolean expression and returns the opposite boolean value.
Its syntax is !$Exp
, for example if (!exists file) fail "$file should exist
.
Compare for (in)equality¶
Compare two expressions for equality or inequality.
Two values are considered equal if they are both null
or if the equals
method in the backing Java class for the first value returns true
when applied to the second value.
Otherwise, they are considered in-equal.
The syntax for equals is $Exp == $Exp
, for example x == null
.
The syntax for in-equals is $Exp != $Exp
, for example x != null
.
Expressions can only be compared if one is a non-strict subtype of the other. This provides protection against accidentally comparing two expressions that can never be equal.
Comparing null
and empty lists
Expressions with nullable types can have equal values despite not being subtypes of each other, if they are both null
.
The same goes for list types with the empty list []
.
In these cases, check for these specific values: x == null && y == null
.
Logical or¶
Logical or.
Takes two boolean expressions and returns true
if either of them is true
and false
if both are false
.
This operator short-circuits if the first expression is true
, in which case the second expression will not be evaluated.
Its syntax is $Exp || $Exp
, for example exists file || default != null
.
logical and¶
Logical and.
Takes two boolean expressions and returns true
if both of them are true
and false
if either of them is false
.
This operator short-circuits if the first expression is false
, in which case the second expression will not be evaluated.
Its syntax is $Exp && $Exp
, for example configFile != null && exists configFile
.
Addition¶
The add operator is an overloaded in the PIE DSL.
Its syntax is $Exp + $Exp
.
The type of adding two values depends on their static types.
Adding two int
s uses mathematical plus: 1 + 2 // result: 3
, and the result is an int
as well.
Adding any value to a string
converts the value to a string
and then concatenates the strings, resulting in a string
: "The value is:" + x
.
String interpolation
It might be clearer to use string interpolation.
Adding a string
or a path
to a relative path
concatenates the values and results in a new path
: projectDir + ./src/test/resources/
Adding a string
or a path
to an absolute path results in a runtime error.
Path interpolation
It might be clearer to use path interpolation.
Finally, adding a type T2
to a list with type T1*
has two cases
- If
T2
is a list as well both lists will be concatenated. The element type ofT2
must be a subtype ofT1
.
Adding a list as an element
To add a list list: T*
as an element to a list of lists lists: T**
, wrap the list in another list: lists + [list]
Empty lists
The PIE DSL keeps track of empty lists statically.
This allows it to give a warning when concatenating an empty list: [1, 2, 3] + []
will give a warning.
- All other cases will append the second item to the first list.
T2
must be a (non-strict) subtype ofT1
. The element type of the resulting list isT1
, unlessT2
is the null type. In that case, the element type of the resulting list is nullable as well.
Value declaration¶
Value declarations declare one or more named values. Expressions that are evaluated afterwards in the same scope or an inner scope can use the declared values by referencing them. For more information on values, see parameters and values.
A basic value declaration declares a name with a value: val x = 9
.
It is possible to give a type hint with the name: val y: int? = 8
.
When a type hint is given, the type of the expression must be assignable to the type of the type hint.
The type of the declared value is the provided type from the type hint, or the type of the expression if there is no type hint.
A value declaration can also do tuple destructuring and assign its values to multiple variables at once: val (name: string, times: int*) = getPerformance(id)
.
Each name in a tuple destructuring can have a type hint.
Tuple destructuring cannot be nested, so the following will not parse: val (a, (b, c)) = (1, (2, 3))
.
Some examples of value declarations
val firstName = "Bob"; // simple value declaration
val age: int = 27; // with type hint
val size: (int, int) = (800, 400); // assign tuple to single value.
val (width, height) = size; // tuple destructuring
// tuple destructuring with type hints
val (name: string, in: path, out: path) = ("expressions", ./in/expressions.pie, ./out/expressions.java);
// tuple destructuring with mixed type hints
val (year, values: (string, bool)*) = (2020, []);
Value declarations have the following syntax: val $Binder = $Exp
, where the binder can be either a single binder $Bind
or tuple binder ($Binds)
, and binds can be only a name $VALID
or a name with a type hint $VALID: Type
.
The type of a value declaration is the type of the variable(s) that it declares. It uses the type hint if available and the expression type otherwise. Single declarations have that single type, tuple declarations return a tuple of all the types that they declare.
Value reference¶
Value references reference a declared value or parameter by name.
The name must be declared beforehand in the current scope or an outer scope.
The type and value of a value reference is the type and value of the referenced value.
The syntax is $VALID
, for example:
func incrementInefficiently(x: int) -> int = {
val y = 1;
val res = x + y; // reference parameter x and value y
res // reference value res
}
if¶
An if expression conditionally evaluates an expression.
It takes two expressions.
The first one is the condition and is of type boolean.
The second one is the body and can have any type.
Its syntax is if ($Exp) $Exp
, for example if (text == null) fail "Could not read $file"
.
If the condition evaluates to true
, the body is evaluated, otherwise not.
The type of an if
expression is the unit
type.
The condition and body are evaluated in their own scope, so value declarations in an if
expression are not visible after the if
.
Because an if expression is evaluated in its own scope and always returns the same value, the only use for an if
expression is if the body has side-effects.
if-else¶
Conditionally evaluates one of two expressions.
It takes three expressions.
The first one is the condition and is of type boolean.
The second one is the true-branch and can have any type.
The third one is the false-branch and can also have any type.
Its syntax is if ($Exp) $Exp else $Exp
, for example if (name != null) name else default
.
If the condition evaluates to true
, the true-branch is evaluated, otherwise the false-branch is evaluated.
The type of an if-else expression is the least upper bound of both branches.
The condition and branches are evaluated in their own scope, so value declarations in an if-else expression are not visible after the expression.
Some examples of the least upper bound of different types
val cat1: Cat = getCat(1);
val cat2: Cat = getCat(2);
val mammal: Mammal = getMammal();
val animal: Animal = getAnimal();
val dog: Dog = getDog();
val fish: Fish = getFish();
val animal1: Animal = getAnimal(1);
val animal2: Animal = getAnimal(2);
val catBox1: Box<Cat> = box(cat1);
val catBox2: Box<Cat> = box(cat2);
val anyBox1: Box<_> = box(animal1);
val anyBox2: Box<_> = box(catBox2);
// same type
if (flag) "hello" else "world" // type: string
if (flag) 0 else 10 // type: int
if (flag) cat1 else cat2 // type: Cat
if (flag) animal1 else animal2 // type: Animal
if (flag) catBox1 else catBox2 // type: Box<Cat>
if (flag) anyBox1 else anyBox2 // type: Box<_>
// subtypes
if (flag) cat else mammal // type: Mammal
if (flag) catBox else anyBox // type: Box<_>
// different types
if (flag) cat1 else dog // type: Mammal
if (flag) dog else cat2 // type: Mammal
if (flag) cat2 else fish // type: Animal
if (flag) "hello" else 2 // type: top type
List comprehension¶
List comprehensions apply an expression to every element of a list and return a new list with the new elements.
They are special syntax for mapping a list, which would not ordinarily be possible in the PIE DSL because there are no higher-order functions.
They have the syntax [$Exp | $Binder <- $Exp]
, for example ["Key: $key; value: $value" | (key, value) <- pairs]
The last expression is the input list and must have type T*
for some T
.
The binder defines names for the elements.
It can either be a single binder to bind the complete element, or a tuple binder if the element is a tuple, see value declarations for more explanation
The first expression can use the names defined by the binder.
The type of that expression is some type Q
.
The type of the full list comprehension is a list of the type that was mapped to, i.e. Q*
.
Empty lists
A list comprehension over an empty list simply returns a new empty list. A list comprehension will give a warning if the input list is statically known to be empty. List comprehensions over empty lists are compiled to an immediate empty list of the declared type because it is not known what the element type of the empty list is.
Function calls¶
Function calls invoke a declared function.
They have the syntax $ModuleList$FUNCID$TypeArgs($Exps)
, for example stdLib:okResult<string>("Hello world!")
.
The second element is the function name.
This function name can either be qualified or left unqualified by the module list.
If it is unqualified, the function name must be defined in the current module or be imported with a function import.
If it is qualified, the function is looked up in that module.
The number of type arguments must match the number of type parameters on the function declaration, and the type arguments must be within bounds for the type parameters.
The expressions are the arguments to the function.
They must match the number of parameters that the function declared and they must be subtypes of the parameters.
The type of a call is the type of the declared function, where type parameters are replaced with their corresponding type arguments.
Return type is a generic parameter
func id<T>(param: T) -> T = param
func test() -> unit = {
id<string>("Hello world!"); // type is string
id<int>(42); // type is int
unit
}
Arguments can declare values
Value declarations in the arguments are legal and are visible after the call. Doing this is bad practice in almost all cases. Declare the value before the call instead. For example:
// declares the value `x` with value 9
// also passes 9 as argument to `test`
test(val x = 9);
x // x is visible after the call.
val x = 9; // declare before call
test(x); // refer to declared value
x
Method calls¶
Method calls invoke a declared method.
They have the syntax $Exp.$FUNCID$TypeArgs($Exps)
, for example file.replaceExtension("pp.pie")
.
The first element is an arbitrary expression.
The second element is the method name.
This method is looked up on the type of the first expression.
The number of type arguments must match the number of type parameters on the method declaration, and the type arguments must be within bounds for the type parameters.
The expressions are the arguments to the method.
They must match the number of parameters that the method declared and they must be subtypes of the parameters.
The type of a method call is the type of the declared method, where type parameters are replaced with their corresponding type arguments.
No methods on nullable types
There are no methods defined on nullable types. To access the methods of the inner type, cast the expression to non-nullable first:
val maybe: Result<string, _ : Exception> = null;
maybe.unwrap(); // error: Cannot call method on nullable type
maybe!.unwrap(); // type checks, but will throw a run time exception
// Better: handle null value before casting
if (maybe == null) {
// handle null value here
"Cannot get value, result is null"
} else {
maybe!.unwrap()
}
Return type is a generic parameter
data Box<T> = foreign java org.example.methodCall.Box {
func get() -> T
}
func test(box1: Box<int>, box2: Box<bool>) -> unit = {
box1.get(); // type is int
box2.get(); // type is bool
unit
}
Expression and arguments can declare values
Value declarations in the expression or the arguments are legal and are visible after the call. Declarations in the expression are also visible to the expressions. Doing this is bad practice in almost all cases. Declare the value before the call instead. For example:
// declares the value `name` and `msg`
// also passes 9 as argument to `test`
// Note: getName returns an stdLib:Result<string, _ : Exception>
(val name = getName()).unwrapOrDefault(
// `name` is visible inside the arguments
val msg = "Could not get name, exception: ${if (val ok = name.isOk())
"No error"
else
name.unwrapErr()
}"
);
(name, ok, msg); // `name`, `ok` and `message` are visible after the call.
val name = getName();
val ok = name.isOk();
val msg = if (ok)
"Could not get name, exception: No error"
else
"Could not get name, exception: ${name.unwrapErr()}"
create supplier¶
A supplier for a value can be created with the supplier
keyword.
It has the syntax supplier$TypeArgs($Exps)
, for example supplier(47)
or supplier<string>("Hello world!")
.
The type arguments can either be omitted or must be a single type argument.
The expressions are the arguments for the supplier.
There should be only one argument, the value that the supplier will supply.
The type T
for the supplier is the type argument if it was provided, or the type of the argument otherwise.
In case a type argument is provided, the argument should be a subtype of that type argument.
The type of a supplier creation expression is supplier<T>
.
Supplier can be treated as a normal function
Creating a supplier is like a normal function call, but built into the language grammar for implementation reasons. This is the only function call where the type argument is derived at the moment.
task supplier¶
A task supplier expression creates a supplier from a function.
A task supplier expression does not execute the task yet, but instead defers it until the supplier's get
method is called.
It has the syntax $ModuleList$FUNCID.supplier$TypeArgs($Exps)
, for example lang:java:parse.supplier(file)
.
The second element is the function name.
This function name can either be qualified or left unqualified by the module list.
If it is unqualified, the function name must be defined in the current module or be imported with a function import.
If it is qualified, the function is looked up in that module.
The number of type arguments must match the number of type parameters on the function declaration, and the type arguments must be within bounds for the type parameters.
The expressions are the arguments to the function.
They must match the number of parameters that the function declared and they must be subtypes of the parameters.
The type of a task supplier expression is supplier<T>
, where T
is the type of the declared function with the type parameters replaced with their corresponding type arguments.
requires¶
A requires expression expresses that the current task depends on the given path.
It has the syntax requires $Exp $FilterPart? $StamperPart?
, for example requires ./metaborg.yaml by hash
or requires sampleDir with extension "sdf3"
.
The expression is the path to depend on.
The filter part is optional and adds a filter to filter out any paths that do not match the filter.
It is described in the section on common lexical elements.
The stamper part is also optional and provides a way to determine if a file or path is up-to-date.
It is also described in the section on common lexical elements.
The type and value of the expression is unit.
Exceptions?
What happens if there is another task that provides the path? Does it quietly schedule that task before this one, does it throw an error? What if that other task runs after this task?
generates¶
Marks the given path as provided by the current task.
It has the syntax generates $Exp $StamperPart?
, for example generates file by hash
.
The expression is the path to depend on.
The stamper part is optional and provides a way to determine if a file or path is up-to-date.
It is described in the section on common lexical elements.
The type and value of this expression is unit.
Make file modifications before using this expression
The contents or metadata of the file at the time that this expression is called may be cached and used for incrementality. Make all modifications to the file before using this expression.
Exceptions?
Can this mark a directory as provided or only a file? What happens when two tasks generate a file? What happens when this task declares it generates a file after another task has already used it? Can a task both require and provide a file? What happens if this task calls a task that provides a file and then this task also declares it generated that file?
list¶
Defining list literals
This section is about listing children of a directory with the list
keyword.
To define a literal list value, see list literal.
Lists direct children of the given directory.
List expressions have the syntax list $Exp $FilterPart?
, for example list getProjectRootDir() + ./examples with extension "pie"
.
The expression must have type path and refer to an existing directory.
The filter part is optional and adds a filter to filter out any paths that do not match the filter.
It is described in the section on common lexical elements.
A list expression returns a list of the direct children of the given directory, and its type is path*
.
Declaring a dependency on the directory
You will likely need to declare a dependency on the directory using requires. You may also need to declare dependencies on the individual files if you do not call a task which already does that.
Recursive listing
List only gets the direct children of the given directory. To recursively get all files and directories in a given directory, use walk.
Todo
What happens if the starting directory does not exist? What happens if it is not a directory?
walk¶
Recursively gets descendants of the given directory.
Walk expressions have the syntax walk $Exp $FilterPart?
, for example walk getProjectRootDir() + ./src/test/java with extension "java"
.
The expression must have type path and refer to an existing directory.
The filter part is optional and adds a filter to filter out any files that do not match the filter.
It is described in the section on common lexical elements.
A walk expression returns a list of all the files in the given directory and its descendants, and its type is path*
.
Declaring a dependency on the directory
You will likely need to declare a dependency on the directory and all subdirectories using requires. You may also need to declare dependencies on the individual files if you do not call a task which already does that.
Getting only the direct descendants
Walk recursively gets all files in the given directory. To only get direct and directories in a given directory, use list.
Todo
What happens if the starting directory does not exist? What happens if it is not a directory? Does the filter also filter directories or only files? Should recursive directories automatically be declared required?
exists¶
Checks if a file or folder exists.
The syntax is exists $Exp
, for example exists ./config.json
.
The expression is the path for which it should be checked if it exists.
It returns a boolean indicating whether the file or path exists.
read¶
Reads the contents of the given file into a string.
The syntax is read $Exp
, for example read pie.sdf3
.
The expression is the file to be read, with type path.
The file is read with the system default file encoding.
It returns a string with the contents of the file.
Return¶
Returns from the current function with the provided value.
Its syntax is return $Exp
, for example return true
or return errResult<FileNotFoundException>(createFileNotFoundException("could not find $file"))
.
The expression is evaluated and its value returned.
The type of the expression should be a subtype of the declared return type of the current function.
The type of a return expression is unit.
Type may get changed to bottom type
The type of a return expression may be changed to the bottom type in the future. This would allow using a return expression as a branch in an if-else expression.
fail¶
Throws an ExecException
with the provided string
as message.
This exits the function, any code after this expression is not evaluated.
Its syntax is fail $Exp
, for example fail "Could not open $file, it does not exist"
The expression is the message for the exception and must be of type string.
The type of a fail expression is unit.
Type may get changed to bottom type
The type of a fail expression may be changed to the bottom type in the future. This would allow using a fail expression as a branch in an if-else expression.
consider using Result<T, E>
Fail throws an exception, which cannot be handled in the PIE DSL.
We recommend using Result<T, E>
from the PIE standard library instead.
Unit literal¶
unit
is a literal expression of the only value of the unit type.
True¶
true
is the literal expression for one of the two values of the boolean type.
False¶
false
is the literal expression for one of the two values of the boolean type.
int literal¶
Int literals are a literal value of the int type.
Their syntax is "-"? [0-9]+
, for example 0
, 1
, 2
, -1
, 47
and -30774
.
That is an optional dash (unary minus, -
), followed by some digits.
This syntax is lexical, meaning that there cannot be any layout between the sign or digits.
valid int values
Int literals represent literal values of the int type. As such, they must be valid values of the int type, i.e. in the range \(-2^{31}\) to \(2^{31}-1\), inclusive. This is currently not enforced, and breaking this constraint will lead to Java compile errors after compiling the PIE code.
-0
is allowed and equal to 0
.
Examples
// Valid integer literals:
0
1
234
2349273
-4
-237894
-0 // same as 0
0010 // same as 10
// invalid integer literals
- 12 // not allowed to have a space between the minus and the digits. Unary minus is not supported.
1 024 // spaces between digits are not allowed
2,048 // commas between digits are not allowed
324.346 // periods between digits are not allowed. Floats do not exist in PIE, separators are not supported.
DEADBEEF // letters are not allowed
0b0101011110010 // binary notation is not supported
0x234e9617ab7 // hexadecimal notation is not supported
abd547 // this is not a hexadecimal value but a value reference
ten // this is not an int literal but a value reference
null¶
null
is the value that is added by making a type nullable.
It is also a value of the top type.
Tuple literal¶
Tuple literals express literal values of the tuple type.
Their syntax is ($Exps)
, for example ("scala", "VF_SCALA", walk (subjectScalaSrcDir + ./lib/scala.jar))
.
The expressions are the elements of the tuple.
There must be at least two elements.
The type of a tuple literal is the tuple type of the types of the elements, so the literal (1?, "a string", true)
has type (int?, string, bool)
.
Tuples with less than two elements
It is not possible to express tuples with zero or a single element. Zero element tuples cannot be expressed by design. Their common use case as a unit type is covered by the unit literal instead. Single element tuples will be parsed as a bracketed expression instead.
Tuple literals from subtype elements
Tuple types cannot be assigned to each other in most cases, so the following is not possible:
data Fruit = $DataImpl
data Apple : Fruit = $DataImpl
data Pear : Fruit = $DataImpl
func getApple() -> Apple = $FuncImpl
func getPear() -> Pear = $FuncImpl
// type (Apple, Pear) cannot be assigned to (Fruit, Fruit)
func example() -> (Fruit, Fruit) = (getApple(), getPear())
To create such a tuple, assign the elements explicitly to the correct types first:
func example() -> (Fruit, Fruit) = {
val apple: Fruit = getApple();
val pear: Fruit = getPear();
(apple, pear)
}
List literal¶
List keyword
This section is about defining list literals.
For information on the list
keyword, see list expressions, which list the children of a directory.
Define a literal list value.
The syntax is [$Exps]
, for example [1, 2, 3]
, or [apple, banana, pear]
.
The expressions are the elements of the list.
The least upper bound of the types of the expressions is the list element type T
.
The type of the list literal is a list of T
, i.e. T*
.
The list element type must not be the top type.
Empty list literals may lead to Java errors
The empty list literal []
has a special type for implementation reasons.
It compiles to a list with the bottom type.
As such, the generated Java code may have compile errors.
String literal¶
Define a literal value of type string.
The syntax is "$StrParts"
, where $StrParts
are parts of the string.
String parts are lexical, which means that there cannot be any layout between them (layout between string parts will be part of the string).
The possible string parts are:
- A sequence of characters excluding
$
,"
,\
and newlines, for exampleA sequence of characters = 123!?
. This expresses that exact sequence of characters. $
followed by the name of a value or parameter, for example$dir
. This converts the value to a string. It is an error to use an undefined name.${$Exp}
, for example${1 + 2}
. This evaluates the expression and converts the resulting value into a string.\$
. This represents the literal character$
.\
followed by another character. This represents a character according to Java semantics. For example,\n
is a newline character,\\
is a single backslash, and\r
is a carriage return character. In particular,\"
represents the literal character"
, and does not end the string literal.
All of the string parts are concatenated into a single string value without separating characters between them.
path literal¶
Define a literal value of type path.
The syntax is $PathStart$PathParts
, for example /home/alice/very-important-documents
or ./src/test/resources/
.
$PathStart
is either /
for absolute paths or ./
for relative paths.$PathParts
are parts of the path.
Path start and path parts are lexical, which means that there cannot be any layout between them (layout between path parts will result in parse errors).
The possible path parts are:
- A sequence of characters excluding
$
,"
,\
and layout, for examplepath/to/the/goods
. This expresses that exact sequence of characters. $
followed by the name of a value or parameter, for example$rootDir
. The value must be of type path. It is an error to use an undefined name.${$Exp}
, for example${getProjectDir()}
. This evaluates the expression. The resulting value must be of type path.\$
. This represents the literal character$
.\
followed by another character. This represents a character according to Java semantics. For example,\\
is a single backslash.
The path start and all of the path parts are concatenated into a single path value without separating characters between them.
Path validity and existence (is not checked)
The validity or existence of path literals is not checked.
This means that a path literal like .////.
is allowed, even though it would be invalid for most file systems.
To check if a path exists, use exists.
Common lexical elements¶
This section describes common lexical elements that are used by multiple expressions.
Filter and FilterPart¶
Filters are used expressions that read directories from the filesystem, so requires, list and walk.
They are used to keep certain paths and ignore all other paths based on the name and the extension.
They have the syntax with $Filter
, for example with extension "str"
or with patterns ["test-res", "result", "generated"]
The possible filters are listed in the table below.
name | expression | description |
---|---|---|
Regex | regex $Exp |
Keeps files if they match the provided regular expression. The expression must be a string representing a regular expression. Todo: Figure out what exactly it matches on (full path, name, includes extension?), regex flavor (Java, some other kind?) |
Pattern | pattern $Exp |
Keeps files if the name contains the provided string. The expression must be a string. TODO: I assume that this only needs to match part of the name and does not include the extension |
Patterns | patterns $Exp |
Keeps files if the name contains any of the provided strings. The expression must be a list of strings. TODO: I assume that this only needs to match part of the name and does not include the extension |
Extension | extension $Exp |
Keeps files if the file extension matches the provided string. The extension must match the string exactly, so pie is different from PIE and PIE-simple . The string should not include the period (. ) separating the filename and the file extension. The expression must be a string. TODO: I assume that it needs to be an exact match. Can it match pp.pie ? |
Extensions | extensions $Exp |
Keeps files if the file extension matches any of the provided strings. The extension must match one of the strings exactly, so pie is different from PIE and PIE-simple . The strings should not include the period (. ) separating the filename and the file extension. The expression must be a list of strings. TODO: I assume that it needs to be an exact match. Can it match pp.pie ? |
Todo
Find exact semantics for the filters. Can they handle directories or do they only work on files? See also todos in the table.
Multiple filters
It is not allowed to use multiple filters. If you need multiple filters, encode your requirements in a regex filter instead.
Stamper, StamperPart and StamperKind¶
A Stamper specifies how it is determined whether a path is up-to-date when executing incrementally.
They are used by requires and generates.
They use the syntax by $StamperKind
, where the stamper kind can be hash
or modified
.
Stamping by hash
will calculate the md5 hash of a file and assume that the file is up to date if the hash matches the cached hash.
Stamping by modified
will check the modification time, and assumes it is up-to-date when that time is at or before the cached time.
Checking the full file contents
There is currently no way in the PIE DSL to specify that the full file contents should match for a file to be considered up-to-date.
If you need this, write the task in Java or use read file
.
Todo
Does it work for directories or only files?
Does the hash calculate md5 hash or another hash?
What happens when a file is generated by modified
but required by hash
?
What is the default modifier?
Created: October 17, 2024