-
Notifications
You must be signed in to change notification settings - Fork 334
Expression 2 Syntax
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, let/local/const , Operators |
Scopes & Conditionals |
if , elseif , else , switch statements |
Loops |
for , while , do while , foreach , continue
|
User Defined Functions | Basic Functions, Lambda Functions |
Specialized functions (misc) |
#include , Exception Handling, Events |
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. |
Important
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.
- 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
]#
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
There are four fundamental types in Expression2, namely:
-
number
- represents numerical data and also truthiness, where any non-zero value is considered "truthy" (see Scopes & Conditionals). -
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 aList
in other programming languages. -
table
- represents a typed table that can have arbitrary string and numerical keys. Similar to aDictionary
in other programming languages. -
function
- represents a user-defined function that you can call
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 |
F | Function |
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)
Number literals can be created in binary or hexadecimal by prefixing the number with 0b
for binary or 0x
for hexadecimal.
print(0b10) # prints "2"
print(0x16) # prints "22"
Hexadecimal and unicode can be used in string literals using \xXX
and \u{XXXXXX}
where \u
requires between 1 and 6 hexadecimal characters less than 10ffff
.
print("\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64\x21") # prints "Hello, World!"
print("\u{2615}") # prints "☕"
So far we've talked about global variables, but if you want to create a variable only accessible inside the current scope, use the let
keyword.
Note
let
is a new keyword replacing local
There's no difference between the two at this moment, but let
reserves the right to make potentially necessary breaking changes.
So if you see local
in code, it just means let
.
if (1) {
let MyVar = 55
}
print(MyVar)
Caution
ERROR: Undefined variable (MyVar)
If you don't plan on modifying the variable, you can define it as const
.
const X = "Hi!"
X = "Hello!"
Caution
ERROR: Cannot redeclare constant variable
Expression2 supports the following operators in the order of precedence (from highest to lowest priority):
Priority | Operator | Syntax | Description |
---|---|---|---|
1 | Increment | Var++ |
Causes Var to increase by 1 |
1 | Decrement | Var-- |
Causes Var to decrease by 1 |
2 | Literals | Hardcoded numbers/strings | |
2 | Trigger | ~Input |
Outputs 1 if the following input caused the current execution |
2 | Delta | $Var |
Outputs the change in Var since the last call of $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 plus | +Var |
Returns the identity (same value) of Var
|
5 | Unary minus | -Var |
Negates the value of Var
|
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 |
Returns the binary XOR of A and 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 Conditional | A ? B : C |
Evaluates to B if A is truthy, otherwise C
|
17 | Binary Conditional | A ?: B |
Equivalent to A : A ? B
|
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.
Important
E2 does not use typical booleans, and instead uses truthiness (or truth value). In place of booleans, most E2 expressions use 0
to represent falsiness and 1
or non-zero to represent truthiness.
However, you're not limited to using numbers. Every built-in type in E2 has a truth value test to it, which determines an object's truthiness. For example, the string ""
is falsy while "0"
is truthy. Empty arrays and tables are falsy while filled ones are truthy.
Generally, you can expect that for any type, it's falsy value is its default, uninitialized value, also called a zero value.
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")
}
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
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")
}
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
For performance reasons, you should avoid using switch statements when possible.
switch (Var) {
case 2,
print("Var is 2")
break
case 3,
print("Var is 3")
break
case 4,
print("Var is 4") # Will fall through to the default case as well
default,
print("Var is not 2 or 3")
}
Equivalent code using if statements
if(Var == 2) {
print("Var is 2")
} elseif(Var == 3) {
print("Var is 3")
} else {
if(Var == 4) {
print("Var is 4")
}
print("Var is not 2 or 3")
}
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 optional increment step value at each iteration. By default, the step count is 1.
#This will call the code 5 times (1 - 5 inclusive). In this case, the third argument is redundant.
for (I = 1, 5, 1) {
...
}
Note
If you don't need to keep track of the iteration count in a for loop, you can discard the iteration variable to save a bit of performance.
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)){
...
}
A do while
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))
The foreach
loop allows you to iterate through all the values of an array or table.
foreach(Key:number, Value: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(Key:string, Value:number = MyTable) {}
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
}
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.
A function can be defined using the following syntax:
function helloWorld() {
print("Hello world!")
}
helloWorld()
This function will have a return type of void
, which means that it does not output a value.
Note
Function parameters are assumed to be numbers by default, unless you specify a different data type using the format <NAME>:<TYPE>
.
This behavior shouldn't be relied on and is deprecated, though.
function hello(Text:string) {
print("Hello ", Text)
}
hello("World!")
You can specify the return type of a function to let you pass values from it to another piece of code.
function string compliment(Name:string) {
return "You look nice, " + Name + "!"
}
print(compliment(owner():name()))
Important
You are required to make sure your function returns at the end of all code paths. The editor will error if you do not. If you're not sure where to place the return, put it at the very end of your function.
Variadic parameters allow you to call a function with a varying amount of parameters.
This is essentially just a function that takes an array, but it allows you to pass the arguments to the function directly rather than needing to wrap the arguments in an array()
# This function calls printColor with a prefix `[neat] `.
function printMessage(...Args:array) {
printColor( array(vec(0, 172, 0), "[neat] ", vec(255)):add(Args) )
}
printMessage("Cool! Here's a number: ", randint(1, 27), ".")
You can also define the variadic parameter as a table, which allows you to use arrays and tables in the function call.
# Sums all arrays together
function number arraySum(...Args:table) {
local Sum = 0
foreach(_:number, R:array = Args) {
Sum += R:sum()
}
return Sum
}
print(arraySum(array(6, 1), array(11))) # Prints "18"
(Formerly called meta-functions)
A method is prefixed by a type followed by a colon. This is functionally identical to a normal function, but looks nicer.
function entity:hello() {
print("Hello " + This:name())
}
owner():hello() # Prints "Hello " followed by your username
Lambdas are functions that can be stored in variables, also known as first-class functions. For simplicity, you should use normal functions when you don't need the flexibility of lambdas.
Lambdas are quite simple to use. Its syntax is similar to creating a function, and you call a lambda similarly to a normal function.
let MyLambda = function(S:string) {
print(S)
}
MyLambda("Hello, World!") # Prints "Hello, World!"
Lambdas automatically determine their return type when you define them. To retrieve the value of a lambda return, you'll need to specify the return type in [
brackets]
after the lambda call.
let Runs = 0
let GetRun = function() {
Runs++
return Runs
}
print(GetRun()[number])
print(GetRun()) # ERROR! GetRun is assumed to return void here
Lambdas work just like any other type, so you can pass them to functions and return them. You can also define a lambda without storing them and pass them directly (known as a function literal).
let Wrapper = function(Func:function) {
return function() { # Create and return function literal
print("Wrapped!")
Func()
}
}
let Wrapped = Wrapper(function() { # Create and pass a function literal
print("Hello, World!")
})[function]
Wrapped() # Prints "Wrapped!" and "Hello, World!"
See also: E2 Guide: Lambdas
Warning
String calling is deprecated and will not be supported in the future. Look above to Lambda Functions to use the replacement.
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.
let F = "print"
F("Hello world!") # Same as print("Hello world!")
let X = "health"
let OwnerHealth = X(owner())[number] # Same as owner():health()
Warning
These are deprecated and have been superceded by lambdas.
Using them will error on @strict
in the future.
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()
}
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:string) { # < -- 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.
Main article: Expression 2 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.
Please do not alter the e2 docs ...
pages manually.
They are autogenerated from the E2Helper. In the future, this will hopefully be its own dedicated website or tool.
Basic Features: core, debug, number, selfaware,
string, timer
🌎 World: angle, color, find, ranger, sound,
🔣 Math: bitwise, complex, matrix, quaternion, vector, vector2/4
📦 Entities: bone, constraint, egp, entity, hologram, npc
👨 Players: chat, console, player, weapon
📊 Data storage: array, files, globalvars, serialization, table
💬 Communication: datasignal, http, signal, wirelink,
❓ Informational: gametick, serverinfo, steamidconv, unitconv
Disabled by default: constraintcore, effects, propcore, remoteupload, wiring
Wire-Extras (repo): camera, ftrace, holoanim, light, stcontrol, tracesystem
- Home 🏠
- Syntax 🔣
- Directives 🎛️
- Editor 🖥️
- Events 🌟 (new!)
Click To Expand
- 🟥 SPU
- 🟥 Address Bus
- 🟥 Extended Bus
- 🟥 Plug/Socket
- 🟥 Port
- 🟥 Transfer Bus
- 🟩 GPU
- 🟥 Dynamic Memory
- 🟥 Flash EEPROM
- 🟥 ROM
- 🟧 Beacon Sensor
- 🟧 Locator
- 🟧 Target Finder
- 🟧 Waypoint
- 🟥 XYZ Beacon
- 🟩 CPU
- 🟩 Expression 2
- 🟩 Gates
- 🟥 PID
- 🟧 CD Disk
- 🟥 CD Ray
- 🟧 DHDD
- 🟥 Keycard
- 🟥 RAM-card
- 🟧 Satellite Dish
- 🟧 Store
- 🟧 Transferer
- 🟥 Wired Wirer
- 🟧 Adv Entity Marker
- 🟧 Damage Detector
- 🟧 Entity Marker
- 🟧 GPS
- 🟧 Gyroscope
- 🟥 HighSpeed Ranger
- 🟧 Laser Pointer Receiver
- 🟥 Microphone
- 🟧 Ranger
- 🟧 Speedometer
- 🟧 Water Sensor
- 🟧 7 Segment Display
- 🟥 Adv. Hud Indicator
- 🟧 Console Screen
- 🟧 Control Panel
- 🟧 Digital Screen
- 🟧 EGP v3
- 🟧 Fix RenderTargets
- 🟥 GPULib Switcher
- 🟧 Hud Indicator
- 🟧 Indicator
- 🟧 Lamp
- 🟧 Light
- 🟧 Oscilloscope
- 🟧 Pixel
- 🟧 Screen
- 🟧 Sound Emitter
- 🟧 Text Screen
- 🟩 Cam Controller
- 🟧 Colorer
- 🟧 FX Emitter
- 🟧 HighSpeed Holoemitter
- 🟧 HoloEmitter
- 🟧 HoloGrid
- 🟥 Interactable Holography Emitter
- 🟥 Materializer
- 🟥 Painter
- 🟧 Adv. Input
- 🟧 Button
- 🟧 Constant Value
- 🟥 Door Controller
- 🟧 Dual Input
- 🟧 Dynamic Button
- 🟧 Eye Pod
- 🟧 Graphics Tablet
- 🟧 Keyboard
- 🟥 Lever
- 🟧 Numpad
- 🟧 Numpad Input
- 🟧 Numpad Output
- 🟧 Plug
- 🟧 Pod Controller
- 🟧 Radio
- 🟧 Relay
- 🟧 Text Receiver
- 🟧 Two-way Radio
- 🟧 Vehicle Controller
- 🟥 Door
- 🟥 Adv. Dupe. Teleporter
- 🟥 Buoyancy
- 🟧 Clutch
- 🟧 Detonator
- 🟧 Explosives
- 🟧 Explosives (Simple)
- 🟥 Forcer
- 🟩 Freezer
- 🟧 Gimbal (Facer)
- 🟧 Grabber
- 🟧 Hoverball
- 🟧 Hoverdrive Controller
- 🟥 Hydraulic
- 🟧 Igniter
- 🟧 Nailer
- 🟩 Prop Spawner
- 🟥 Servo
- 🟥 Simple Servo
- 🟧 Thruster
- 🟥 Touchplate
- 🟥 Trail
- 🟩 Turret
- 🟩 User
- 🟥 Vector Thruster
- 🟥 Vehicle Exit Point
- 🟧 Weight (Adjustable)
- 🟧 Weld/Constraint Latch
- 🟥 Wheel
- 🟥 Wire Magnet
- 🟥 Wired Npc Controller
- 🟧 Debugger
- 🟥 GUI Wiring
- 🟥 Multi Wire
- 🟧 Namer
- 🟥 Simulate Data
- 🟩 Wiring
- 🟥 Beam Reader
- 🟥 Implanter
- 🟥 Reader
- 🟥 Target Filter
- 🟥 User Reader
Gates 🚥
Click To Expand
TBD