Skip to content

Expression 2 Syntax

Seven edited this page Jul 11, 2023 · 61 revisions

Expression 2 is its own custom language, its syntax will take some time to get used to. While it does have some syntax inspiration from languages like C or TypeScript you shouldn't expect the syntax to work the same as those languages.

Also since Expression 2 is its own language, github highlighted should be taken with a grain of salt. We try to swap in other language's syntax highlighting to help visually convey the concepts but your best course of action is to copy any example code and paste it into the ingame editor.



Table of Contents Talks about...
Directives & Preprocessing All Directives, Comments, #ifdef & related
Variables Variables, Datatypes, local, Operators
Scopes & Conditionals if, elseif, else, switch statements
Loops for, while, dowhile, foreach, continue
User Created Functions Basic Functions, Function Parameters, Variadic Parameters, Meta Functions, String Calling Functions
Specialized functions (misc) #include, Exception Handling, Events

Directives

Directives are what explain what should be changed about the E2 at compile time. They Follow this format:

@<NAME> <ARGS>

# Where <ARGS> is usually a whitespace delimited list of variables and their types. Like so:
@persist A:number B:string [C D E]:entity

Here is a table for all Expression2 Directives

Directive Use-case
@name Set the name of the chip that shows when hovering over it. Note this does not have to be the same as the filename.
@input This is where you will define sources of information your chip will take in.
@output This is the information your chip sends out to be used in other wire components.
@persist Any Variable placed after this directive will stay defined between multiple chip operations
@trigger The trigger directive can selectively enable or disable inputs from triggering executions.
@model Adding the path to a prop model after this directive will cause your chip model to display as that prop.
@strict This makes the E2 throw runtime errors whenever an internal error occurs. Not including this directive will make the E2 fail silently
@autoupdate Using @autoupdate will enable auto updating. What this means is that whenever you paste a duplication of an E2 with autoupdate enabled, the E2 will check your files for a new version of that E2 and upload it.

Note regarding @autoupdate:

  • Only works on saved E2s.
  • Auto Updating is disabled by default and will only work if @autoupdate is specified.
  • If you for some reason need to get an old version of your E2 from a dupe, you will have to temporarily change the name of your E2 so that the autoupdate feature doesn't find the file. Then it'll silently fail and leave you with the old code.

Comments

  • adding a # will comment out the rest of that line
  • Multiline comments can be started with #[ and ended with ]#
print("Hello World!") # Prints Hello World!

#[
    The print function requires a string
]#

PreProcessing

The preprocessor commands #ifdef, #ifndef, and #else can be used to determine which code to execute based on whether a function exists or not.

#ifdef propSpawn(svan)
   print("You have propcore!")
#else
    error("This E2 requires propcore!")
#endif

Library creators can use the #error and #warning commands in combination with #if(n)def commands to generate compile-time errors or warnings. For instance:

#ifndef propSpawn(svan)
    #error This chip cannot run without propcore! Enable the `propcore` extension!
#endif

#ifndef superCoolFunction()
    #warning This E2 won't be as cool without superCoolFunction()
#endif

In Expression 2, every variable must begin with a capital letter.

# COMPILE ERROR!
varibleName = 1 

#VALID
VaribleName = 1

DataTypes

There are four fundamental types in Expression2, namely:

  • number - represents numerical data and also truthiness, where any non-zero value is considered "truthy".
  • string - represents a simple string enclosed in quotation marks. It can be multiline.
  • array - represents a sequence of items and can't contain other arrays or tables. Similar to a List in other programming languages.
  • table - represents a typed table that can have arbitrary string and numerical keys. Similar to a Dictionary in other programming languages.

These four basic types are utilized in virtually every Expression2 script. The table below presents all of the types supported by Expression2.

Shorthand Datatype
N Number
S String
R Array
T Table
V Vector
E Entity
A Angle
RD RangerData
B Bone
M2 Matrix2
XWL Wirelink
C ComplexNumber
Q Quaternion

Unlike dynamic programming languages, Expression2 is a statically-typed language, meaning that variables cannot be assigned to different types.

# COMPILE ERROR!
Variable = 55
Variable = "Fifty Five"

# VALID
MyList = array(1, 2, 3, 4)
MyList = array(5, 6, 7, 8)

Locality

Using the local keyword when declaring a variable confines it to the scope in which it is defined. For example:

if (1) {
    local MyVar = 55
}
# COMPILE ERROR!
print(MyVar)  # MyVar is only available inside the if scope


Operators

