On this page
Reuse calculations and choose when state is shared.
Atlas functions can accept untyped or typed values, return results, deep-copy a typed
struct, share a struct's members with &, or work with a true writable alias
created by var alias = ⌖. These mechanisms are related, but they are
not interchangeable.
Function declaration and call syntax
A function is declared at the top level with the function keyword. Its
parameter list is positional and its body is a block. A call supplies exactly one argument
for each parameter.
function writeMessage(string message) {
system.log(message);
}
function clampValue(var value, var minimum, var maximum) {
if (value < minimum) {
return minimum;
}
if (value > maximum) {
return maximum;
}
return value;
}
function stateLabel(bool active) {
if (active) {
return "active";
}
return "inactive";
}
event on_frame() {
var bounded = clampValue(125, 0, 100);
writeMessage("value: " + bounded);
writeMessage(stateLabel(bounded > 0));
}
One header form
Every function is declared as function name(parameters) { ... }. This is the
only way to declare a function, whether it returns a value or not.
return statements return: stateLabel above returns text because
both of its return statements return text, and writeMessage
returns no value because it has no return value;. A header that starts with a
type or another name instead of function, such as
string stateLabel(bool active) { ... },
is the compile error atlas.typed_function_header. In the editor, its quick fix
replaces the leading type with function.
Parameters
A parameter has one of these forms:
value // untyped parameter; accepts any value kind
var amount // explicit numeric parameter; let amount and auto amount are synonyms
bool enabled // explicit bool parameter
string label // explicit text parameter; char label is a synonym
font style // explicit font parameter
model detector // explicit model parameter
target candidate // explicit target parameter
color marker // any documented vision-tool type is accepted
TrackingSettings data // typed struct, copied deeply
TrackingSettings &data // typed struct, shared members
The vision-tool parameter types are color, contour,
motion, edges, circle, line,
template, feature, match, qr,
barcode, ocr, track, face, and
person. In a function header, types appear only on parameters, never before the
function name.
- Parameter and argument order is significant.
- User functions do not have optional, default, named, or variadic parameters.
- Too few or too many arguments are validation errors.
- Parameters may be used like local variables inside the function.
- A parameter name may not appear twice in one parameter list.
- There is no formal array-parameter declaration syntax.
handleis a runtime value kind, not a declaration type; receive a handle through an untyped parameter.
Return values
Use return expression; to produce a result or return; to leave a
no-result function early. Events may use only the bare form. A function declares no return
type, so its return statements alone decide what it returns; the rules below
keep that result consistent.
function absoluteDistance(var distance) {
if (distance < 0) {
return -distance;
}
return distance;
}
function reportIfLarge(var value) {
if (value < 10) {
return;
}
system.log("large value: " + value);
}
event on_frame() {
var distance = absoluteDistance(-12.5);
reportIfLarge(distance);
}
Rules for value-returning functions
| Rule | Meaning |
|---|---|
| One valued return makes the function value-returning | If any path contains return value;, Atlas expects a value result. |
| Every structural path must return | An if needs returning branches or a later guaranteed return. Loops do not count as guaranteed because they may not execute. |
| Do not mix bare and valued returns | A value-returning function cannot use return; on another path. |
| Return kinds must agree | String, struct, array, target, and other distinct kinds cannot be mixed across paths. |
| Number and bool are compatible | The analyzer permits these two kinds together because bool participates as 0/1 at a numeric boundary. |
| Null does not select a concrete kind | A null return can coexist with a concrete optional-like result, but callers must check for null before member access. |
function category(var score) {
if (score >= 90) {
return "high";
} else if (score >= 50) {
return "medium";
} else {
return "low";
}
}
event on_frame() {
system.log(category(72));
}
A function with only bare returns, or with no explicit return, produces no usable value.
Using such a call in an initializer, argument, condition, assignment, array index, or
return expression is an atlas.void_result error.
Using a result where a type is required
Because a function has no return type, Atlas reads a call's type from the function's
return statements. A return's type is known when it returns:
- a literal (a number, a string,
trueorfalse), an enum member (a number), ornull; - a name declared as
var,string,bool, a struct, a vision type,target, ormodel, including a typed parameter; avar alias = ⌖reference has its target's type; - a struct field, such as
profile.nameorholder.inner, or an element of an array or an array field, such aspoints[i]orholder.list[1]: the element's type; - arithmetic (
-,*,/,%, unary minus,++,--) or+of numbers: a number;+with a string: a string; a comparison,!,&&, or||: a bool; - a conditional (
ready ? a : b): the types of both results; - a
vision.<type>(...)call; a built-in call with a known result, such asmath.abs(...)(a number),string.format(...)(a string), orvision.target(...)(a target);.count()(a number);type(value)(a string); or another function's result.
A name is read in the block where the return is: a declaration inside an inner
block or a for header does not reach a return after it. A struct,
vision-type, or target declaration, an assignment to a struct variable, a struct
field, or an element of a struct array field, and a typed struct parameter accept the call
when every return whose type is known has the required type, also through parentheses or a
conditional. A vision-type or target declaration also accepts a null
result, as it accepts null itself; a struct does not. A call whose returns are all
of unknown type, such as a function that returns its untyped parameter, is accepted too.
Otherwise the declaration or assignment is atlas.type_mismatch and the argument is
atlas.argument_type.
The same rule works the other way: Atlas reports a struct or vision value stored where a
var, string, or bool is declared, so
var kept = makeProfile(); is atlas.type_mismatch when every
return path of makeProfile names that type. A function that can also return a
number or null, or that has one path Atlas cannot type, is not reported — while a
declaration of that struct's own type still accepts it (see
what is not reported). Keep a returned struct in a variable of its own type, such as
Profile kept = makeProfile();, and it returns as that struct again.
struct Profile {
string name = "Default";
var gains[2] = { 1.0, 1.0 };
};
function makeProfile(string name) {
Profile result;
result.name = name;
return result;
}
function makeMarker() {
return vision.color([0, 90, 80], [15, 255, 255]);
}
Profile active;
color marker = makeMarker();
event on_start() {
Profile spare = makeProfile("Spare");
active = makeProfile("Replacement"); // replaces the whole value with a copy
system.log(active.name + " " + spare.name);
}
Storing a struct always stores a deep copy of it, including its nested structs and arrays:
a declaration, an assignment to a struct variable, to a struct field
(holder.inner = makeProfile("A");), to an array element
(list[0] = spare;), or through a reference (slot = spare; with
var slot = &list[1];), and each struct in an array literal. Later writes to
the stored copy never reach the value the function returned, or the variable it was copied
from. A copy follows at most 128 levels of nested structs and arrays; a deeper value, which
only a script storing a struct inside an untyped field of itself over and over can build,
stops the event with the runtime error
Struct value is nested too deeply or refers to itself.
Calls, forward references, and recursion
Atlas collects function names before compiling bodies, so a function may call a helper declared later in the file. Functions may also call themselves, subject to the runtime call-depth limit.
function prepare() {
writeReadyMessage();
}
function writeReadyMessage() {
system.log("ready");
}
event on_start() {
prepare();
}
function factorial(var value) {
if (value <= 1) {
return 1;
}
return value * factorial(value - 1);
}
event on_frame() {
system.log(factorial(5));
}
The VM permits at most 40 active user-function calls. Exceeding that depth reports a runtime error in the session log instead of overflowing the native stack. Prefer loops for ordinary repeated work, and always give recursion a reachable base case.
Function names use a namespace separate from variables, so a function and variable may share a spelling. Calls resolve the function; ordinary reads resolve the variable. Overloading is not supported, and duplicate function declarations are not a portable way to replace a function.
Keep controller I/O directly in on_input
Controller namespace validation is lexical. A function is analyzed as a function, not as
the event that might call it, so input, output, rumble,
lightbar, gyro, accel, and platform controller roots
cannot appear inside a helper function. input.time is the only
input.* property exempt from this restriction.
Pass already-read scalar values into pure helpers, then apply the decision directly in
on_input:
function exceedsThreshold(var value, var threshold) {
return value >= threshold;
}
event on_input() {
var pressure = input.get(ps5.r2);
bool shouldPress = exceedsThreshold(pressure, 50);
if (shouldPress) {
output.set_button("cross", true);
}
}
input.time
because it is a timing value rather than a live controller-state read. Every other
controller read or write should remain in on_input. When the condition above is
false, the script leaves Cross untouched so the physical button continues to pass through.
The packet object follows the same lexical rule: it may appear only directly
inside on_network_recv or on_network_send. Read the packet values
you need in the event and pass them to a helper as ordinary arguments.
network.* capture control may be used in any function.
What pass by value means
An ordinary function call copies each argument's VM value into a new local parameter slot. What that means for later mutation depends on the value category.
| Argument kind | Ordinary parameter behavior | Recommended contract |
|---|---|---|
| Number, bool, null | Inline scalar value is copied. Reassigning the parameter cannot change the caller. | Use an explicit var or bool parameter when practical. |
| String | The VM copies a shared string payload, but the language exposes no string mutation operation. | Treat it as an immutable value. |
| Explicit typed struct | The VM recursively deep-copies the struct, nested structs, and nested arrays. | Use StructName value for a real independent working copy. |
| Untyped parameter receiving a struct | No struct-type metadata requests a deep copy, so the implementation can share the object. | Never rely on this; explicitly type every struct parameter. |
| Array | Arrays are shared heap payloads in the VM, and there is no formal typed array parameter. | Pass a specific element, or put the array in a typed struct and choose copy/reference semantics explicitly. |
| Font, model, target, vision tool, handle | The handle value is copied while the runtime-owned/shared payload remains shared. | Treat these as handles, not deep-copied resources. |
Wrap an array in a struct for clear copy semantics
struct SampleBuffer {
var values[4] = { 1, 2, 3, 4 };
};
function sumBuffer(SampleBuffer buffer) {
var total = 0;
for (var index = 0; index < buffer.values.count(); index++) {
total += buffer.values[index];
}
buffer.values[0] = 99; // changes only the deep copy
return total;
}
SampleBuffer samples;
event on_frame() {
system.log(sumBuffer(samples));
system.log(samples.values[0]); // still 1
}
Struct parameters: deep copy or shared members
Struct parameters are the one place where Atlas exposes an explicit copy-versus-share
choice. A typed struct declared with & is passed by reference in the limited
sense that its object and members are shared; assigning the whole parameter still rebinds
only the callee's local slot. The marker appears in the declaration, never at the call site.
struct Profile {
string name = "Default";
var sensitivity = 1.0;
var gains[3] = { 1.0, 1.0, 1.0 };
};
function editCopy(Profile profile) {
profile.name = "Copy";
profile.sensitivity = 2.0;
profile.gains[1] = 1.5;
}
function editShared(Profile &profile) {
profile.name = "Shared";
profile.sensitivity = 2.0;
profile.gains[1] = 1.5;
}
Profile active;
event on_start() {
editCopy(active); // active is unchanged
editShared(active); // active fields are changed; no & here
}
Profile profile
The callee receives a recursive deep copy. Field and array-element writes remain local to the function.
Profile &profile
The callee receives the same struct object. Field and nested-array-element writes are visible to the caller.
- A reference parameter must explicitly name a declared struct type.
var &value,string &text, and other scalar reference parameters are rejected.- The call is
editShared(active), noteditShared(&active). - The argument must be a compatible struct variable, struct field, or array element rather than a temporary expression.
Whole-parameter replacement is not caller-slot replacement
A shared struct parameter suppresses the deep copy; it does not become a reference to the caller's variable slot. Member writes reach the caller because both values point at the same object. Assigning the parameter itself replaces only the callee's local binding.
struct Profile {
string name = "Default";
};
function replaceLocally(Profile &profile) {
Profile replacement;
replacement.name = "Replacement";
profile = replacement; // only this function's local binding is replaced
}
Profile active;
event on_start() {
replaceLocally(active);
system.log(active.name); // still "Default"
}
If a function must update the caller through a shared struct parameter, mutate fields:
profile.name = "Replacement";. If whole-variable replacement is required,
return a struct and assign the result in the caller (see
using a result where a type is required), or use a true alias
where appropriate.
True writable aliases with var alias = ⌖
An alias is a fixed path back to writable storage. Reading the alias automatically reads
its target; assigning the alias writes the target. It is different from a struct
& parameter.
struct Settings {
var threshold = 0.5;
var weights[3] = { 1.0, 1.0, 1.0 };
};
var total = 10;
Settings settings;
event on_frame() {
var totalRef = &total;
var fieldRef = &settings.threshold;
var elementRef = &settings.weights[1];
var chainedRef = &fieldRef;
totalRef += 5;
fieldRef = 0.75;
elementRef++;
chainedRef += 0.05;
system.log(total); // 15
system.log(settings.threshold); // 0.8
system.log(settings.weights[1]); // 2
}
Supported targets
| Target | Example | Binding behavior |
|---|---|---|
| Variable | var ref = &count; | Reads and writes the variable slot. |
| Struct field | var ref = &settings.threshold; | Retains a path through the base variable slot to that member. |
| Array element | var ref = &values[index]; | Captures the numeric index at the moment the alias is bound. |
| Existing alias | var second = &first; | Keeps the original binding rather than referring to the first alias's private slot. |
Operations through an alias
Plain assignment, compound assignment, and increment/decrement write through. Ordinary expression use reads through. An alias inherits enough target information for string and struct field access to work naturally.
string status = "before";
event on_frame() {
var statusRef = &status;
statusRef = "after";
if (statusRef == status) {
system.log(status);
}
}
A member alias follows replacement of its base variable
The alias stores a reference path, not a raw pointer into one temporary struct payload. If the whole base struct variable is replaced later, the member alias follows the slot and writes the same member of the replacement.
struct Settings {
var threshold = 0.5;
};
Settings current;
event on_frame() {
var thresholdRef = ¤t.threshold;
Settings replacement;
replacement.threshold = 0.9;
current = replacement;
thresholdRef = 0.7;
system.log(current.threshold); // 0.7
}
Alias restrictions
- The declaration must use
var, even when the target currently contains a string or struct. - An alias declaration cannot be const and cannot declare array dimensions.
- The target must be a writable script variable, field, or array element.
- Literals, temporary expressions, call results, enum members, constants, functions, namespace roots, and readonly API properties cannot be targets.
- A reference cannot bind to itself.
&is allowed only directly in an alias initializer. It cannot be used in a later assignment, argument, return, or general expression.- Assigning an alias writes through; it does not rebind the alias. Atlas has no alias-reseat operation.
- A local alias is useful only while its containing function or event call is active.
var badLiteral = &5;
const LIMIT = 5;
var badConst = &LIMIT;
string badDeclaration = &status;
status = &anotherStatus;
Choose the right sharing mechanism
| Mechanism | Declaration | What is shared? | Whole-value assignment |
|---|---|---|---|
| Ordinary scalar parameter | var value | Nothing mutable; the scalar binding is copied. | Changes only the callee local. |
| Typed struct value parameter | Settings value | Nothing; struct, nested structs, and arrays are deep-copied. | Changes only the deep copy. |
| Typed struct shared parameter | Settings &value | The struct object and its members. | Replaces only the callee binding; mutate fields to affect caller. |
| True alias | var ref = ⌖ | The target variable slot or captured member/index path. | Writes through and can replace the target's whole value. |
Function design checklist
- Declare every function with
function; put types on parameters, never before the name. - Give every user call exactly the declared argument count.
- Explicitly type struct parameters so copy behavior is intentional.
- Use
StructName &valueonly for member mutation, and call it without&. - Return a value on every path once any path returns a value.
- Keep all return kinds compatible; a function returns whatever its
returnstatements return. - Keep controller reads and writes directly inside
on_input, andpacketdirectly inside the network events; pass scalar data to pure helpers. - Keep recursion bounded and event work small.
- Use a true alias only when a fixed writable binding is clearer than direct assignment.
Continue the guide
Language
Review declarations, types, arrays, structs, operators, control flow, scope, and limits.
Events
Learn exactly when each callback, including the network packet events, runs and what context it may access.
Recipes
Use complete generic examples built around current event and API behavior.
Diagnostics
Resolve parameter, return, type, reference, and runtime failures.