Fluid is a Lua-based scripting language, specially customised for integration with the Parasol Framework. It supports garbage collection, dynamic typing and a bytecode interpreter for compiled code. Lua was chosen as our preferred programming language due to its extensive popularity amongst game developers, a testament to its low overhead, speed and lightweight processing when compared to common scripting languages.
Full compatibility with Parasol's C++ APIs is included. This includes support for all common constants found in the C headers (e.g. error codes, action ID's and special flags). Object classes are self-describing, so the need for external libraries or header files to communicate with a given type of object is unnecessary under normal circumstances. Module functions are also self-describing, offering the same level of access to function calls as one would expect when programming in C.
This manual expands on the information in the existing Lua Reference Manual, and covers features that are exclusive to Fluid. The official Lua 5.1 Reference Manual is required reading before you continue with this document, as a working knowledge of the Lua language is assumed.
For more information on the usage of available classes and modules, please refer to the Parasol API at www.parasol.ws.
For general information on the syntax provided by Lua, please read the following online manuals:
To run a Fluid script, use the parasol executable:
parasol myfile.fluid
Named arguments can be passed to the Fluid script by following the script location with a series of variable values:
parasol myfile.fluid name="John" surname=Bloggs
To debug a Fluid script, use the --log-api
parameter.
For further information on available options, execute parasol with the --help
parameter.
Fluid files can use the file extension .lua
or .fluid
for identification. Ideally, scripts should start with the comment -- $FLUID
near the start of the document so that they can be correctly identified by the Fluid parser.
A number of extensions have been added (and some features removed) to Lua 5.1 in order to add value to the Fluid language. This section examines the major changes in regards to the published Lua Reference Manual.
The Package, IO and OS libraries normally found in Lua are removed as they duplicate features already found in Parasol's Script, File and Time classes. The introspective debug library is also removed for the purpose of reducing size.
Bitwise operations are supported via the [https://bitop.luajit.org/api.html](bitop API).
Single-line C++ style comments are supported in addition to Lua's commenting standard. For example:
// This is a C++ style, single line comment.
--[[ This is a Lua comment --]]
Arguments passed to the Fluid script can be accessed via the arg()
function. In the following example either 'width' is returned or 1024
otherwise:
width = arg('width', 1024)
All arguments are managed as strings regardless of the type of the value.
A non-zero value detection function, nz()
, is included to simplify the handling of nil and zero values (such as a zero length string). Using nz()
simplifies program flow where variables must have a value, for example:
myvalue = nz(var.shoppinglist, 'Empty Basket')
The first parameter is checked if it is empty. If so, the function's second parameter is returned if provided. Otherwise a value of true
is returned.
string.alloc(Size)
Return a string of Size
bytes that is uninitialised. Useful for creating buffers that can be passed to Parasol functions.
str:trim()
Trims whitespace from the left and right sides of a string.
str:rtrim()
Trims whitespace from the right side of a string.
str:split([Sep])
Split a string using Sep
to identify the space between each word. If Sep
is not specified, it defaults to all whitespace characters. Multiple characters can be specified in Sep
if required.
str:startsWith(Cmp)
Returns true
if the string starts with Cmp
.
str:endsWith(Cmp)
Returns true
if the string ends with Cmp
.
str:join({ Values }, Sep)
Join all Values
into a single returned string, separated by Sep
.
The include
statement is used to load the definitions for an API's functions and classes. It accepts a series of API names as input, and will load each in sequence, for example:
include 'core','xml','display'
API interfaces are protected from being loaded more than once. Calls to mod.load()
result in API definitions being loaded automatically, and may make a call to include
being unnecessary in that case.
The require
statement will load a Fluid language file from the scripts:
volume and then executes it. It differs from loadFile()
in that the script will be registered so as to prevent multiple executions, and the volume restriction improves security.
The chosen library may return a table as an interface - a practice we recommend as it allows the client to define the namespace and avoid conflicts with program variables. E.g.
local mylib = require('mylibrary')
mylib.doSomething()
If you're building a complex application and need to include a custom library file, this can be achieved with the ./
prefix. Standard path rules still apply, e.g. ./lib/customlib
would be valid, ./../../h4cklib!3
is not.
Use loadFile()
to load, parse and execute a Fluid script. This feature is useful for re-using code, or breaking a large project into multiple script files. Example:
loadFile('programs:tools/project/example.fluid')
If the path is not fully qualified, the current path of the process will be used to determine the location of the source file.
If the executed script ends with a return
statement, the value(s) will be returned from loadFile()
in their original form.
Use exec()
to parse a Fluid statement and execute it. Can raise an exception if the statement is unparseable or fails during execution. Example:
exec('print("Hello World")')
Calls to the Parasol API do not raise exceptions by default, instead opting to return ERR
codes that can be managed by the client (or ignored). Promoting ERR
codes to exceptions can be a useful at times, so some functionality is provided to enable this feature as seen in the functions described below.
check()
is the equivalent of an assert()
for ERR
codes. Any non-safe error code passed to this function will raise an exception that includes a readable message matching the ERR
code.
Incoming parameters are returned without modification, allowing check() to operate seamlessly in function call chains. Example:
local err, bytes_read = check(file.acRead(file, buffer))
The following ERR
codes will not raise an exception: Okay
, False
, LimitedSuccess
, Cancelled
, NothingDone
, Continue
, Skip
, Retry
, DirEmpty
raise()
will raise an exception immediately from an ERR
code; for example raise(ERR_Failed)
. Unlike check()
, all codes have coverage, including minor codes. The error code will also be propagated to the Script object's Error
field if called from a C/C++ program.
error()
is the standard Lua function for raising exceptions and this is also the case in Fluid. It should be used when raising an exception with a custom error message, e.g. error('I have failed.')
.
Use catch()
to switch on exception handling for functions that return ERR
codes, as well as regular exceptions that would otherwise be caught by pcall()
. Affected code includes: obj.new()
; any module function that returns an ERR
type; and any method or action call.
catch()
is typically called with two parameters - the first being a custom function that will be executed, and the second a function handler that will be activated if an exception is raised. The handler will receive an Exception parameter that contains fields that describe what happened. The fields are code
for the ERR
code (if any); message
describes the exception; line
is the line number that raised the fault.
err, result = catch(function()
-- Code to execute
return 'success'
end,
function(Exception)
print('Code: ' .. nz(Exception.code,'LUA') .. ', Message: ' .. Exception.message)
end)
Sometimes it can be convenient to catch only specific error codes of interest, which can be achieved by listing them just prior to the handler reference:
err, result = catch(function()
-- Code to execute
return 'success'
end,
{ ERR_Failed, ERR_Terminate }, -- Errors to catch
fuction(Exception)
end)
Calling catch()
without an exception handler will handle the exception silently and return the Exception table for further inspection, or nil
if the calls was successful:
local exception, result = catch(function()
-- Code to execute
return 'success'
end)
if exception then
print('Code: ' .. nz(exception.code,'NIL') .. ', Message: ' .. exception.message)
end
In all other cases the ERR
code is returned by default.
Be aware that the scope of catch runs deep, and extends into any functions that are called. Using catch()
over a broad section of code is considered mis-use, and can lead to unexpected confusion. Consider using xpcall()
instead when broad exception handling is desired.
The following ERR
codes are not treated as exceptions:
Okay
, False
, LimitedSuccess
, Cancelled
, NothingDone
, Continue
, Skip
, Retry
, DirEmpty
.
The module interface provides a means for Fluid programs to communicate with Parasol's APIs. They are loaded using the mod.load()
function, for instance:
mGfx = mod.load('display')
Calling mod.load()
returns the function table for the requested module, allowing calls to be made to its functions. For instance:
local err, x, y = mGfx.getCursorPos()
Notice that Fluid is capable of returning multiple results if a function has declared more than one return value. Typically the first value will be an ERR
code.
For consistency, we use a set of commonly named global variables to store module function tables. This is because loading an API more than once is undesirable. The following table illustrates the global names that we use for the default set of APIs.
Module | Global Name |
---|---|
audio | mAudio |
code | mSys |
display | mGfx |
font | mFont |
network | mNet |
vector | mVec |
xml | mXML |
Taking the above into account, our first example should now be written as follows:
if not mGfx then mGfx = mod.load('display') end
Note that the Core module, identified by the mSys
global variable is considered an essential API and is always available by default.
When calling an API function, Fluid uses best efforts to convert function values to the types declared in the function definition. For instance, if a string value is used for a numeric argument, Fluid will automatically convert the string to an integer. If the type cannot be converted (for instance, an abstract pointer cannot be converted to a number) then a type mismatch occurs and an exception will be raised.
While type conversion is convenient, we recommend using the correct type whenever possible as this will ensure that your calls are being made efficiently.
When calling functions that copy results to a user-supplied buffer, pass an array interface. The following example illustrates reading content from a file into an array buffer:
buffer = array.new(file.size, 'byte')
err, result = file.acRead(buffer)
print(tostring(buffer))
Notice that the acRead()
call hasn't been given its second parameter (the amount of data to read). This is possible because Fluid knows the size of the buffer and will use it for the second parameter if not already defined.
Some functions may return multiple result values. Here is an example of a module function that returns two results:
ERR ListMemory(INT Flags, ListMemory **Memory)
The first result is an ERR
code. The ListMemory
result is stored in the variable pointed to by the Memory argument. In a C program, this function would be used as follows:
if (!ListMemory(0, &memory)) { ... }
In Fluid, pointer results are dropped from function specifications because writing to pointer buffers is risky. Instead, these parameters are handled internally and their values are returned as part of the result set. The following example shows how to call ListMemory()
in Fluid:
local err, list = mSys.ListMemory(0)
Some functions like the above can return allocated memory or some other form of temporary resource. These are normally marked as such in the function definition, which allows the garbage collector to automatically remove it once its references have been reduced to zero. Remember to use local
wherever possible to clean up resources that have gone out of scope.
The object interface provides the necessary functionality to create and manage objects. It allows you to read and manipulate object fields, call methods and actions, manage subscriptions and connect to existing named objects.
To create a new object, use the object interface's new()
method with the name of the class that will be instantiated. Here's an example that creates an object, sets necessary field values and initialises it:
file = obj.new('file')
file.path = 'readme.md'
file.flags = '!READ'
err = file.init()
The first result from new()
is the created object's interface and the second result is an ERR
code if creation fails. Take note that an object must always be initialised before any real interaction occurs (anything beyond setting field values).
The above example can be simplified by providing a field list immediately after the class name:
file = obj.new('File', { path='readme.md', flags='!READ' } )
In this case the path
and flags
values will be set on our new object and then initialised automatically (it is assumed all field settings are being defined up-front). The field values may be of any type and Fluid will use best efforts to convert them to the correct field type.
In the event of an error, new()
will throw an exception. It never returns nil. We advise guarding against problems with pcall()
or catch()
, particularly if using classes like File
where error management is good practice.
Initialises the object if not already done so with obj.new()
. Useful if you need to set field values after creation but before initialisation. Does not throw in normal operation, instead an ERR
code is returned to indicate success or failure.
Parasol enforces OO relationships so that an object will always have a parent, and consequently it may have siblings and its own children. By default, all objects created by obj.new()
are owned by the Fluid script (which exists as a runtime object).
Sometimes a new object will need to be declared as the child of an existing object. Imagine for instance, that we have a window open and we want to draw a rectangle to it. This can be achieved with something like:
viewport = glWindow:clientViewport({ aspectRatio = ARF_MEET })
viewport.new('VectorRectangle', { x=0, y=0, width='100%', height='100%', fill='rgb(255,0,0)' })
Notice that the call to viewport.new()
looks suspiciously like an obj.new()
call. Functionally they are identical, but the key difference is that the rectangle is now going to be a child of the viewport instead of our script. When the rectangle is initialised, it will determine that it is owned by the viewport and will appear in that context when displayed.
It is the case that all objects support the new()
method for the purpose of supporting these complex hierarchies.
Be aware that object relationships have absolute priority over other factors in determining their lifetime. In the above example, if the viewport is destroyed (e.g. the user closes the window) then the rectangle will be destroyed with it because its state is linked to the parent. If an object is loosely coupled to a script due to such circumstances, you can call the exists()
method at any time on the object's interface to confirm it is not destroyed.
You can get a list of the children that have been attached to an object by calling the children()
method. It returns an array of UID's that can be converted to interfaces with obj.find()
. The children()
method also supports a class filter in the first parameter, e.g. thing.children('VectorRectangle')
would return a list of all rectangles.
It is possible to access the interface of an existing object by searching for its name (if given one on initialisation) or UID:
window = obj.find('my_window')
winsurface = obj.find(window.surface)
If failure occurs, nil
is returned by the find()
method. If multiple objects exist with the same name, the most recently created object is returned first.
If an object is being shared in a threaded program, locking may be necessary to prevent data corruption. This can be achieved by calling lock()
and passing a function that will gain an exclusive lock on the object. The lock will remain until the function returns.
obj:lock(function()
--Code--
end)
The Lua garbage collector will automatically terminate an object once all references to that object are released or the script is terminated. If an object has a single reference, you can manually terminate it by setting the reference to nil, for example:
file = obj.new('File')
...
file = nil
Sometimes you may need to create an object that remains after the script has executed and is not removed by falling out of scope. Calling detach()
will unlink the object from the garbage collector.
Call free()
to terminate an object resource immediately without removing the interface. If the object is detached then free()
is the most practical means of removal.
Calling free()
is not recommended in general usage - it is preferable to set object references to nil
and either wait for garbage collection or call collectgarbage()
.
After successfully initialising an object interface, interaction with it is possible through actions, methods and fields. The following example illustrates how we could change the size of the VectorRectangle we created earlier:
rect.acRedimension(20, 20, 0, 100, 100, 0)
Action calls are identified by their ac
prefix. Method calls are prefixed by mt
. Field references can be directly accessed without a prefix. The naming scheme for methods, actions and fields is case insensitive.
For information about the actions and methods supported by an object class, refer to its published API documentation on our website.
Object fields are accessible by conventional means, i.e. thing = my_object.field_name
and my_object.field_name = thing
for get and set respectively. There are get()
and set()
methods that are equivalent to these, and they can be useful in special circumstances such as retrieving a value from an array-based field without first retrieving the entire array:
item = view.get('item(10)')
Some classes support key-value pairs in cases where custom string names need to be associated with an object. The getKey()
and setKey()
methods are provided to manage these fields. For example:
surface.setKey('Title', 'My Window')
title = surface.getKey('Title', 'UNDEFINED')
The _state()
method is a special accessor that attaches a Lua table to the object and returns it. This allows you to store custom data with the object that is seperate to its definition. The table will be removed when the object is destroyed.
To test for the presence of a bit in a flag-based field, the following template supported by get()
can be used:
state = object.get('flagfield.flagname')
For example:
state = surface.get('flags.visible')
If the named flag is set, a value of 1 is returned, otherwise 0.
To get the value of a flag or lookup field in string format, prefix the name of the field with a $
symbol when using the get()
method as follows:
name = object.get('$flags')
Parasol allows clients to monitor the calls made to an object by subscribing to its actions and methods. One commonly used strategy involves subscribing to the free()
action of an object so that the client is notified of object termination.
Subscription is enabled via the subscribe()
method. The following example illustrates the creation of an HTTP object that performs a download and calls my_function()
on completion of the download process. Note that Args
is a table that contains named arguments for the action that was subscribed to. The CustomRef
is an optional value that will be passed to the subscriber.
function my_function(UID, Args, CustomRef)
print('Download complete.')
end
http = obj.new('http', { ... } )
http.acActivate()
handle = http.subscribe('deactivate', my_function, customref)
To unsubscribe from an action, call unsubscribe()
with the action/method name. Doing so will allow the garbage collector to remove associated resources that are no longer required.
The processing interface assists with the management of your program's idle state and reaction to signals. Its most basic feature is the commonly used sleep()
function, as follows:
processing.sleep([Timeout], [WakeOnSignal=true], [ResetState=true])
Calling sleep()
in this way will put the script to sleep until a message is received to awaken it. Typically this means that the function won't return until a QUIT
message is received, which would be delivered if the user opts to close the main window.
sleep()
can also return early if a Timeout
in seconds is specified in the first parameter. Waking on signal can be disabled by setting the second parameter to false
. The third parameter controls whether Lua's internal signal state is reset before sleeping. If set to false
and there have been signal()
calls that have gone unprocessed, sleep()
will return immediately and reset the signal state.
If a Fluid script is utilising threads, synchronisation can be an important issue that requires careful management. The processing interface makes it possible to put the main thread to sleep and wake it once a series of signals has been triggered. In the following working example, we create a basic thread that prints a message and then signals two XML objects in order to wake the main thread. This might be plausible if we were asking the thread to process XML data for instance:
function testSignals()
local signal_a = obj.new('xml', { flags='NEW' })
local signal_b = obj.new('xml', { flags='NEW' })
-- Note that the thread code is parsed as a string and can't see any variables without a call to obj.find().
-- The termination handler on the other hand has access to signal the objects directly.
thread.script([[
msg('Thread is now in session.')
]],
function()
('Thread has been executed.')
msgsignal_a.acSignal()
signal_b.acSignal()
end)
local proc = processing.new({ timeout=1.0, signals = { signal_a, signal_b } })
('Sleeping....')
msglocal err = proc.sleep()
assert(err == ERR_Okay, "Unexpected error: " .. mSys.GetErrorMsg(err))
end
Notice that we called processing.new()
to create a dedicated interface for signal management. The two objects that will be signalled are passed in via the signals
field. When the thread completes, it calls acSignal()
on the objects to change their signal state. It is necessary for both objects to change state in order to wake the main thread.
If no signal objects are listed in a call to processing.new()
, it is possible to manually call the signal()
method to wake the sleeping thread instead. This simple technique is sometimes used in passive event systems, e.g. the user clicking a mouse button could result in a signal()
call that wakes the main thread.
The task()
function returns a Fluid object that references the current task:
local currentTask = processing.task()
This function provides access to the task object representing your script's execution context. The returned object can be used to modify task properties such as process priority, which is useful for applications requiring consistent performance:
local task = processing.task()
if task then
task.set('Priority', 15) -- Set high priority for time-critical operations
end
Use delayedCall()
to call a function on the next message processing cycle inside processing.sleep()
. This is useful in situations where calling a function is necessary, but to do so immediately would cause logistical issues with the order of execution.
processing.delayedCall(function
print('This message is appearing after being delayed.')
end)
math.round(Num, [DecimalPlaces])
Round floating point Num
to the nearest integer. If DecimalPlaces
is specified then the function will round to the place indicated. For example math.round(8.2468,2)
rounds to 8.25
.
The Lua strings interface is extended with a number of useful functions that are commonly required in programming. The following functions are included:
str = string.alloc(Size)
Create a string of a given size in bytes. This feature is intended for creating sized data buffers that can be passed to Parasol interfaces.
str:cap()
Recreate a string with the first character in upper case.
str:decap()
Recreate a string with the first character in lower case.
str:escXML()
Escape str
for an XML attribute value or content.
hash = str:hash([CaseSensitive])
Return a hash value for the string. The CaseSensitive
parameter is optional and defaults to false
.
str:join(Table, Separator)
Join the contents of Table
into a single string, using Separator
between each item. If Separator
is not specified, the items are joined consecutively.
str:trim()
Trims whitespace from the left and right sides of a string.
str:rtrim()
Trims whitespace from the right side of a string.
str:split([Pattern])
Split a string using Pattern
to identify each valid word. If Pattern is not specified, it defaults to %s
.
str:startsWith(Cmp)
Returns true
if the string starts with Cmp
.
str:endsWith(Cmp)
Returns true
if the string ends with Cmp
.
Fluid supports a simplified threading model so as to minimise the potential problems occurring from their use. The functionality is as follows:
The script()
method compiles a statement string and executes it in a separate script state. The code may not directly share variables with its creator, but it can find existing objects and interact with them by calling obj.find()
.
The Callback
parameter is a function that will be executed once the threaded script has returned. It executes in the space of the caller and not the thread itself, which means it can access local variables safely.
thread.script([[
print('Thread is now in session.')
]],
function()
print('Thread has finished executing.')
end)
The thread.action()
and thread.method()
interfaces provide a convenient means of executing an object function in a separate thread. There is some overhead in executing a new thread, but this strategy becomes highly effective when used to perform lengthy processes such as the loading and parsing of data.
Here's an example that reads the first 1Kb of data from a file and prints the content:
function on_complete(Action, File, Error, Buffer)
print('Read string: ' .. Buffer)
File.free()
end
str_buffer = string.rep(1024)
thread.action(file, AC_Read, on_complete, str_buffer, str_buffer)
processing.sleep()
The Action
parameter is accepted as an action ID (fast) or a string (slower). The Key
makes it possible to pass a custom parameter to the callback, and can be used to pass multiple parameters if a Lua table is used.
The Callback
routine receives notification of the thread's completion, and in this example uses this as an opportunity to free the file object. This is a recommended pattern, and ensures that the target object is not destroyed while the thread is executing. Never declare the object value as local
.
Internally, callbacks are executed on the next message processing cycle and this is why a call to processing.sleep()
is used in this example.
The Error
parameter in the callback reflects the ERR
code returned by the action - but bear in mind the possibility that if thread preparation fails, the callback will never be executed and an exception will be thrown instead.
The structure interface is provided so that Fluid can use C/C++ structures declared in the Parasol API. You will find that many class and module API's use structures for returning condensed information.
Structures are created using the struct interface's new()
method. The prototype is as follows:
newstruct = struct.new(struct_def, [address])
The struct_def is a reference to a named structure definition declared in the Parasol API, or one returned from MAKESTRUCT()
. The address is an optional setting for linking a struct to an existing pointer rather than allocating an empty memory block for the structure. Example:
xmltag = struct.new('XMLTag', address)
It is possible to manually change the address pointer that is assigned to a struct - this saves on having to create a struct object for every address to be processed. Example:
oldaddress = xmltag.ptr(NewAddress)
To retrieve the current address:
address = xmltag.ptr()
Note that nil is returned if the address is set to zero.
The byte size of a structure can be retrieved with the structsize()
method, for example:
print('The size of the structure is ' .. xmltag.structsize())
To get the total number of fields in the structure, use #
, e.g. #xmltag
.
The MAKESTRUCT()
function is used to build structure definitions. A structure definition is a single string that defines all fields in a structure, matched in the order in which they appear in the structure. Consider the following C structure:
struct XMLTag {
int Index;
int ID;
XMLTag *Child;
XMLTag *Prev;
XMLTag *Next;
APTR Private;
XMLAttrib *Attrib;
int16_t TotalAttrib;
uint16_t Nest;
};
To define this structure in Fluid we would use the following code:
MAKESTRUCT('XMLTag', 'lIndex,lID,pChild,pPrev:XMLTag,pNext:XMLTag,pPrivate,pAttrib,wTotalAttrib,uwNest')
Notice that each field name is defined using the same order and names as identified in the structure. Each name is prefixed with a lower-case character that indicates the field type. Using the correct field types is the most crucial part of the structure definition and each must be chosen from the following table:
Character | Field Type |
---|---|
l | Integer (32-bit) |
d | Double (64-bit) |
x | Integer (64-bit) |
f | Float (32-bit) |
w | Integer (16-bit) |
c | Char (8-bit) |
p | Pointer. You can use ':StructName' as a suffix to reference other structures. |
s | String |
o | Object (Pointer) |
u | Unsigned (Use in conjunction with a type) |
Fixed arrays are also permitted in the structure definition if a field name is followed with enclosed square brackets that contain the array size, e.g. [12]
.
The regular expression interface provides compiled regex objects for high-performance pattern matching and text manipulation. Unlike Lua's basic pattern matching, the regex interface offers full PCRE-compatible regular expression support through compiled objects that can be created once and reused multiple times.
To create a compiled regex object, use the new()
method with the following prototype:
myregex = regex.new(Pattern, [Flags])
The Pattern
is a string containing the regular expression pattern to compile. The optional Flags
parameter can be used to modify regex behavior using the flag constants described below.
-- Simple pattern
local digits = regex.new('\\d+')
-- Case insensitive pattern
local email = regex.new('[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}', regex.ICASE)
If the provided pattern is invalid, an error will be raised. For this reason it is advisable to use catch()
when creating regex objects that use untested patterns, e.g. from user input.
The following flags can be combined to modify regex compilation:
Flag | Description |
---|---|
regex.ICASE |
Case insensitive matching |
regex.MULTILINE |
^ and $ match line boundaries |
regex.DOTALL |
. matches newlines |
regex.EXTENDED |
Allow whitespace and comments in patterns |
Flags can be combined using addition: regex.ICASE + regex.MULTILINE
.
Use the test()
method to perform boolean pattern matching:
result = myregex:test(text)
This returns true
if the pattern matches anywhere in the text, false
otherwise.
local phoneRegex = regex.new('\\d{3}-\\d{3}-\\d{4}')
if phoneRegex.test(userInput) then
print('Valid phone number format')
end
Use the match()
method to find the first match and return capture groups:
matches = myregex.match(text)
Returns a table containing the full match and any capture groups, or nil
if no match is found. Array indices start at 1 (full match), with capture groups at indices 2, 3, etc.
local urlRegex = regex.new('(https?)://([^/]+)(.*)')
local matches = urlRegex.match('https://example.com/path')
-- matches[1] = 'https://example.com/path' (full match)
-- matches[2] = 'https' (first capture group)
-- matches[3] = 'example.com' (second capture group)
-- matches[4] = '/path' (third capture group)
Use the matchAll()
method to find all matches in the text:
allMatches = myregex.matchAll(text)
Returns a table where each element is a match table (as returned by match()
).
local wordRegex = regex.new('(\\w+)')
local allWords = wordRegex.matchAll('hello world test')
-- allWords[1][1] = 'hello', allWords[1][2] = 'hello'
-- allWords[2][1] = 'world', allWords[2][2] = 'world'
-- allWords[3][1] = 'test', allWords[3][2] = 'test'
Use the replace()
method to replace the first occurrence:
result = myregex.replace(text, replacement)
Use the replaceAll()
method to replace all occurrences:
result = myregex.replaceAll(text, replacement)
Replacement strings support backreferences using $1
, $2
, etc., to reference capture groups.
local phoneRegex = regex.new('(\\d{3})-(\\d{3})-(\\d{4})')
local formatted = phoneRegex.replace('555-123-4567', '($1) $2-$3')
-- Result: '(555) 123-4567'
Use the split()
method to split text using the regex pattern as a delimiter:
parts = myregex.split(text)
Returns a table containing the split parts (empty strings are excluded).
local csvRegex = regex.new('\\s*,\\s*')
local fields = csvRegex.split('apple, banana, cherry')
-- fields[1] = 'apple', fields[2] = 'banana', fields[3] = 'cherry'
Compiled regex objects provide the following read-only properties:
pattern
: The original pattern stringflags
: The compilation flags usedvalid
: Boolean indicating if compilation succeedederror
: Error message if compilation failed (or nil
if valid)local regex = regex.new('[invalid', 0)
if not regex.valid then
print('Regex error: ' .. regex.error)
end
Regex objects are compiled once and can be reused many times, making them significantly more efficient than traditional string matching for complex patterns. Store frequently used regex objects in variables rather than recreating them:
-- Efficient: compile once, use many times
local emailValidator = regex.new('^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$')
for _, email in ipairs(emailList) do
if emailValidator.test(email) then
(email)
processEmailend
end
The array interface is used to manage the arrays that are returned by object fields, actions, methods and certain functions. Although Lua includes native support for arrays, their functionality is particular to the Lua environment. Arrays for use within the Parasol API must use the interface that we provide.
To create an array, use the new()
method with the following prototype:
myarray = array.new(Size, Type)
The Size
indicates the total number of entries in the array. The Type
is a string that declares the type to use for each element in the array. Supported types are byte
, word
, long
, large
, string
, ptr
, float
and double
.
Strings can also be converted into an array of bytes using the special bytestring
type, as in the following template:
myarray = array.new('enter your string here', 'bytestring')
Array elements can be retrived using the Lua syntax for array referencing:
var = myarray[20]
The total number of elements in the array, as defined on the array's creation, can be retrieved by using the '#' prefix:
total = #array
string = myarray.getstring(Start, [Length])
The getstring()
method is available for byte arrays only. It converts part of the array into a Lua string. If the Length
is unspecified then the full length of the array is retrieved as a string.
array.copy(Source, [DestIndex], [TotalElements])
The copy()
method is used to copy the data from Source
to the array, targeting DestIndex
. The Source
can be either a string
type or another array
.
The following functions are included as standard so that messages can be logged to the console.
msg()
accepts a single string parameter and prints it to the debug log. The log level must be set to api
or higher in order to be visible.
print()
accepts a single string parameter and prints it to stderr
. If stderr
is unavailable on a system like Android, the message is printed to the debug log instead.
For more precise control over printing, use io.write()
and target stderr
or stdout
instead of the print()
function.
Fluid programs can receive system-wide events whenever they are broadcast. Two functions are provided for the purpose of event management. The first is subscribeEvent(), which will connect a Fluid function to a specific event:
error, handle = subscribeEvent(eventname, function)
The eventname
is a string that must follow the format group.subgroup.name
, for example system.task.created
. Valid event strings are described in full detail in the Events Manual of the Parasol SDK. A single asterisk wildcard is allowed in the subgroup and/or name if listening to multiple events is desirable, for example system.*.*
would listen for all system events. The referenced function will receive two arguments if the event is signalled - EventID
and Args
. The Args
parameter is a table containing named parameters - if the event does not include any parameters then the table will be empty.
To unsubscribe from an event, call unsubscribeEvent() with the event handle that was returned by the initial subscribeEvent() call:
unsubscribeEvent(handle)
Fluid is developed by Paul Manias and runs on Mike Pall's LuaJIT framework, which in turn is based on the Lua programming language.
Lua is designed and implemented by a team at PUC-Rio, the Pontifical Catholic University of Rio de Janeiro in Brazil. Lua was born and raised at Tecgraf, the Computer Graphics Technology Group of PUC-Rio, and is now housed at Lua.org. Both Tecgraf and Lua.org are laboratories of the Department of Computer Science.