Expression2 supports the following operators in the order of precedence (from highest to lowest priority):

Priority Operator Syntax Description
1 Increment Var++ Causes the Var to increase by 1
1 Decrement Var-- Causes the Var to decrease by 1
2 Literals Hardcoded numbers/strings
2 Trigger ~Input ...
2 Delta $Var ...
2 Connected Wires ->InOut is InOut connect to the wire I/O?
3 Parenthesis () can act as a function call or be used in math equations to control order of operations
3 Function Calls function(Var) Calls said function with given Var
4 Method Calls Var:method(...) calls the method() function on the given Var
4 Indexing Var[Value,type] Will attempt to locate the provided Value with the provided type within the provided Var
5 Unary positivation +Var Will make the attached Var positive
5 Unary negation -Var will make the attached Var negative
5 Logical NOT !Var if something returns true, attaching this operator will make it return false
6 Exponentiation A ^ B Raised A to the power of B
7 Multiplication A * B Multiplies A by B
7 Division A / B Divides A by B
7 Modulo A % B Returns the remainder of division
8 Addition A + B Adds A and B
8 Subtraction A - B Subtracts A and B
9 Binary Left Shift A << B Equivalent to A * (2^B)
9 Binary Right Shift A >> B Equivalent to floor(A/(2^B))
10 Greater Than A > B if A's value is more than B
10 Less Than A < B if A's value is less than B
10 Greater Than or Equal to A >= B if A's value is more than or equal to B
10 Less Than or Equal to A <= B if A's value is less than or equal to B
11 Equals A == B if A's value equals B's value
11 NOT Equals A != B if A doesn't equal the value of B
12 Binary XOR A ^^ B ...
13 Binary AND A && B Note that this is reversed with the "classical" & operator
14 Binary OR A || B Note that this is reversed with the "classical" | operator
15 Logical AND A & B Note that this is reversed with the "classical" && operator
16 Logical OR A | B Note that this is reversed with the "classical" || operator
17 Ternary A ? B : C and A ?: B which is equivalent to X ? X : Y
18 Assignment A = B The value of A now equals the value of B

It's important to note that some operators work with other types, such as vector arithmetic with +, -, *, and / (per-component), and string concatenation with + (although format or concat should be used for most concatenation operations).



Scopes restrict variables to a particular code block (usually enclosed by curly braces { <CODE HERE> }). The highest level of scope outside of any blocks is called the "global" scope, where @persist, @inputs, and @outputs variables are stored, allowing them to be accessed from anywhere in the code.


if

An if statement executes a block of code only if a condition is met (i.e., the passed variable is non-zero).

if (1) {
    print("This will run")
}

elseif

By using elseif, you can chain conditions together.

if (0) {
    print("never runs since 0 does not meet condition")
} elseif (2) {
    print("This will run since the first if statement failed AND this if statement passes ")
}

else

else is used to handle cases where none of the conditions are met.

if (0) {
    print("This will not run since (0) is not truthy")
} else {
    print("This will always run")
}

Switch Statements

