Skip to content

Expression 2 Syntax

Vurv edited this page Jan 2, 2023 · 60 revisions

Expression 2's syntax will take some time to get used to. It has similar syntax to Typescript, but blends elements from other languages like C. Expression 2 is it's own custom language.

In Expression 2 conditionals, numbers are considered false if they equal 0. Otherwise, they are considered true. Functions and methods which conceptually return "true" or "false" return 1 and 0 respectively.

Table of Contents

Variables

Casing

In Expression 2, all variables must start with a capital letter.

variable = 5 # COMPILE ERROR! Must begin with a capital letter
Variable = "test" # VALID

Types

There are 4 "base" types in E2. These are:

  • number - The number type. This also represents truthiness, a "truthy" number being anything but 0.
  • string - A simple string delimited by "". They can be multiline.
  • table - A typed table that can have arbitrary string and number keys. Essentially a Dictionary in most other languages.
  • array - A sequential list of items. Cannot contain other arrays or tables. Essentially a Vector or List in most other languages.

There are other types, but they are not essential for all uses (e.g. vector) Unlike dynamic programming languages, E2 is typed, so you cannot set variables to different types.

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

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

local

You may define variables with the local keyword before it, like so:

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

local keeps the variable inside of the scope it is located in.
For more about scopes, read the next section.

Scopes (Conditionals)

Scopes are what limit variables to exist only in a block of code (often delimited by curly braces { <CODE_HERE> }). The "global" scope is the topmost scope outside of any sort of blocks. This is where @persist, @inputs and @outputs variables are stored (so they can be accessed anywhere in the code).

If Statement

An if statement allows you to only run a block of code if a condition is met (variable passed is not 0)

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

Else if

You can use elseif to chain conditions.

if (0) {
    ...
} elseif (2) {
    print("This will run since (0) does not meet conditions and (2) does.")
}

Else

To handle no conditions being met, use else.

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

Switch Statement

A switch statement is essentially just an if chain. It does have other features, such as fallthrough. This is where if you don't add the "break" under each case, it will run every case under it (even if the condition isn't met) until it finds a break or the end of the switch.

Note: Avoid calling a function inside the switch() itself, as it would be called once for each condition until a match is found. Instead you should cache the result in a variable.

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

    case 3, # elseif Var == 3
        print("Var is 3")
    break

    default, # else
        print("Var is not 2 or 3")
}

Loops

Loops are special scopes that repeat themselves. They run at a certain condition, through each element of a list, and can exit/skip iterations.
There are many ways to loop something in Expression 2.

Traditional for range loop.

This will call the code 5 times (1 - 5 inclusive) with an optional (step) parameter being the third number

for (I = 1, 5, 1) {
    ...
}

Traditional while loop

This will call the code in the block while the condition is met.

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

do while loop (new)

This loop is the same as a while loop, except it will run once, then check if the condition is met at the end, to see whether to exit the loop or repeat.

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

Loop through an array with foreach

You can loop through all the values of an array/table with foreach.

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

Loop through table with string keys

You often will / should annotate the type of the keys to iterate through a table.
By default foreach on a table will iterate string keys. So if you want to iterate numbers, annotate with :number

foreach(K:string, V:number = MyTable) {
    ...
}

break the loop

You can use the break keyword 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
}

Skip an element

You can use continue to skip an execution 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).
}

Preprocessor

Expression 2 has a preprocessor that allows you to write comments, and tell if functions from E2 Extensions exist at compile time to avoid errors.

Comments

