Functions

Declare FUNCTIONs with default, optional, variadic and REF parameters, return typed values, overload by argument count, hook into the Engine's lifecycle, and pass handles.

In one line#

A FUNCTION is a named block of code that takes parameters and gives back a value. It runs from start to end in one go, inside the current frame. This page covers how to declare one, how values go in and out, which names the Engine calls by itself, and the handles that let a value point at a variable.

Declaring a function#

FUNCTION <name>[<sigil>]([<parameter>, ...]) [AS <type>[[]...]]
  <statements>
ENDFUNCTION [<value>]
FUNCTION Area(width, height)
ENDFUNCTION width * height

FUNCTION ShowTitle()
  Console("Level 1")
ENDFUNCTION

ShowTitle()                ' Level 1
Console(Str(Area(3, 4)))   ' 12
  • ENDFUNCTION is one word. END FUNCTION is refused with "FUNCTION requires ENDFUNCTION".
  • A call always carries parentheses, even with no argument: ShowTitle().
  • Order does not matter. You can call a function above the line that declares it, and declare it after the main DO … LOOP.
  • Names are case-insensitive. area(3, 4) calls Area.
  • Declare functions at the top level of a file, never inside an IF, a loop or another function.
  • A user function wins over a catalogue command of the same name. Name a function Str and every Str(...) in the program calls yours.

⚠️ A bare name is not a call. ShowTitle alone on a line compiles, and fails only when the program runs, because the compiler reads it as a variable. Write ShowTitle().

The PureBasic spellings are aliases: Procedure is FUNCTION, ProcedureReturn is EXITFUNCTION, EndProcedure is ENDFUNCTION. The typed form Procedure.s Name() is not accepted: write Procedure Name() AS string.

Procedure Greet(name AS string) AS string
  ProcedureReturn "Hello " + name
EndProcedure

Console(Greet("Ada"))   ' Hello Ada
DialectWhat changes in aukimi
AGK, DarkBASICSame FUNCTION / ENDFUNCTION value / EXITFUNCTION value. aukimi adds default values, OPTIONAL, variadic parameters and overloads.
PureBasicProcedure works as an alias, but types are written AS <type>, not .s or .i after the keyword.
Visual BasicNo Sub: a FUNCTION that returns nothing is a procedure. REF goes after the parameter name (see below).

Returning a value#

There are three ways to leave a function:

You writeWhereWhat it does
ENDFUNCTION <value>the last linereturns the value when the code reaches the end
EXITFUNCTION [<value>]anywhere in the bodyleaves at once, with or without a value
RETURN [<value>]anywhere in the bodythe same as EXITFUNCTION inside a function
FUNCTION Grade$(score)
  IF score >= 90 THEN EXITFUNCTION "A"
  IF score >= 50
    RETURN "B"
  ENDIF
ENDFUNCTION "C"

Console(Grade$(95) + Grade$(60) + Grade$(10))   ' ABC
  • EXITFUNCTION works on a one-line IF … THEN, as above.
  • A function that ends without a value gives back nothing. If you print that result, you see undefined.
  • A function with a return type (AS integer, AS string…) gives back the default value of that type instead: 0, "", an empty record.

⚠️ EXIT FUNCTION in two words is not refused, but the program cannot run. Write EXITFUNCTION.

FUNCTION Sign(n)
  IF n < 0 THEN EXIT FUNCTION -1
ENDFUNCTION 1

Typed returns#

AS <type> after the parentheses converts every returned value to that type.

FUNCTION Whole(n) AS integer
ENDFUNCTION n

FUNCTION Label(n) AS string
ENDFUNCTION n

Console(Str(Whole(7.9)))       ' 7
Console(Label(3) + Label(4))   ' 34
  • A record type (AS Vec) or an array type (AS integer[], up to six []) returns an independent copy. Changing what you received never changes the original.
  • An unknown type is a compile error: "Unknown BASIC return type".