A switch statement is similar to an if chain and allows for "fallthrough" behavior. If you omit the break statement after each case, it will execute every case underneath it (even if the condition isn't met) until it finds a break or reaches the end of the switch block.

Note: It's important to avoid calling a function within the switch statement itself, as it will be called for each condition until a match is found. Instead, the result of the function should be cached in a variable beforehand.

switch (Var) {
    case 2,
        print("Var is 2")
    break

    case 3,
        print("Var is 3")
    break

    default,
        print("Var is not 2 or 3")
}
Same example written with if statements
if(Var == 2) {
    print("Var is 2")
} elseif(Var == 3) {
    print("Var is 3")
} else {
    print("Var is not 2 or 3")
}


for

The for loop consists of three numbers: the first represents the current iteration count, the second represents the iteration count at which the loop should end, and the optional third number represents the increment value at each iteration.

Note: If you don't need to keep track of the iteration count in a for loop, you can omit the variable assignment and directly provide the starting value.

#This will call the code 5 times (1 - 5 inclusive) with an optional (step) parameter being the third number
for (I = 1, 5, 1) {
    ...
}

while

A while loop will execute the code within its block as long as the specified condition is true. It's important to note that while loops can be unstable without perf() included in the conditional statement.

while (perf(80)){
    ...
}

dowhile

A dowhile loop is similar to a while loop, but it will always execute its block of code at least once. It checks the condition at the end of each iteration to determine whether to continue or exit the loop.

do {
    ...
} while (perf(80))

foreach

The foreach loop allows you to iterate through all the values of an array or table.

foreach(K, V:entity = findToArray()) {
    ...
}

It's recommended to annotate the type of keys when iterating through a table using foreach. By default, foreach on a table will iterate through string keys. To iterate through number keys, annotate with :number.

foreach(K:string, V:number = MyTable) {
    # K is the iteration count
    # V is the current value the loop has found
}

break

The break keyword allows you to exit a loop prematurely.

foreach(K, Ply:entity = players()) {
    if (Ply:distance(entity()) < 400) {
        break
    }
    # Haven't found a player closer than 400 units yet
}

continue

The continue keyword allows you to skip a single iteration of a loop.

for (I = 1, 5) {
    if (I % 2 == 0) {
        continue  # Skip even numbers (You should use the for loop step parameter instead, though).
    } 
}


Functions help to improve code organization and readability by allowing you to break down code into reusable and modular components.

Basic function (No parameters, no returns, no meta-type)

function helloWorld() {
    print("Hello world!")
}

helloWorld()

Basic function with parameters (No returns, no meta type)

Note that function parameters are assumed to be numbers by default, unless you specify a different data type using the format <NAME>:<TYPE>.

function hello(Text:string) {
    print("Hello ", Text)
}

hello("World!")

Variadic Parameters

Variadic parameters allow you to call a function with a varying amount of parameters. these are common names for functions where tables or arrays are passed as parameters.

local MSG_PREFIX = array(vec(0, 172, 0), "[neat] ", vec(255))

function void printMessage(...Args:array) {
	printColor(MSG_PREFIX:add(Args))
}

printMessage("Cool! Here's a number: ", randint(1, 27), ".")

Meta Functions (No Parameters, No Returns)

A meta function takes a function as input, allowing the user to build upon the output of the input function. Establishing a Meta Function:

function entity:hello() {
    print("Hello", This:name())
}

Calling the Meta Function:

entity():hello()

function returns

To return a value from a function, use the return keyword. Additionally, you must specify the return type of the function explicitly, such as number in the case of a function named "multiply".

function number multiply(X, Y:number) {
   return X * Y
}

Calling functions with strings

Using string calls to dynamically call functions should be avoided due to their performance cost and hacky nature. However, this can serve as a substitute for "delegates" or "lambdas" from other languages until they are available.

local F = "print"
F("Hello world!") # Same as print("Hello world!")

local X = "health"
local OwnerHealth = X(owner())[number] # Same as owner():health()


#include

The #include syntax in E2 is similar to that of C and C++. When an #include statement is encountered, E2 will execute the code in the specified file. It's recommended to put the #include statement inside an if(first()) statement, and note that despite its appearance, it is parsed like a normal statement and is not related to the preprocessor.

# example_lib.txt
function doStuff() {
    print("Hello world!")
}
if (first()) {
    #include "example_lib"
    #^^ make sure not to include .txt ^^

    # Now you can use functions and global variables from the "example_lib" file.
    doStuff()
}

Exception Handling

By default, Expression 2 does not often throw errors; instead, it will silently fail and return a default value on function calls. However, if you enable the @strict directive, these errors will be thrown as proper runtime errors. To handle them, you should use try and catch blocks.

try { # < -- If an error occurs in this block, it will exit the block and call the catch {} block, passing the error message string.
    error("Unexpected error")
} catch(E) { # < -- Can rename E to anything. It just stores the error message.
    assert(E == "Unexpected error")
}

However, it's important to note that this only covers errors that occur within Expression 2 and not internal Lua errors caused by E2 extensions.

Events

Expression 2 inherited a problematic "clk" system from E1, which involved re-running the code and checking a variable to determine what caused it to run, leading to confusion and headaches. To address this issue, the event system was created as a replacement.

event chat(Ply:entity, Said:string, Team:number) {
    print(Ply, " just said ", Said)
}

Owner = owner()
event keyPressed(Ply:entity, Key:string, Down:number, Bind:string) {
    if (Ply == Owner & !Down) {
        print("Owner released key " + Key)
    }
}

see all events on the ###Expression2 Events### page


See Also

Expression 2 ⚙️

Getting Started 🕊

Guides 🎓

Tools 🛠️

Click To Expand

Advanced

Beacon 💡

Control 🎛️

Data 💿

Detection 👀

Display 💻

Render 🖌

I/O 🔌

Physics 🚀

Utilities 🛠️

RFID 💳

Wireless 🛜

Gates 🚥

Click To Expand

TBD

Extras 🔭

Clone this wiki locally