You can write single line comments. (Everything after a # will not run code)

print("Hello World!") # Prints out "Hello world"!

And multiline comments (everything inside #[]# will not be run)

#[
    My hello world E2
]#
print("Hello World")

Directives

Directives are what explain what should be changed about the E2 at compile time.
They are in 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

Read what each one does here

Checking if E2 functions exist

You can use #ifdef, #ifndef, and #else preprocessor commands to define what code to run in case a function exists, or does not exist.

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

or

#ifndef print(...)
    error("E2 is broken..")
#endif

Misc (new)

Additionally, commands #error and #warning exist for you to give a compile time error or warning. This is intended for library creators, in conjunction with the #if(n)def commands. For example:

#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

Functions

Functions are ways to organize your code-flow.

Basic function (No parameters, return value or meta-type)

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

Parameters

  • Note parameters are by default numbers, unless you give a type like <NAME>:<TYPE>
function hello(Text: string) {
    print("Hello ", Text)
}
hello("World!")

Variadic Parameters (new)

You can use variadic parameters to be able to call functions as if wrapped by a table() or array() function.

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

Prints hello with the name of the entity The value from the : type is stored in a variable named "This".

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

Call them like so:

entity():hello()

Returns

You can return a value from your functions using the return keyword.
Note you need to explicitly say the type the function will return (Note the "number" between "function" and the function name (multiply))

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

String Calls

You can call functions dynamically with string calls.
This is basically E2's version of delegates/lambdas until lambdas are implemented, and should be avoided at all costs as they are expensive and hacky.

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 is similar to #include in C and C++.

Given a file, it will run the E2 code in that file at the #include.
Note, since E2 runs the whole chip every execution, this should also be placed inside of an if (first()) statement.

Note, despite looking like a preprocessor command, this has nothing to do with the preprocessor. It is parsed like a normal statement.

Example

# main.txt
if (first()) {
    #include "mylibs/hololib" # <- leave .txt omitted

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

# mylibs/hololib.txt
function doStuff() {
    print("Hello world!")
}

Exception Handling (new)

Expression 2 does not often throw errors, instead, by default, it will often silently fail and return a default value on function calls. If you turn on @strict, these will be thrown as proper runtime errors. To handle these, you need to use try & catch.

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")
}

This statement allows you to catch runtime errors and handle them. Note this does NOT cover internal lua errors caused by E2 Extensions erroring.

Events (new)

Expression 2 inherited an awful "clk" system from E1, based on re-running the code and essentially checking a variable to see what caused it to run, in order to scope events. This has caused so much confusion and headaches, that I've created the event system in order to replace it.

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)
    }
}

Here are the currently supported events and their parameters. Note that autocomplete should work when typing event.

  • event tick() (replacing runOnTick)
  • event chat(Player:entity, Message:string, Team:number) (replacing runOnChat)
  • event keyPressed(Player:entity, Key:string, Down:number, Bind:string) (replacing runOnKey)
  • event input(Input:string) (replacing inputClk, inputClkName, ~Input)
  • event chipUsed(Player:entity) (replacing runOnUse)
  • event removed(Resetting:number) (replacing runOnLast)
  • event playerSpawn(Player:entity) (replacing runOnSpawn)
  • event playerDeath(Victim:entity, Inflictor:entity, Attacker:entity) (replacing runOnDeath)
  • event playerConnected(Player:entity) (replacing runOnPlayerConnect)
  • event playerDisconnected(Player:entity) (replacing runOnPlayerDisconnect)
  • event httpErrored(Error:string, Url:string) (replacing runOnHttp)
  • event httpLoaded(Body:string, Size:number, Url:string) (replacing runOnHttp)
  • event fileErrored(File:string, Error:number) (replacing runOnFile)
  • event fileLoaded(File:string, Data:string) (replacing runOnFile)

Operators