TYPE Vec
  x AS float
  y AS float
ENDTYPE

FUNCTION MakeVec(x AS float, y AS float) AS Vec
  LOCAL v AS Vec
  v.x = x
  v.y = y
ENDFUNCTION v

p AS Vec
p = MakeVec(3, 4)
Console(Str(p.x + p.y))   ' 7

A sigil on a function name#

A sigil ($, #, %, &) can end a function name, as in classic BASIC. It is a label for the reader, not a conversion.

FUNCTION Half#(n)
ENDFUNCTION n / 2

FUNCTION Title$(name$)
ENDFUNCTION "Sir " + name$

Console(Str(Half#(7)))     ' 3.5
Console(Title("Robin"))    ' Sir Robin
  • You may call the function with or without its sigil: Title("Robin") and Title$("Robin") are the same call.
  • The sigil never converts the value. Whole%(9.8) with no AS returns 9.8. To convert, write AS integer.
  • The sigil is not part of the name. FUNCTION Score(a) and FUNCTION Score$(a) declare the same function, so writing both is an error.

Parameters#

[OPTIONAL] <name>[<sigil>] [REF | BYREF] [AS <type>[[]...]] [= <default>]
PARAMARRAY <name> [AS <type>]
<name> [AS <type>]...

The second and third lines are the variadic forms. They must be the last parameter.

Default values and OPTIONAL#

FUNCTION Box(width, height = width)
ENDFUNCTION width * height

FUNCTION Greet$(name$ = "stranger", OPTIONAL mark AS string)
ENDFUNCTION "Hello " + name$ + mark

Console(Str(Box(3)))           ' 9
Console(Str(Box(3, 2)))        ' 6
Console(Greet$())              ' Hello stranger
Console(Greet$("Ada", "!"))    ' Hello Ada!
  • = <default> makes a parameter optional. The default is computed at each call, so it may use an earlier parameter (height = width).
  • OPTIONAL without a default gives the default of the parameter's type: 0 for a number, "" for AS string, an empty record or array.
  • A required parameter cannot follow an optional one: compile error.

⚠️ A sigil does not type an optional parameter. OPTIONAL name$ receives 0, not "", when it is left out. Write OPTIONAL name AS string, or give it a default: name$ = "".

⚠️ You cannot skip an argument in the middle. F(1, , 5) is not refused, but the empty slot disappears and 5 lands in the second parameter. Pass every argument up to the last one you need: F(1, 2, 5).

Variadic parameters: PARAMARRAY and ...#

The last parameter can collect every remaining argument into an array.

FUNCTION Average#(PARAMARRAY values AS float)
  LOCAL sum# = 0
  IF values.length < 0 THEN EXITFUNCTION 0
  FOR i = 0 TO values.length
    sum# = sum# + values[i]
  NEXT i
ENDFUNCTION sum# / (values.length + 1)

FUNCTION Join$(separator$, parts...)
  result$ = ""
  FOR i = 0 TO parts.length
    IF i > 0 THEN result$ = result$ + separator$
    result$ = result$ + parts(i)
  NEXT i
ENDFUNCTION result$

Console(Str(Average#(2, 4, 9)))         ' 5
Console(Str(Average#()))                ' 0
Console(Join$("-", "a", "b", "c"))      ' a-b-c
  • PARAMARRAY values and values... are the same thing.
  • With a type, write PARAMARRAY values AS float or values AS float.... The form values... AS float is refused.
  • Every value is converted to the type, and records are copied.
  • A variadic parameter cannot have a default and cannot be REF.

⚠️ .length is the last index, not the count (as in AGK). Three values give values.length = 2, and no value gives -1. Loop with FOR i = 0 TO values.length, and count with values.length + 1.

Typed parameters#

AS <type> converts the argument when the function starts.

FUNCTION Pad$(text AS string, width AS integer)
  WHILE Len(text) < width
    text = "0" + text
  ENDWHILE
ENDFUNCTION text

Console(Pad$(42, 5.7))   ' 00042

Here 42 became the text "42", and 5.7 became the integer 5.

  • Any numeric type, string, HANDLE, a TYPE you declared, or an array of them (AS integer[], AS integer[][], up to six dimensions).
  • An unknown type is a compile error: "Unknown BASIC parameter type".

⚠️ AS Sprite is not a parameter type. The family declaration that turns on dot notation works on a variable, not on a parameter, and FUNCTION Move(s AS Sprite) is refused. Inside a function, call the full command: SpriteX(s), SpritePosition(s, x, y), not s.x.

Copies, and REF#

By default a function works on a copy of what you pass. REF passes the variable itself, so the function can change it.

TYPE Hero
  hp AS integer
ENDTYPE

FUNCTION HealCopy(h AS Hero)
  h.hp = 100
ENDFUNCTION

FUNCTION HealRef(h REF AS Hero)
  h.hp = 100
ENDFUNCTION

hero AS Hero
hero.hp = 10
HealCopy(hero)
Console(Str(hero.hp))   ' 10
HealRef(hero)
Console(Str(hero.hp))   ' 100
ParameterA number or a stringA record or an array
p (no type)copy⚠️ shared: changes reach the caller
p AS Hero, p AS integer[]copy, convertedcopy
p REF AS Herothe caller's variablethe caller's variable

⚠️ An untyped parameter does not copy a record or an array. Only a typed parameter does. If a function must not touch the original, give the parameter its type.

Where REF goes#

<name> REF AS <type>
<name> BYREF AS <type>

REF and BYREF are the same word. Both go after the name and before AS, and AS <type> is required.

FUNCTION SwapValues(a REF AS integer, b REF AS integer)
  LOCAL t AS integer = a
  a = b
  b = t
ENDFUNCTION

x AS integer = 1
y AS integer = 2
SwapValues(x, y)
Console(Str(x) + " " + Str(y))   ' 2 1

The Visual Basic order is refused ("Invalid BASIC parameter"), and so is REF without a type:

FUNCTION Bump(BYREF n AS integer)
  n = n + 1
ENDFUNCTION
  • The argument must be a place you can assign: a variable, a record field, an array element, the current element of a list, or a map entry. A literal or an expression is refused: SwapValues(1, y) and SwapValues(x + 1, y) give "REF argument is not assignable".
  • REF forwards. A function can pass its own REF parameter on to another REF parameter, and the change reaches the first caller.
  • The type still applies. A REF AS integer that receives a float variable writes an integer into it.
  • A REF parameter cannot be OPTIONAL and cannot be variadic.

⚠️ Do not put a sigil on a REF parameter. text$ REF AS string compiles, but the program does not run as soon as the body reads text$. Write text REF AS string.

Overloads by argument count#

Several functions can share a name if they take a different number of arguments.

FUNCTION Area(side)
ENDFUNCTION side * side

FUNCTION Area(width, height)
ENDFUNCTION width * height

Console(Str(Area(3)))      ' 9
Console(Str(Area(3, 4)))   ' 12
  • The choice is made at compile time, on the argument count only. Types never select an overload.
  • Each function accepts a range of counts: its required parameters at least, all its parameters at most, no limit with a variadic. Two ranges must not overlap.
  • A call that no version accepts is a compile error: "No BASIC overload of Area accepts 1 argument(s)".
FUNCTION Area(side)
ENDFUNCTION side * side

FUNCTION Area(width, OPTIONAL height = 1)
ENDFUNCTION width * height

This one is refused with "Ambiguous BASIC overload: Area": a call with one argument would fit both.

Variables inside a function#

Parameters and LOCAL variables belong to the function. They are created at each call and disappear when it returns. GLOBAL variables are shared by the whole program.

OPTION EXPLICIT
GLOBAL score AS integer = 0

FUNCTION AddPoints(amount AS integer) AS integer
  LOCAL bonus AS integer = amount DIV 10
  score = score + amount + bonus
ENDFUNCTION score

AddPoints(50)
Console(Str(AddPoints(20)))   ' 77

What happens when a function assigns a name that also exists at the top of the program:

At the top of the programInside the function
GLOBAL score (anywhere, even after the function)the shared variable
score AS integer = 0 (typed, no keyword)the shared variable
score = 0 on a line above the functionthe shared variable
score = 0 on a line below the function⚠️ a new local variable
LOCAL score⚠️ a new local variable

Reading works in every case, once the line that creates the variable has run. Only assigning creates the surprise.

⚠️ LOCAL at the top level keeps a variable out of the functions' reach. A function that writes score = score + 1 then gets its own score, and the program stops with "Cannot access 'score' before initialization". Declare shared variables with GLOBAL.

Turn on OPTION EXPLICIT and declare shared variables with GLOBAL. An undeclared name inside a function then becomes a compile error, instead of a silent local copy.

Recursion works: a function can call itself.

FUNCTION Fact(n AS integer) AS integer
  IF n <= 1 THEN EXITFUNCTION 1
ENDFUNCTION n * Fact(n - 1)

Console(Str(Fact(5)))   ' 120

See Variables and types for GLOBAL, LOCAL and OPTION EXPLICIT in full.

What a function cannot do#

A function runs to its end without giving the frame back. So nothing that waits is allowed inside it.

Inside a FUNCTIONResult
ENDcompile error: "END is not supported inside FUNCTION; use EXITFUNCTION"
Wait(...), Sleep(...)compile error
AWAITTASK, AWAITALL, AWAITANYcompile error
Sync(), Swap(), YieldTask()⚠️ no compile error, see below

⚠️ Never call Sync(), Swap() or YieldTask() in a function. In a program with a DO … LOOP or an END, the compiler accepts it but the program cannot start. In a program without them, the call does not wait for anything.

FUNCTION Pause()
  Sync()
ENDFUNCTION

DO
  Pause()
  Sync()
LOOP

To wait across frames, use the main loop, a GOSUB subroutine (which may wait), or a task. See Control flow and The game loop and tasks.

Lifecycle functions#

A program can run in two ways. Either it owns a frame loop (DO … Sync() … LOOP), or it declares functions with reserved names and the Engine calls them. After compiling a script, the Engine looks for these names:

FunctionWhen the Engine calls itArguments
start()once, after the script has loadednone
update(dt#)every frameseconds since the last frame (at most 0.1)
fixedUpdate(step#)at a fixed rate, for physicsthe fixed step, 1/60 s
onCollisionEnter(other), onCollisionStay(other), onCollisionExit(other)when a physics contact starts, lasts or endsthe other entity
onTriggerEnter(other), onTriggerExit(other)when an entity enters or leaves a triggerthe other entity
onMessage(message$, data, sender)when another script sends a message, for example with EntitySend("door", "open")the message, its data, the sender
onAnimationEvent(name$, data)when an animation reaches an eventthe event name and its data
main()instead of start(), if the program has onenone

onStart, onUpdate, onFixedUpdate are accepted for the first three. onCollision and onTrigger mean onCollisionEnter and onTriggerEnter. onAnimEvent means onAnimationEvent.

GLOBAL elapsed# = 0
GLOBAL hits AS integer = 0

FUNCTION start()
  Console("ready")
ENDFUNCTION

FUNCTION update(dt#)
  elapsed# = elapsed# + dt#
ENDFUNCTION

FUNCTION onCollisionEnter(other)
  hits = hits + 1
  Console("hit " + other.name)
ENDFUNCTION

FUNCTION onMessage(message$, data, sender)
  IF message$ = "reset" THEN hits = 0
ENDFUNCTION
  • They keep their name. In BASIC the case does not matter (Update works); the Engine receives the exact spelling it expects.
  • They cannot be overloaded. A second update is a compile error: "BASIC lifecycle function cannot be overloaded".
  • The lines outside any function run once, when the script loads, before start(). That is the place for GLOBAL declarations and setup.
  • No Sync() in update. The Engine calls update once per frame; the frame ends when the function returns.
  • An error in update does not stop it. The error is reported, and update is called again on the next frame, so the same error repeats. An update that takes more than 200 ms for 5 frames in a row is switched off.
  • You can still call these functions yourself, like any other.

⚠️ A DO … LOOP program is run as main(). The loop, and any top-level Wait() or Sleep(), make the compiler build a main(). The Engine then calls main() and never calls start(). Do not declare your own FUNCTION main() in such a program: the loop replaces it without a warning.

⚠️ lateUpdate and onDestroy keep their reserved names, but the editor does not call them on an entity's script at the time of writing. Do not rely on them for game logic or cleanup.

Handles#

A handle is a value that points at a variable. You can store it, pass it and return it, then read or write the variable through it.

@<target>
<name> AS HANDLE
<name> AS POINTER
GetRef(<handle>)
SetRef(<handle>, <value>)
IsRef(<handle>)
ReleaseRef(<handle>)
OPTION EXPLICIT
GLOBAL hp AS integer = 100
GLOBAL link AS HANDLE = @hp

SetRef(link, 75)
Console(Str(hp))             ' 75
Console(Str(GetRef(link)))   ' 75

ReleaseRef(link)
IF NOT IsRef(link) THEN Console("released")   ' released
Console(Str(hp))             ' 75
You writeWhat it does
@targetmakes a handle to target
x AS HANDLE, x AS POINTERa variable that holds a handle (POINTER is the same type). It starts empty.
GetRef(h)reads the variable h points at
SetRef(h, value)writes it, converted to the target's type
IsRef(h)true if h still points at something
ReleaseRef(h)ends the handle. The variable itself is not touched.

What @ accepts: a variable, a record field (@hero.hp), an array element (@scores[1]), the current element of a list, a map entry (@names("ann")). @42 is a compile error: "BASIC @ requires an assignable variable, field, array, list, or map element".

OPTION EXPLICIT
GLOBAL gold AS integer = 5

FUNCTION AddGold(target AS HANDLE, amount AS integer)
  SetRef(target, GetRef(target) + amount)
ENDFUNCTION

FUNCTION MakeCounter() AS HANDLE
  LOCAL counter AS integer = 0
ENDFUNCTION @counter

AddGold(@gold, 10)
Console(Str(gold))   ' 15

GLOBAL c AS HANDLE = MakeCounter()
SetRef(c, GetRef(c) + 1)
SetRef(c, GetRef(c) + 1)
Console(Str(GetRef(c)))   ' 2
  • A handle keeps its target alive. MakeCounter returns a handle to a LOCAL, and the counter survives the end of the function.
  • The target's type wins. A handle to an AS uint8 variable stores SetRef(p, 300) as 44.
  • Reading a record through a handle gives a copy when you assign it, like any record assignment.

When a handle stops working#

EventIsRef(h)GetRef(h) / SetRef(h, …)
ReleaseRef(h)falseruntime error: "BASIC reference handle has been released"
DELETE of the record that holds the fieldfalseruntime error
a HANDLE variable never assignedfalseruntime error

Test IsRef(h) before using a handle you are not sure about. A handle is never left pointing at freed memory.

Handles, REF and classic pointers#

REF parameterHandle
Livesduring one callas long as you keep it
Can be stored, returned, put in a recordnoyes
Syntaxp REF AS T in the parameter list@target, then GetRef / SetRef

PureBasic's @variable gives a memory address, and PeekI / PokeI read and write through it. An aukimi handle is not a number: there is no address arithmetic, no cast to an integer, and no way to make one point at memory you did not name. That is what makes it safe on every target.