E2 supports the following operators in the listed priority (highest priority first):

  • Increment (Var++) / Decrement (Var--)
  • Literals (hardcoded numbers/strings) / Trigger (~Input) / Delta ($Var) / Connected wires (->InOut)
  • Parenthesis ((...)) / Function calls
  • Method calls (Var:method(...)) / Indexing (Var[Index, type])
  • Unary positivation & negation ((+Var), (-Var), but not Var+Var ) / Logical NOT !Var
  • Exponentiation (A ^ B)
  • Multiplication (A * B) / Division (A / B) / Modulo (A % B, remainder of division)
  • Addition (A + B) / Subtraction (A - B)
  • Binary Left Shift (A << B, equivalent to A * (2^B)) / Binary Right Shift (A << B, equivalent to floor(A / (2^B)))
  • Comparisons (A > B, A < B, A >= B, A <= B)
  • Equal (A == B) / Not Equal (A != B)
  • Binary XOR (A ^^ B)
  • Binary AND (A && B). Note that this is reversed with the "classical" & operator
  • Binary OR (A || B). Note that this is reversed with the "classical" | operator
  • Logical AND (A & B). Note that this is reversed with the "classical" && operator
  • Logical OR (A | B). Note that this is reversed with the "classical" || operator
  • Ternary (A ? B : C and X ?: Y, which is equivalent to X ? X : Y)
  • Assignment (A = B) and some in-place arithmetic (A += B, A -= B, A *= B, A /= B)

Note that many of the operators function for other types, ie vector arithmetic with +-*/ (component-wise), concatenating strings with + (but you should prefer format or concat for most operations).

Some of these operators are explained in more detail below:

Ternary

You can think of ternary as an inline if. You use it to assign or use different variables based on a condition

# Var is set to "Pick me", since 1 is truthy. Otherwise it'd be set to "Will never be picked :(".
Var = 1 ? "Pick me" : "Will never be picked :("

# You can also use :? if the second argument is used as a condition. In this case, "Pick me" is truthy, so it will be the value of Nullish.
Nullish = "Pick me" :? "Will not be picked"

Logical AND (&)

This is not to be confused with binary AND, which E2 uses as &&.

X = 1
Y = 0
if (X & Y) {
    # This will not run, as both X and Y are not truthy (Y is 0).
}

Logical OR (|)

This is not to be confused with binary OR, which E2 uses as ||.

X = 1
Y = 0
if (X | Y) {
    # This will run, since X is truthy, and the OR needs either X or Y to be truthy.
}

Logical NOT (!)

This will return a falsey value, if given a truthy value, and vice versa.

if (!0) {
   # This will run, because the inverse of a falsy value is a truthy one.
}

Input Changed (~)

Must precede an @input variable
Returns 1 if the current execution was caused by a change in the input variable. See the E2 Triggers tutorial
USE THIS INSTEAD OF changed()

if (~Button) {
    print("Button has changed to " + Button)
}

Delta Operator ($)

Must precede an @input or @output or @persist variable
Returns the difference/delta of a variable between executions. See the E2 Triggers tutorial
USE THIS INSTEAD OF changed()

local ScrollChange = $Scroll

Input Wired / # of Outputs (->)

Must precede an @input or @output variable
If used on an input, returns 1 if it's wired to something. If used on an output, returns the number of wires to the output.
USE THIS INSTEAD OF changed()

if (->Digiscreen & Digiscreen) {
    print("Digital screen wired.")
}

Advanced Syntax

Index with type

Get or Set the given index at a table-like object (array, table, gtable, etc). As E2 always needs to know the type of everything ahead of time, you need to provide a type when indexing an object that can contain different types (table, array, gtable):

local Index = 55
Table[Index, string] = "Foo"
assert("Foo" == Table[Index, string])

Index without type

Some types can be indexed without type, because they always contain the same type, like strings (characters as strings) and vectors (coordinates as numbers).

# Get the third character of a string (equivalent to String:index(3))
print(String[3])

# Get the z part of a vector, equivalent to Vector:z(),
# :z() is more efficient, but there are some niche applications for indexing in this way.
print(Vector[3])

Highspeed Memory

When indexing wirelink without type, you access the highspeed memory of that device, when availiable.

@inputs Digiscreen:wirelink
# Set the color mode of a digital screen. This is equvialent to `Digiscreen:writeCell(1048569, 3)`
Digiscreen[1048569] = 3

Expression 2 ⚙️

Getting Started 🕊

Guides (In learning order) 🎓

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