## Introduction Tiri is a [Lua-based](https://lua.org) scripting language built on top of [LuaJIT](https://luajit.org). 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. We have heavily customised our Lua implementation for full integration with Kōtuku, as well as extending it with many new language features that are often requested by Lua developers. This approach is known as a 'hard fork', meaning we do not maintain compatibility with Lua as a language. Tiri is fully interoperable with Kōtuku's C++ APIs due to a requirement that all interfaces are fully described. You can therefore read our online API documentation with confidence that the interfaces are universal, irrespective of language. This manual expands on the information in the existing [Lua Reference Manual](http://www.lua.org), and covers features that are exclusive to Tiri. The official Lua 5.1 Reference Manual is required reading if you are unfamiliar with the language, and a working knowledge of Lua is assumed from this point onward. ### Goals Tiri was designed with the following goals in mind: - **Laziness**: We want developers to be able to write code quickly and with minimal boilerplate. For instance, our type-inference-by-default approach allows variables to type-cast without explicit declarations. Features like safe navigation and short-hand operators are available to reduce verbosity. - **Simplicity**: The language should be easy to learn and understand, with a minimal set of core concepts. Operator tokens and keywords have been carefully chosen to maximise readability and keep the parsing process simplified. - **Intuitiveness**: The syntax and semantics should be logical and consistent. Lua features that weren't aligned with our changes have been removed. - **Interoperability**: It must work seamlessly with existing C++ APIs and libraries to leverage the power of Kōtuku. - **Performance**: Many of our changes to the LuaJIT base code have been made to maximise the efficiency of code produced by the compiler and JIT recorder. Some language features like the introduction of arrays allow for faster processing of sequential data. The new type system enables more optimised code paths to be generated in conjunction with the added type safety guarantees. Some of Tiri's best features are under the hood and out of sight - but they can be felt in day to day usage and are clear in performance comparisons to other scripting languages. ## Table of Contents - [Resources](#resources) - [Usage](#usage) - [File Recognition](#file-recognition) - [Lua Extensions](#lua-extensions) - [Breaking Changes](#breaking-changes) - [Bitwise Operations](#bitwise-operations) - [Bitshift Operators](#bitshift-operators) - [Unicode Operators](#unicode-operators) - [Script Parameters](#script-parameters) - [Compound Assignment Operators](#compound-assignment-operators) - [Concatenation Assignment](#concatenation-assignment) - [String Interpolation](#string-interpolation) - [Postfix Increment](#postfix-increment) - [Arrow Functions](#arrow-functions) - [Continue Statement](#continue-statement) - [To-Be-Closed Variables](#to-be-closed-variables) - [Constant Variables](#constant-variables) - [Enum Declarations](#enum-declarations) - [Safe Navigation Operator](#safe-navigation-operator) - [Type Annotations](#type-annotations) - [Sticky Types (Type Inference)](#sticky-types-type-inference) - [Implicit Type Fixing](#implicit-type-fixing) - [Explicit Type Annotations](#explicit-type-annotations) - [The Any Type](#the-any-type) - [Nil Semantics](#nil-semantics) - [Function Parameters](#function-parameters) - [Function Return Types](#function-return-types) - [Variable Scoping](#variable-scoping) - [Local Variables](#local-variables) - [Global Variables](#global-variables) - [Migration from Standard Lua](#migration-from-standard-lua) - [Result Management](#result-management) - [Blank Identifier](#blank-identifier) - [Pipe Operator](#pipe-operator) - [Pipe Iteration with Ranges](#pipe-iteration-with-ranges) - [Result Filter Operator](#result-filter-operator) - [Exception Handling](#exception-handling) - [Try-Except Statement](#try-except-statement) - [Raising Exceptions](#raising-exceptions) - [Deference](#deference) - [Defer Statement](#defer-statement) - [Deferred Expressions](#deferred-expressions) - [Thunks](#thunks) - [Equality Operators](#equality-operators) - [Approximate Equality](#approximate-equality) - [If-Empty Logical Operator](#if-empty-logical-operator) - [If-Empty Postfix Operator](#if-empty-postfix-operator) - [Ternary Operator](#ternary-operator) - [Ranges](#ranges) - [Range Constructor](#range-constructor) - [Range Literals](#range-literals) - [Iteration with Ranges](#iteration-with-ranges) - [range.slice()](#rangeslice) - [Functional Iteration with each()](#functional-iteration-with-each) - [Functional Range Methods](#functional-range-methods) - [String Slicing with Ranges](#string-slicing-with-ranges) - [Table Slicing with Ranges](#table-slicing-with-ranges) - [Membership Testing](#membership-testing) - [Choose Expressions](#choose-expressions) - [Script Management](#script-management) - [include](#include) - [import](#import) - [Module Interface](#module-interface) - [Type Conversion](#type-conversion) - [Buffer Handling](#buffer-handling) - [Multiple Result Values](#multiple-result-values) - [Object Interface](#object-interface) - [Object Creation](#object-creation) - [Object Relationships](#object-relationships) - [Accessing Objects](#accessing-objects) - [Garbage Collection](#garbage-collection) - [Action and Method Calls](#action-and-method-calls) - [Field Interface](#field-interface) - [Action & Method Subscriptions](#action--method-subscriptions) - [Processing Interface](#processing-interface) - [Signals](#signals) - [Garbage Collector](#garbage-collector) - [Math Interface](#math-interface) - [Strings Interface](#strings-interface) - [Thread Interface](#thread-interface) - [Structure Interface](#structure-interface) - [Creating a Structure](#creating-a-structure) - [Additional Functionality](#additional-functionality) - [Custom Structures](#custom-structures) - [Regular Expression Interface](#regular-expression-interface) - [Creating a Regex](#creating-a-regex) - [Regex Properties](#regex-properties) - [Performance Considerations](#performance-considerations) - [Array Interface](#array-interface) - [Array Manipulation Methods](#array-manipulation-methods) - [Array Navigation Methods](#array-navigation-methods) - [Array Data Extraction Methods](#array-data-extraction-methods) - [Array Functional Methods](#array-functional-methods) - [Logging](#logging) - [Annotations](#annotations) - [Parser Annotation Expressions](#parser-annotation-expressions) - [Annotation Syntax](#annotation-syntax) - [Documentation Annotation](#documentation-annotation) - [Compile-Time Pre-Processor](#compile-time-pre-processor) - [Standard Annotations](#standard-annotations) - [Test Framework Annotations](#test-framework-annotations) - [debug.fileSources Interface](#debugfilesources-interface) - [debug.anno Interface](#debuganno-interface) - [Event Subsystem](#event-subsystem) - [Credits](#credits) ## Resources For more information on the usage of available classes and modules, please refer to the Kōtuku API at [www.kotuku.dev](https://www.kotuku.dev). For general information on the syntax provided by Lua, please read the following online manuals: * [www.lua.org/manual/](http://www.lua.org/manual/) * [www.lua.org/pil/](http://www.lua.org/pil/) * [lua-users.org/wiki/SampleCode](http://lua-users.org/wiki/SampleCode) --- ## Usage To run a Tiri script, use the Origo executable: `origo myfile.tiri` Named arguments can be passed to the Tiri script by following the script location with a series of variable values: `origo myfile.tiri name='John' surname=Bloggs` To debug a Tiri script, use the `--log-api` parameter. For further information on available options, execute Origo with the `--help` parameter. ### File Recognition Tiri files can use the file extension `.lua` or `.tiri` for identification. Ideally, scripts should start with the comment `-- $TIRI` near the start of the document so that they can be correctly identified by the Tiri parser. --- ## Lua Extensions A number of extensions have been added (and some features removed) to Lua 5.2 in order to add value to the Tiri language. This section examines the major changes in regards to the published Lua Reference Manual. ### Breaking Changes The Package and OS libraries normally found in Lua are removed as they duplicate features already found in Kōtuku's Script, File and Time classes. The introspective debug library is also removed for the purpose of reducing size. The following Lua/LuaJIT features are unsupported or modified: - `~=` not equal operator; replaced with `!=` - `==` equality operator; replaced with `is` - `goto` and `::label::` statements; superceded by `continue`, `break`, `defer` - `bit.*` library; replaced with native bitwise operators - `select()`; replaced with result masks `[_*]` - `loadstring()`; replaced with `load()` - `dofile()`; replaced with `loadFile()` - `pcall()` and `xpcall()`; replaced with `try … except` - `getfenv()`, `setfenv()`, `gcinfo()`, `table.maxn()`, `unpack()`; removed - `string.gsub()`, `string.match()`, `string.gmatch()`; removed - `string.sub()`; renamed to `string.substr()` - `io.*` interfaces now throw exceptions on error - One-based indexing for tables, strings and arrays; replaced with zero-based indexing - Variables are local by default; use the `global` keyword for globals - Arrays are a distinct and performance enhanced type; avoid using tables for sequential data storage - Lua patterns deprecated in favour of regex patterns ### Bitwise Operations Tiri supports C-style bitwise operators on 32-bit integers: - `~x` bitwise NOT - `a & b` bitwise AND - `a | b` bitwise OR - `a ^ b` bitwise XOR Results follow two's-complement 32-bit integer behaviour. Compound assignment variants are not supported: `&=`, `|=`, `^=` (bitwise) do not exist. Use `x = x & y` and `x = x | y`. Examples: ```lua -- Bitwise NOT, shifts left by 2 result = ~a << 2 -- Bitwise AND, then shifts right logical by 1 result = (a & b) >> 1 -- Masks out the lower 8 bits result = a & 0xFF ``` ### Flag Testing Operator The `has` operator tests whether a bitwise flag is set: - `a has b` is equivalent to `a & b != 0` The result is always a boolean. Both operands must be numeric. Examples: ```lua permissions = PERMIT_READ | PERMIT_WRITE if permissions has PERMIT_READ then print('Readable') end ``` When both operands are constants, the expression is folded at compile time. ### Bitshift Operators Tiri adds infix bitshift operators for convenience: - `<<` left shift - `>>` right shift Precedence and associativity: - Shifts bind tighter than addition and subtraction, e.g. `1 + 1 << 3` evaluates as `1 + (1 << 3)` producing `9`. - The right-hand side of a shift is parsed as a full expression. For example, `8 >> 1 + 1` produces `2`. - Shifts are left-associative when chained: `x << y << z` parses as `(x << y) << z`. ### Unicode Operators Tiri supports Unicode alternatives for some multi-character ASCII operators as well as Unicode arithmetic operators. These provide a cleaner visual appearance in editors with good Unicode font support. #### Comparison and Logical Operators |Unicode|ASCII|Description| |-|-|-| |`≠`|`!=`|Not equal| |`≈`|None|Approximate equality| |`≤`|`<=`|Less than or equal| |`≥`|`>=`|Greater than or equal| |`«`|`<<`|Left shift| |`»`|`>>`|Right shift| |`‥`|`..`|Concatenation| |`⁇`|`??`|Null coalescing (if-empty)| |`▷`|`:>`|Ternary separator| |`⧺`|`++`|Increment| #### Arithmetic Operators |Unicode|ASCII|Description| |-|-|-| |`×`|`*`|Multiplication| |`÷`|`/`|Division| |`↑`|`**`|Exponentiation (right-associative)| Examples: ```lua -- Comparison and logical operators x = 5 x⧺ -- increment name = user ⁇ 'Anonymous' -- null coalescing max = a ≥ b ? a ▷ b -- comparison with ternary msg = 'Hello' ‥ ' World' -- concatenation flags = 1 « 4 -- left shift -- Arithmetic operators result = 10 × 5 ÷ 2 -- 25 (multiplication and division) area = radius × radius × PI -- circle area ``` Both ASCII and Unicode forms can be mixed freely in the same source file: ```lua x = 5 * 3 × 2 ÷ 1 / 2 -- All forms work together ``` ### Script Parameters Arguments passed to the Tiri 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. ### Compound Assignment Operators Tiri adds C-style compound assignment operators for convenience: - `+=`, `-=`, `*=`, `/=`, `%=` on numeric values - `..=` for string concatenation RHS behaviour: - The right-hand side is evaluated exactly once. - If the RHS is a function call or vararg, only the first return value is used. Errors and types: - Using a non-assignable left-hand side (e.g. a constant or a function call result) is an error. ### Concatenation Assignment The `..=` operator appends to an existing string: ```lua s = 'a' s ..= 'bc' -- s is 'abc' s ..= tostring(1) -- s is 'abc1' ``` Semantics mirror `s = s .. rhs`. The left-hand side is evaluated once and the RHS uses only its first return value. ### String Interpolation Tiri supports Python-style f-strings for embedding expressions directly within string literals. F-strings are prefixed with `f` and use curly braces `{}` to delimit expressions. **Basic Syntax:** ```lua name = "World" greeting = f"Hello {name}" -- "Hello World" a = 10 b = 20 result = f'{a} + {b} = {a + b}' -- '10 + 20 = 30' ``` Both single and double quotes are supported. **Expression Support:** Any valid Tiri expression can be used inside the braces: ```lua -- Arithmetic f"Result: {1 + 2 * 3}" -- "Result: 7" -- Function calls f"Upper: {string.upper('hello')}" -- "Upper: HELLO" -- Table field access user = {name = "Alice", age = 30} f"{user.name} is {user.age}" -- "Alice is 30" -- Method calls f"Name: {obj:getName()}" -- Nested tables in expressions f"Point: {point.x}, {point.y}" -- Nil handling with ?? f"Your name is {user.name ?? 'unknown'}" ``` It is strongly recommended that nil handling is always employed with the `??` operator if the data originates from outside the script. **Automatic Type Conversion:** All interpolated expressions are automatically wrapped in `tostring()`, ensuring proper conversion of any value type: ```lua f"{nil}" -- "nil" f"{true}" -- "true" f"{42}" -- "42" f"{3.14}" -- "3.14" ``` **Escaping Braces:** To include literal braces in the output, double them: ```lua f"Use {{braces}} for interpolation" -- "Use {braces} for interpolation" ``` **Restrictions:** - Long string syntax is not supported: `f[[...]]` is invalid - Empty expressions `{}` or whitespace-only expressions `{ }` are syntax errors ### Postfix Increment Tiri supports a postfix increment operator for convenience: ```lua counter++ obj.field++ t[i]++ ``` Notes: - Postfix only; there is no prefix form. - The operator mutates the target by adding 1. Use only on assignable values (locals, upvalues, globals, fields, indexed elements). - Intended for statement use. The value of the `x++` expression itself is unspecified and should not be relied upon in expressions. ### Arrow Functions `=>` provides concise anonymous function syntax. Single-expression bodies are implicitly returned; multi-statement bodies use `do ... end` with an explicit `return`. - Single identifiers do not need parentheses: `value => value * 2` - Multiple parameters must be parenthesised: `(left, right) => left + right` - Use empty parentheses for no parameters: `() => 42` - Varargs are not supported; use `function(...)` when needed. Examples: ```lua double = n => n * 2 adder = (a, b) => a + b on_click = () => do print('Clicked') return true end numbers = {1 to 10} |> map(i => i * 3) |> filter(i => i > 10) ``` Arrow bodies bind loosely, so the expression after `=>` extends as far right as possible. ### Continue Statement Tiri adds a `continue` statement for all loop forms: ```lua for {1 to 10} do if i % 2 is 0 then continue end -- odd numbers only end while cond do if skip() then continue end work() end repeat if not ready() then continue end until done ``` `continue` skips the remainder of the current loop body and advances to the next iteration. In `repeat … until`, it jumps to the condition check. ### To-Be-Closed Variables The `` attribute marks local variables for automatic cleanup via the `__close` metamethod when scope exits. This is more optimal than manually calling the garbage collector. ```lua resource = acquire_resource() -- resource.__close(resource, nil) called automatically when scope ends ``` The `__close` metamethod receives two arguments: the object being closed and an error value (`nil` for normal exit, the error object during error unwinding). **Execution order:** - Close handlers run before `defer` blocks (both in LIFO order) - Triggered on normal scope exit, `return`, `break`, `continue`, and error unwinding **Error propagation:** When an error is thrown and caught by `try`, the `__close` handler receives the error as its second argument, enabling proper error-aware cleanup. **Primitive values:** Values without metatables (`nil`, `false`, strings, numbers) are safely ignored. **Error handling:** For cleanup that needs to know about errors, check the second argument: ```lua try file = open_file() risky_operation() -- If this throws, file.__close still runs end ``` ### Constant Variables The `` attribute marks local or global variables as constant, preventing reassignment after initialisation. This provides compile-time enforcement of immutability for variable bindings. **Syntax:** ```lua local max_size = 100 local prefix :str = "test_" global DEBUG_MODE = true ``` The `` attribute can appear before or after a type annotation: ```lua local value :num = 42 -- Attribute before type local value:num = 42 -- Type before attribute (also valid) ``` **Initialiser requirement:** Const variables must be initialised at declaration. Declaring a const without an initialiser is a compile-time error: ```lua local x -- Error: const 'x' requires an initialiser ``` **Reassignment prevention:** Attempting to reassign a const variable produces a compile-time error: ```lua local x = 1 x = 2 -- Error: cannot assign to const local 'x' global CONFIG = {} CONFIG = {} -- Error: cannot assign to const global 'CONFIG' ``` **Table contents are mutable:** The `` attribute protects the variable binding, not the contents of the value. Table and object contents can still be modified: ```lua local data = { x = 1, y = 2 } data.x = 999 -- Valid: modifying table contents data.z = 3 -- Valid: adding new fields data = {} -- Error: cannot reassign the binding ``` **Multiple declarations:** In multiple variable declarations, each variable can independently have the `` attribute: ```lua local a , b, c = 1, 2, 3 -- a and c are const, b is mutable b = 20 -- Valid a = 10 -- Error: cannot assign to const local 'a' ``` **Scope behaviour:** Const variables follow normal scoping rules. A const in an outer scope can be shadowed by a new variable in an inner scope: ```lua local x = 1 do local x = 2 -- Valid: shadows outer const x = 3 -- Valid: inner x is not const end -- Outer x is still 1 and still const ``` ### Enum Declarations Enum declarations generate a group of compile-time numeric constants from a shared prefix. Their use is favoured over constant variable declarations, as the parser can replace enum references with their numeric value for efficiency. The enum prefix is used only to build the generated names; it does not create a runtime table, variable, or value. **Syntax:** ```lua [global] enum PREFIX { MEMBER = 0, NEXT_MEMBER, } ``` Each generated constant is named `PREFIX_MEMBER` and is substituted by the parser as a numeric constant. For example: ```lua enum HTTP_METHOD { GET = 0, POST, PUT, DELETE, } assert(HTTP_METHOD_GET is 0) assert(HTTP_METHOD_PUT is 2) ``` The bare and `global` forms are equivalent: ```lua enum MODE { FAST, SLOW } global enum ERR { OKAY = 0, FAIL } ``` If the first member has no explicit value, it starts at `0`. Later members without explicit values increment from the previous member value: ```lua enum SAMPLE { FIRST, -- SAMPLE_FIRST = 0 SECOND, -- SAMPLE_SECOND = 1 CUSTOM = 10, -- SAMPLE_CUSTOM = 10 AFTER, -- SAMPLE_AFTER = 11 } ``` Explicit values must be integer literals. Signed decimal and hexadecimal forms are accepted: ```lua enum MASK { NONE = 0x0, READ = 0x1, WRITE = 0x2, ALL = 0x3, } ``` #### Restrictions Enum declarations must appear at the top-level scope of a script or imported file. They are not permitted inside functions or statement blocks, and `local enum` is invalid. Enum prefixes and member names must use uppercase identifier style. The declaration must contain at least one member and cannot use a prefix type annotation, ``, or `` attribute. The `enum` word is reserved and cannot be used as an identifier. ## Safe Navigation Operator The safe navigation operator (`?.`) provides null-safe access to object fields, methods, and indexes. ### Syntax ```lua obj?.field -- Safe field access obj?.method() -- Safe method call obj?[key] -- Safe index access obj?.a?.b?.c -- Chaining ``` ### Behaviour If the object is `nil`, the safe navigation operator returns `nil` without attempting to access the field/method/index. This prevents "attempt to index a nil value" errors. **Important:** The safe navigation operator only checks for `nil`. Other falsey values like `false`, `0`, or `""` are treated as valid objects and field access proceeds normally. ### Examples ```lua -- Safe field access user = nil name = user?.name -- Returns nil instead of error user2 = { name = "Alice" } name2 = user2?.name -- Returns "Alice" -- Chaining city = user?.profile?.address?.city -- Returns nil if any level is nil -- With default values using if-empty operator displayName = user?.name ?? "Guest" -- "Guest" if user or name is nil -- Safe method calls result = obj?.calculate() -- Returns nil if obj or calculate() is nil -- Multiple return values preserved a, b = obj?.getTwoValues() -- Both a and b will be nil if obj or getTwoValues() is nil -- Safe index access value = table?[key] -- Returns nil if table is nil ``` ## Type Annotations Tiri supports optional type annotations on function parameters to surface static analysis diagnostics during parsing. Annotations follow the parameter name after a colon and constrain the expected argument type. ### Syntax ```lua function process(Path:str, Count:num, Options:table) -- Path must be a string -- Count must be a number -- Options must be a table end ``` Untyped parameters omit the annotation: ```lua function mixed(Untyped, Typed:bool): return Untyped, Typed end ``` ### Supported Type Names |Name|Notes| |-|-| |`any`|Accepts any type| |`nil`|Explicit nil| |`num`|Numeric values| |`str`|Text strings| |`bool`|Boolean values| |`table`|Tables and dictionaries| |`func`|Callable values| |`thread`|Coroutines| |`obj`|Kōtuku objects (userdata)| Unknown type names raise diagnostics during parsing with the `UnknownTypeName` error code. --- ## Sticky Types (Type Inference) Tiri employs a lazy type inference system that commits local variables to a specific type upon their first meaningful assignment. This approach balances the convenience of dynamic typing with the safety and performance benefits of static type knowledge. Unlike traditional static type systems that require explicit annotations, sticky types work transparently: - Typeless variables are automatically fixed to the type of their first non-nil value. - The `nil` value acts as a placeholder that does not commit the type. - Explicit type annotations are optional, primarily useful for pre-declarations or guarding function parameters. - The `any` type annotation opts out of type fixing for variables that genuinely need variant behaviour. **Benefits:** 1. **Error detection**: Type mismatches are reported as compile-time errors, catching bugs before execution 2. **Code clarity**: Reading code becomes easier when variables maintain consistent types 3. **Optimisation potential**: The JIT can generate more efficient code when variable types are known to be stable ### Implicit Type Fixing When a local variable is assigned a non-nil value, its type becomes fixed to that value's type: ```lua local count = 0 -- count is fixed to 'number' count = 10 -- Valid count = "ten" -- Error: cannot assign 'string' to variable of type 'number' name = "Alice" -- name is fixed to 'string' name = "Bob" -- Valid name = 42 -- Error: cannot assign 'number' to variable of type 'string' global glItems = {} -- glItems is fixed to 'table' glItems = { a = 1 } -- Valid glItems = "list" -- Error: cannot assign 'string' to variable of type 'table' ``` This behaviour is automatic and requires no additional syntax. The compiler infers the type from the first assignment and enforces consistency thereafter. ### Explicit Type Annotations Type annotations can be added to local variable declarations using the `:type` syntax. This is useful for pre-declaring variables or documenting intent: ```lua local limit: num -- pre-declared as number, starts as nil limit = 100 -- Valid limit = "high" -- Error: cannot assign 'string' to variable of type 'number' local message: str = "" -- pre-declared as string with initial value message = "hello" -- Valid message = nil -- Clears the value (type remains 'str') message = 42 -- Error: cannot assign 'number' to variable of type 'string' ``` Explicit annotations also catch type mismatches in the initial value: ```lua local count: num = "text" -- Error: cannot assign 'string' to variable of type 'number' ``` The supported type names are: `any`, `nil`, `bool` (or `boolean`), `num` (or `number`), `str` (or `string`), `table`, `array`, `func` (or `function`), `obj` (or `object`). ### The Any Type Use the `any` type annotation to preserve traditional dynamic typing for variables that genuinely need to hold different types: ```lua variant: any = 0 -- Explicitly variant, all further assignments are valid variant = "now a string" variant = { key = true } variant = nil variant = load_json() ``` The `any` type is the escape hatch when you need flexibility. However it does come with disadvantages: 1. It disables the type safety benefits for that variable; 2. The JIT compiler has fewer optimisation opportunities when dealing with variant types. ### Nil Semantics The `nil` value has special status in the type system, allowing variables to be cleared to their empty state regardless of their type. **Uninitialised variables**: Variables declared without an initial value start as nil with no type commitment. The type is fixed when the first non-nil value is assigned: ```lua local result -- result is nil (type uncommitted) result = nil -- still uncommitted result = calculate() -- result is now fixed to whatever calculate() returns result = "fallback" -- Error if calculate() returned a non-string type ``` **Nil as a clear operation**: Once a variable has a fixed type, assigning nil clears the value but preserves the type constraint. The next non-nil assignment must still match the fixed type: ```lua name = "Alice" -- Assign a string to new variable 'name' name = nil -- Clear the value name = "Bob" -- Valid name = 42 -- Error: Cannot assign 'number' to variable of type 'string' ``` This design allows variables to represent "optional" or "nullable" values naturally without requiring a separate nullable type annotation, while still enforcing type consistency for non-nil values. ### Function Parameters Function parameter type annotations serve as guards that validate incoming arguments. Unlike local variable type fixing (which is lazy), parameter annotations are checked at call-time: ```lua function greet(Name: str, Times: num) for i in {1 into Times} do print("Hello, " .. Name) end end greet("World", 3) -- OK greet(42, 3) -- ERROR: expected 'string' for parameter 'Name' ``` Parameter annotations are particularly important for public APIs and library functions where input validation is critical. They provide documentation and runtime safety at the function boundary. **Mixing typed and untyped parameters:** ```lua function process(Data, Count: num, Validate: bool) -- Data accepts any type (no annotation) -- Count must be a number -- Validate must be a boolean end ``` Untyped parameters remain fully dynamic and accept any value, preserving flexibility where needed. ### Function Return Types Tiri supports optional return type declarations on functions to enable compile-time validation of returned values. Additionally, more optimal code can be generated through the narrowing of return types. Return types are declared after the parameter list using a colon followed by the type (or a tuple of types for multiple returns). **Single return type:** ```lua function calculate_area(Radius: num): num return math.pi * Radius * Radius end function get_name(): str return "Alice" end ``` **Multiple return types:** Functions that return multiple values can declare each type in angle brackets: ```lua function divide(A: num, B: num): return math.floor(A / B), A % B -- quotient and remainder end function parse_header(Line: str): -- Returns name, value, and position return name, value, pos end ``` **Variadic return types:** When the last return value can repeat (e.g., returning variable numbers of values), use `...` after the last type: ```lua function get_values(): return 1, 2, 3, 4, 5 -- First is num, rest are also num end ``` #### Return Type Inference When no explicit return type is declared, Tiri infers the return type from the function's `return` statements using a "first-wins" rule: 1. The first `return` statement with a non-nil value establishes the expected type(s) 2. Subsequent `return` statements must be consistent with the established types 3. Returning `nil` is always permitted (it acts as "no value" and doesn't establish a type) ```lua function find_user(Id: num) user = database.lookup(Id) if not user then return nil end -- nil doesn't establish a type return user -- First non-nil return: fixes type to whatever user is end function get_status(Code: num) if Code < 0 then return nil end -- nil is allowed if Code is 0 then return "OK" end -- Establishes type as 'str' return "Error" -- Must also be 'str' end ``` **Type mismatch errors:** ```lua function broken() if condition then return "text" -- Establishes type as 'str' end return 42 -- Error: inconsistent return type, expected 'str', got 'num' end ``` #### The `any` Return Type Use `any` to opt out of return type checking for functions that genuinely return different types: ```lua function json_decode(Text: str): -- Returns decoded value (could be table, string, number, bool, or nil) -- and the position after parsing return decoded_value, pos end function dynamic_result(Mode: str): any if Mode is "number" then return 42 end if Mode is "string" then return "hello" end return { key = "value" } end ``` #### Recursive Functions Recursive functions (functions that call themselves) require explicit return type declarations. This is because the type inference cannot determine the return type before analysing the function body, which contains the recursive call: ```lua -- Error: recursive function 'factorial' must have explicit return type declaration function factorial(N: num) if N <= 1 then return 1 end return N * factorial(N - 1) end -- Correct: explicit return type declared function factorial(N: num): num if N <= 1 then return 1 end return N * factorial(N - 1) end ``` This requirement also applies to mutually recursive functions (function A calls function B, which calls function A). #### Return Type Benefits Declaring return types provides several advantages: 1. **Documentation**: Return types serve as built-in documentation for function contracts 2. **Error detection**: Type mismatches in return statements are caught at parse time 3. **Call-site inference**: When a function has declared return types, variables assigned from calls can infer their types 4. **Optimisation**: The compiler will generate faster code if the return types are narrower than `any`. ```lua function get_count(): num return 42 end result = get_count() -- 'result' is automatically inferred as 'num' result = "text" -- Error: cannot assign 'str' to variable of type 'num' ``` --- ## Variable Scoping Tiri enforces local-by-default variable scoping, a significant departure from standard Lua where undeclared variables are implicitly global. This design prevents accidental pollution of the global namespace and catches common programming errors at parse time. ### Local Variables Variables and functions are local by default. Any assignment to an undeclared variable creates a new local in the current scope: ```lua counter = 0 -- Creates local 'counter' name = "Alice" -- Creates local 'name' bare_var -- Invalid, results in an error from the parser a, b, c -- Invalid: use 'local a, b, c' or an assignment instead function example() -- Function is local total = 100 -- Creates local 'total' in function scope counter += 1 -- Modifies 'counter' in the outer scope end ``` The `local` keyword remains available for explicit declarations and is required when initialising multiple variables on one line: ```lua local a, b, c = 1, 2, 3 -- Multiple locals on one line local config -- Explicit nil initialisation ``` ### Global Variables To create or access global variables, use the `global` keyword. It is recommended that global declarations appear before any reference to the variable: ```lua global DEBUG_MODE = true global APP_VERSION global function configure() -- Global function is accessible in the parent context APP_VERSION = "1.0" -- Assigns to the global and makes it immutable global LATE_CREATION = true -- Valid, but won't exist until this function runs global DEBUG_MODE = false -- Modifies the original global without shadowing it local DEBUG_MODE = true -- Valid; this local shadows the global within this scope print(DEBUG_MODE) -- Prints local 'true' end ``` It is recommended that global variables follow our naming conventions, which are UPPER_CASE for constants and `glCamelCase` for mutable globals. Exceptions may apply - but sticking to conventions helps with code readability. **Global Functions:** Declaring a function with the `global` keyword allows the parent scope to access it. This feature allows library scripts to expose functions and variables to the caller. **Global Declaration Rules:** - `global` declarations must precede any use of the variable name - A local can shadow a global with the same name if explicitly marked `local` - Global variables are accessible from nested functions without re-declaration - The `global` keyword can appear at any scope level, but the variable becomes globally visible --- ## Result Management ### Blank Identifier The blank identifier `_` allows you to explicitly ignore values in assignments and loop variables: ```lua -- Ignore error from function call file, _ := openFile("data.txt") -- Ignore multiple return values _, _, result := getValues() -- Loop without index for _, value in ipairs(items) do print(value) end -- Works with pairs() too for _, v in pairs(table) do process(v) end -- Multiple positions local x, _, y = 1, 2, 3 -- x=1, y=3 ``` Notes: - The blank identifier does not allocate a variable or consume a register - Values are still consumed from the right-hand side for proper stack management - Can be used multiple times in the same statement - Cannot be read as a variable (e.g., `local x = _` is an error) ### Pipe Operator The pipe operator (`|>`) provides a functional programming style for chaining function calls. It passes the result of the left-hand side expression as the first argument(s) to the right-hand side function call. **Basic Syntax:** ```lua result = expression |> function_call() ``` The expression on the left is evaluated first, and its result is prepended to the arguments of the function call on the right. This allows for readable left-to-right data flow instead of deeply nested function calls. **Multi-Value Support:** When the left-hand side returns multiple values (e.g., from a function call), all values are forwarded as arguments: ```lua local function get_bounds() return 10, 20 end local result = get_bounds() |> math.max() -- math.max(10, 20) = 20 ``` **Result Limiting:** Use `|N>` syntax to limit the number of return values forwarded from the left-hand side: ```lua local function get_many() return 1, 2, 3, 4, 5 end local result = get_many() |2> math.max() -- math.max(1, 2) = 2 ``` **Chaining:** Pipes can be chained for multi-step transformations: ```lua local function double(x) return x * 2 end local function square(x) return x * x end local result = 3 |> double() |> square() -- square(double(3)) = 36 ``` **Real-World Examples:** ```lua -- Data transformation pipeline local function load_config(path) return [*_]obj.new('file', { path = path, flags = '!READ' }) end local function parse_json(file) local content = [_*]file.acRead() return json.decode(content) end local function validate(config) assert(config.version, "Missing version") return config end local config = "config.json" |> load_config() |> parse_json() |> validate() ``` ```lua -- Processing user input with additional arguments local function clamp(value, min, max) return math.max(min, math.min(max, value)) end local safe_value = tonumber(arg('value', '50')) |> clamp(0, 100) ``` **Notes:** - The right-hand side must be a function call; `x |> 5` is a syntax error - Pipe has higher precedence than logical operators (`and`, `or`) but lower than comparison operators - Right-associative: `a |> b() |> c()` evaluates as `a |> (b() |> c())` - For method calls, use full syntax: `obj |> obj:method()` (not `obj |> method()`) #### Pipe Iteration with Ranges When the left-hand side of a pipe is a range literal, the pipe automatically iterates, calling the right-hand function for each value: ```lua {1 to 5} |> print -- Prints 1, 2, 3, 4 {1 into 5} |> print -- Prints 1, 2, 3, 4, 5 -- With anonymous function {0 to 10} |> function(i) if i % 2 is 0 then print(i, 'is even') end end ``` The pipe returns the original range, enabling chaining: ```lua {1 to 10} |> i => log('Processing', i) |> i => validate(i) |> i => store(i) ``` Return `false` from the callback to terminate early: ```lua {1 to 1000} |> i => do if found_target(i) then print('Found at', i) return false -- Stop iterating end end ``` **Pipe Iteration Notes:** - Only range literals (`{start to stop}`) trigger automatic iteration at parse time - For ranges stored in variables, use the `:each()` method directly: `r:each(func)` - Chained pipes continue iteration: `{1 to 5} |> func1 |> func2` calls both functions for each value ### Result Filter Operator The result filter operator (`[mask]`) provides selective extraction of return values from multi-value function calls. It uses a prefix bracket syntax with a mask of `_` (drop) and `*` (keep) characters to specify which values to retain. **Basic Syntax:** ```lua result = [mask]function_call() ``` The mask is placed inside square brackets immediately before a function call. Each character in the mask corresponds to a return value position: - `_` drops the value at that position - `*` keeps the value at that position - The last character determines the behaviour for any excess values beyond those explicitly specified **Examples:** ```lua function multi() return 1, 2, 3, 4, 5 end -- Keep all values (default behaviour) local a, b, c, d, e = [*]multi() -- a=1, b=2, c=3, d=4, e=5 -- Drop first value, keep the rest local a, b, c, d = [_*]multi() -- a=2, b=3, c=4, d=5 -- Drop first two values, keep the rest local a, b, c = [__*]multi() -- a=3, b=4, c=5 -- Keep first value only, drop all others local a, b = [*_]multi() -- a=1, b=nil -- Drop first, keep second, drop the rest local a, b = [_*_]multi() -- a=2, b=nil -- Keep values at positions 2, 3, 4 only local a, b, c, d = [_***_]multi() -- a=2, b=3, c=4, d=nil -- Keep second and fourth values onwards local a, b, c = [_*_*]multi() -- a=2, b=4, c=5 -- Empty filter drops all values local x = []multi() -- x=nil ``` **With Method Calls:** ```lua obj = { method = function(self) return 10, 20, 30 end } second = [_*]obj:method() -- second=20 ``` **Use Cases:** The result filter is particularly useful when: - A function returns an error code as the first value that you want to ignore - You only need specific return values from a multi-value function - You want to skip metadata or status values and get directly to the data ```lua -- Skip error code, get file content directly content = [_*]file.acRead() -- Get only the second and third return values local b, c = [_**_]get_stats() ``` **Notes:** - The mask must be followed by a function call; `[_*]variable` is a syntax error - Maximum mask length is 64 positions --- ## Exception Handling Tiri supports an exception handling mechanism that recognises that errors can come in one of two forms: - Internal errors, as generated by the Tiri language and the runtime engine. - External errors, as generated by system API calls. In normal operation, these distinct patterns are handled differently, with external errors requiring manual handling. We designed the exception handling system to resolve this problem by unifying all error forms, providing a simple syntax for error management, and supporting exception handlers to deal with errors when they arise. ### Try-Except Statement Structured exception handling is provided through the `try-except` syntax, and is the primary means offered for exception management. **Features:** * Lazy syntax is permitted. * Try blocks can be nested, with each level catching its own exceptions. * Multiple except handlers with their own filters for both system and user error codes. * Support for control flow statements (`return`, `break`, `continue`) within try blocks. * Optional stack trace capture for debugging. * Very fast performance compared to the Lua `pcall` approach, especially when JIT compiled. **Basic Syntax:** ```lua try risky_operation() [ except [e] [ when Exception, ... ] ] print("Error: " .. e.message) [ except... ] [ success ] end ``` The `except` blocks catch exceptions raised within the preceding `try` block. Multiple `except` handlers are permitted, each with their own optional filter using the `when` clause. An unfiltered handler is recommended at the end to catch unexpected errors. The exception parameter `e` (or any name of your choosing) is a table containing information about the error: |Field|Description| |-|-| |`code`|The `ERR` code, e.g. `ERR_Failed`. `ERR_Exception` is the default for Lua errors.| |`message`|Human-readable error description.| |`line`|Line number where the error occurred.| |`trace`|Native array of stack frames (only with `try` enabled).| |`stackTrace`|Formatted traceback string (only with `try` enabled).| The `success` block is optional and runs if no exception occurred in the `try` block. It runs only when all resources in the `try` block have been cleaned up. There is no support for `finally` blocks; the standard pattern is to use `defer` statements or `__close` within the `try` block for cleanup actions instead. **Exception Handling:** Exceptions come in two flavours: Error codes (16-bit integer constants) and string messages. - Error messages originate from `error()` or Tiri runtime errors. They have a default error code of `ERR_Exception`. - Error codes are returned by API calls and can be triggered by calling `raise` and `check`. Preset error codes are integer constants following the `ERR_` naming convention, e.g. `ERR_Okay`, `ERR_Failed` and `ERR_Args`. The full list of error codes are listed in the Kōtuku Appendix. A key benefit to the use of error codes is that they can be filtered in exception handlers, allowing for specific handling of known error conditions. For example: ```lua try error("Lua error") except e assert(e.code is ERR_Exception) success print("No error occurred") end try raise ERR_Failed except e when ERR_Failed print('Caught ERR_Failed') end ``` **Lazy Exception Syntax:** Suppressing exceptions is permitted with the simplest form of syntax: ```lua try potentially_failing_operation() end ``` Simple catch-alls are also possible: ```lua try risky_operation() except print("An error occurred") end ``` **Re-throwing Exceptions:** Call `error()` within an except block to propagate the exception to an outer handler: ```lua try try error("Original error") except e -- Log and re-throw msg(e.message) error(e) end except e -- Catches the re-thrown exception print("Caught re-thrown error") end ``` **Filtered Exception Handling:** Use the `when` clause to catch specific `ERR` codes. Multiple codes are permitted per `when` clause, up to a limit of 4 per handler. ```lua try raise ERR_Args except e when ERR_Args print("Invalid arguments") except e when ERR_Read, ERR_Write, ERR_Seek print("Operation failed") except e print("Unexpected error: " .. e.message) end ``` Error codes are just constant integers, so custom codes can also be created and thrown through use of the `raise` function. To prevent namespace pollution, use codes in the range 8000 to 10000 for client code. **Filter Ordering Rules:** - Filtered handlers must appear before the catch-all handler - Multiple filtered handlers are checked in order; the first match wins - If no filter matches _and_ no catch-all exists, the exception propagates ```lua -- Valid: filters before catch-all try raise ERR_Failed except e when ERR_Args -- First filter except e when ERR_Failed -- Second filter (matches) except e -- Catch-all (must be last) end -- Invalid: catch-all before filter (parse error) -- try ... except e ... except e when ERR_Failed ... end ``` **Control Flow in Try Blocks:** `return`, `break`, and `continue` work correctly within try blocks: ```lua function find_value() try result = search() return result -- Returns from function normally except e return nil -- Returns nil on error end end for i in {0 to 10} do try if i is 5 then break end -- Exits loop if i % 2 is 0 then continue end -- Skips to next iteration process(i) except e -- Handle error and continue loop end end ``` **Stack Traces:** By default, stack trace information is not captured for performance reasons. Use the `` attribute to enable stack trace capture when an exception occurs: ```lua try risky_operation() try error("Inner error") -- No trace captured, missing from prior try statement except e assert(e.trace is nil) error(e) end except e print(e.stackTrace) -- Formatted traceback string -- Or access individual frames for i, frame in ipairs(e.trace) do print(f"{frame.source}:{frame.line}: in {frame.func ?? 'anonymous'}") end end ``` The `trace` field contains an array of frame tables, each with the following fields: |Field|Description| |-|-| |`source`|Source file name (may be `nil`).| |`line`|Line number (0 if unknown).| |`func`|Function name (may be `nil` for anonymous functions).| The `stackTrace` field provides a pre-formatted string in the standard "stack traceback:" format, suitable for logging or display. ### Raising Exceptions Exceptions can be raised at runtime by using the following features: #### assert() `assert()` is used to validate conditions and raise exceptions when the condition is false. It accepts two parameters - the first being the condition to evaluate, and the second an optional error message to include in the exception. Example: ```lua assert(x > 0, 'x must be greater than zero') ``` The message parameter is not evaluated if the condition was true. This allows expensive computations (e.g. long string concatenation) to be written without incurring overhead during normal runtime operations. #### check and raise `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: ```lua 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 for reporting back to the C/C++ client program. Raise also accepts an optional second parameter for a custom error message, e.g. `raise ERR_Failed, 'Operation failed due to X'`. If you only want to supply a message, use `raise 'Operation failed due to X'`; the error code defaults to `ERR_Exception`. The raise and check keywords will work for custom error codes defined by the client, the only requirement is that they are integer constants. Use codes in the range 8000 to 10000 to avoid conflicts with Kōtuku system codes. #### error() `error()` is the standard function for raising customised exceptions with the generic error code `ERR_Exception`. It should be used when raising an exception with a custom error message, e.g. `error('I have failed.')`. Passing an exception table to `error()` causes it to rethrow the exception to the next capture block. --- ## Deference ### Defer Statement The `defer` statement schedules a function to execute when the enclosing scope exits. Deferred functions execute in LIFO order (last deferred, first executed) and are guaranteed to run on normal scope exit, early `return`, `break`, or `continue`. **Basic syntax (no arguments):** ```lua function example() file = io.open("data.txt") defer file:close() end -- ... use file ... end -- file:close() executes here ``` **With argument snapshot:** ```lua function example() status = "initial" defer(s) print("Final status: " .. s) end(status) status = "modified" end -- prints "Final status: initial" ``` **Multiple defers execute in reverse order:** ```lua defer print("third") end defer print("second") end defer print("first") end -- Output: "first", "second", "third" ``` **Execution guarantees:** - Defers execute when leaving scope via normal flow, `return`, `break`, or `continue` - Deferred functions capture upvalues from the enclosing scope - Arguments passed via `end(...)` are snapshotted at registration time - Multiple defers in the same scope execute in LIFO order (last registered executes first) **Limitations:** - Error path execution during stack unwinding is not currently supported - Errors in deferred functions propagate normally ### Deferred Expressions Deferred expressions provide lazy evaluation of expressions, delaying computation until the value is actually accessed. This is particularly useful for avoiding expensive computations in conditional parameters or logging statements that may not be executed. **Basic syntax:** ```lua or <{ expression }> ``` The expression inside `<{ }>` is not evaluated immediately. Instead, evaluation is deferred until the value is accessed through standard API functions or explicitly resolved. Consider this common pattern: ```lua some_function(conditional_value, "Error occurred: " .. expensive_debug_info()) ``` The string concatenation and `expensive_debug_info()` are processed immediately irrespective of whether `some_function()` will use the computed variable. With deferred expressions: ```lua some_function(conditional_value, <{ "Error occurred: " .. expensive_debug_info() }>) ``` The expensive computation only occurs if the assertion fails and the message is accessed. **Type Inference:** Tiri automatically infers types from deferred expressions: ```lua str = <{ 'hello' }> -- Inferred as string num = <{ 42 }> -- Inferred as number bool = <{ true }> -- Inferred as boolean tbl = <{ {} }> -- Inferred as table result = <{ a + b }> -- Inferred as number (arithmetic) msg = <{ s .. t }> -- Inferred as string (concatenation) ``` **Explicit Type Annotation:** When type cannot be inferred (such as function calls), use explicit type annotation: ```lua ``` **The `resolve()` Function:** Use `resolve()` to explicitly evaluate a deferred expression: ```lua lazy = <{ expensive_computation() }> -- Later, when you need the value: value = resolve(lazy) ``` The `resolve()` function returns non-deferred values unchanged, making it safe to call on any value. **Single Evaluation:** Once evaluated, the result is cached in the variable. Subsequent access returns the cached value without re-evaluation: ```lua count = 0 x = <{ count++; count }> print(resolve(x)) -- Prints 1, increments count and caches result back in x print(resolve(x)) -- Prints 1 again, uses cached result ``` Single-evaluation is a super-power for deferred expressions if used correctly, and offers creative programming opportunities. For instance: ```lua glSelf = -- Executes once, does nothing if never used. activate_object = -- On resolution stores the ERR code permanently ``` **Important Notes:** - Calling `type()` on a deferred expression will return the type associated with the expression without evaluating it. - They capture upvalues from the enclosing scope - Error handling works naturally - errors during evaluation propagate normally - Deferred expressions can be nested, stored in tables, and passed as arguments - The type annotation or inference is required for correct behaviour with type-checking code ### Thunks Thunk functions extend deferred expressions to support parameterised lazy evaluation. While deferred expressions wrap a single expression, thunk functions allow you to define reusable lazy computations with parameters. **Syntax:** ```lua thunk name(params):type -- body return value end ``` The `thunk` keyword declares a function that, when called, is primed by capturing its arguments and returns a deferred value. The body is not executed until the result is accessed through a read operation or explicitly resolved. **Example - Lazy Database Query:** ```lua thunk fetch_user(id:num):table print("Fetching user " .. id) return database.query("SELECT * FROM users WHERE id = ?", id) end user = fetch_user(123) -- Function primed in user and not executed yet print(user.name) -- Query executes here, prints "Fetching user 123" print(user.email) -- Uses cached result, no re-query ``` **Example - Conditional Computation:** ```lua thunk expensive_report(year:num):str return generate_annual_report(year) -- Only runs if report is actually used end report = expensive_report(2024) -- Store reference to thunk with 2024 parameter value if user_requested_report then print(report) -- Report generated only when needed end ``` **Anonymous Thunks:** Anonymous thunks can skip the invocation process if they are parameterless: ``` local isAvailable = thunk():bool -- Prepare isAvailable without executing the body -- Do something -- return true end print(isAvailable) -- Resolved and cached here, no need to prime with isAvailable() ``` If one or more parameters are specified in the thunk signature then this feature does not apply. **Key Characteristics:** - Parameters are captured at call time, not resolution time. - The body executes once on first access; the result is cached. - `type()` returns the declared type without executing the body. - Use `resolve()` for explicit evaluation. - Thunks resolve automatically when used in operations, comparisons, or passed to API functions. --- ## Equality Operators ### Approximate Equality The `≈` operator compares two numeric values using an inclusive absolute tolerance of `1e-5`. It returns `true` when `Left - Right` is between `-1e-5` and `1e-5`, matching the rule `math.abs(Left - Right) <= 1e-5` without evaluating either operand more than once. ```lua if 0.33333 ≈ 1 / 3 then print('close enough') end ``` There is no ASCII alias for this operator. Use `not (a ≈ b)` when an approximate inequality check is required. Operands must be numeric; non-numeric values fail through the normal arithmetic path. `NaN` is never approximately equal, and infinities follow the subtraction rule, so `math.huge ≈ math.huge` is false because the intermediate difference is `NaN`. ### If-Empty Logical Operator Tiri adds a `??` operator that extends the falsey semantics of the `or` operator. The `??` operator treats the following values as falsey: `nil`, `false`, `0`, and `""` (empty string). This feature was introduced so that it would be easier to write shorthand for dealing with values that are empty. In addition, the right-hand side is not evaluated if the left-hand is true, leading to faster code. **Examples:** ```lua -- Standard 'or' vs '??' with zero a = 0 or "default" -- a is 0 (zero is truthy by default) b = 0 ?? "default" -- b is "default" (zero is falsey in ??) -- Standard 'or' vs '??' with empty string c = "" or "fallback" -- c is "" (empty string is truthy by default) d = "" ?? "fallback" -- d is "fallback" (empty string is falsey in ??) -- Chaining value = "" ?? 0 ?? "final" -- value is "final" (both "" and 0 are falsey) -- Short-circuit evaluation function expensive() error("Should not be called") end result = "valid" ?? expensive() -- result is "valid", expensive() is never called ``` **If-Empty Conditional Shorthand** The `?!` operator can be used as a conditional operator for routines and control flow. For instance `if not value then error() end` can now be written as simply as: ```lua value ?! error('Value is empty') ``` Use `?!` when the same extended falsey check should guard control flow with `return`, `break`, `continue`, `raise` or `check`: ```lua user_input ?! return ERR_InvalidInput ``` **Notes** - Standard `or` treats only `nil` and `false` as falsey. Values like `0` and `""` are considered truthy. - The `??` operator treats `0` and `""` as falsey. - The `??` operator has the same precedence as `or` and `and` (lowest priority). - It is left-associative, like `or` and `and`. - Line splits immediately following `??` are intentionally forbidden. ### If-Empty Postfix Operator If the `??` operator is used in the absence of a value to its right-hand-side, it is treated as a postfix operator. In this mode it will return a boolean indicating whether a value is present, using extended falsey semantics. It returns `false` for `nil`, `false`, `0`, and `""` (empty string), and `true` for all other values. ```lua -- Basic usage if comment?? then print("Comment exists") end -- In expressions greeting = name?? and "Hello, " .. name or "Hello, Guest" -- With field access and expressions if config.timeout?? then ... end if (x + y)?? then ... end ``` As a postfix operator, `??` has high precedence and evaluates before logical operators. ### Ternary Operator The use of a single `?` can be complemented with a C-style ternary conditional operator `:>` that creates an expression equivalent to an if-then-else statement. `result = condition ? true_expr :> false_expr` The standard ternary evaluates `condition` using the same falsey semantics as `if`: only `nil` and `false` are falsey. If truthy, it returns `true_expr`; if falsey, it returns `false_expr`. Only one branch is evaluated to ensure run-time efficiency. ```lua status = user ? "logged in" :> "guest" max = a > b ? a :> b msg = error ? "Error: " .. error :> "Success" ``` Use `?? :>` when the condition should use extended falsey semantics, matching the `??` operator: `nil`, `false`, `0`, `""`, and empty arrays are falsey. ```lua status = value ?? "present" :> "empty" ``` **Notes:** - Short-circuit evaluation: only the selected branch is evaluated - Precedence: same as `or`, `and`, `?`, and `??` (lowest priority) - Right-associative: `a ? b :> c ? d :> e` parses as `a ? b :> (c ? d :> e)` - Use parentheses in expression lists: `local a, b = (cond ? x :> y), z` --- ## Ranges Tiri provides a dedicated `range` type, implemented as userdata, for expressing numeric intervals. Ranges are primarily used for iteration, slicing and membership tests, and integrate with the language via both constructor functions and literal syntax. Ranges are immutable. Once created, a range's `start`, `stop`, `step` and `inclusive` properties cannot be modified. ### Range Constructor The `range()` constructor function creates a range object: ```lua r1 = range(0, 5) -- Exclusive: 0,1,2,3,4 r2 = range(0, 5, true) -- Inclusive: 0,1,2,3,4,5 r3 = range(0, 10, false, 2) -- Exclusive with step: 0,2,4,6,8 r4 = range(10, 0) -- Reverse: 10,9,8,...,1 (step inferred as -1) ``` Constructor parameters: - `range(Start, Stop)` - Creates an exclusive range from `Start` up to, but not including, `Stop`. - `range(Start, Stop, Inclusive)` - When `Inclusive` is `true`, the `Stop` value is included. - `range(Start, Stop, Inclusive, Step)` - Explicit step value (positive or negative). `Step` must be a non‑zero integer. All arguments must be integers (or integer‑valued numbers). Passing non‑integer or `nil` values will raise an error. Range properties and helpers: - `r.start` / `r.stop` / `r.step` / `r.inclusive` — access range parameters. - `#r` or `r.length` — number of elements in the range. Ranges report their type as `range`: ```lua r = range(0, 5) assert(type(r) is "range") ``` ### Range Literals Tiri adds literal syntax for constructing ranges using braces and word separators: ```lua r1 = {0 to 5} -- Exclusive: 0,1,2,3,4 r2 = {0 into 5} -- Inclusive: 0,1,2,3,4,5 r3 = {10 to 1} -- Reverse exclusive: 10,9,8,...,2 r4 = {5 into 1} -- Reverse inclusive: 5,4,3,2,1 r5 = {0 to 10 by 2} -- Exclusive with step: 0,2,4,6,8 r6 = {10 into 0 by -2} -- Reverse inclusive with step: 10,8,6,4,2,0 ``` Rules: - `{Start to Stop}` creates an exclusive range. - `{Start into Stop}` creates an inclusive range. - `{Start to Stop by Step}` and `{Start into Stop by Step}` create stepped ranges. - Reverse ranges (where `Start > Stop`) automatically infer a negative `step`. - Explicit steps are used exactly as written. Use a negative step for reverse stepped ranges. - Negative indices are supported and behave as expected for interval math. - Variable references are supported. - Expressions are supported in the start, stop and step positions. Literal ranges are fully compatible with the constructor: ```lua assert({0 to 5} is range(0, 5)) assert({0 into 5} is range(0, 5, true)) assert({0 to 10 by 2} is range(0, 10, false, 2)) ``` Invalid literal operands (non‑numeric or `nil`) will raise an error at runtime when the underlying `range()` call is evaluated. ### Iteration with Ranges Ranges support Tiri's generic `for` loops via a special iterator protocol. The recommended style is to use range literals directly in the loop header: ```lua for i in {1 to 6} do print(i) -- 1,2,3,4,5 end for i in {1 into 5} do print(i) -- 1,2,3,4,5 end for i in {5 to 1} do print(i) -- 5,4,3,2 (exclusive) end for i in {5 into 1} do print(i) -- 5,4,3,2,1 (inclusive) end ``` Anonymous loops omit the need for a variable store: ``` for {0 to 10} do count++ end ``` You can also iterate using a range stored in a variable by calling it to obtain the iterator triple: ```lua r = {0 to 5} sum = 0 for i in r() do sum += i -- 0+1+2+3+4 end ``` For constructor‑based ranges, the iterator is obtained in the same way: ```lua for i in range(0, 10, false, 2)() do -- i = 0,2,4,6,8 end for i in {0 to 10 by 2} do -- i = 0,2,4,6,8 end for i in {10 into 0 by -2} do -- i = 10,8,6,4,2,0 end ``` ### range.toArray() Returns the range as an array of integers that match its sequencing and step attributes. ### range.slice() `result = range.slice(Object, Range)` The `range.slice()` function provides a unified API for slicing arrays, tables and strings. It is the underlying implementation used by the `t[{range}]` and `s[{range}]` index syntax, as well as `table.slice()`. `Object` is an array, table or string to slice. `Range` is a range object (literal or constructed) specifying the slice bounds. **Examples:** ```lua -- Table slicing t = {10, 20, 30, 40, 50} result = range.slice(t, {1 to 4}) -- {20, 30, 40} result = range.slice(t, {0 into 4}) -- {10, 20, 30, 40, 50} -- String slicing s = "Hello, World!" result = range.slice(s, {0 to 5}) -- "Hello" result = range.slice(s, {-6 into -1}) -- "World!" result = range.slice(s, {-6 to -1}) -- "World" -- With stepped ranges (requires constructor) t = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} r = range(0, 9, true, 2) result = range.slice(t, r) -- {0, 2, 4, 6, 8} -- Reverse slicing result = range.slice(t, {9 into 0}) -- {9, 8, 7, 6, 5, 4, 3, 2, 1, 0} ``` ### Functional Iteration with each() Ranges provide an `each()` method for functional-style iteration. The method accepts a callback function that is invoked once for each value in the range: ```lua sum = 0 {1 to 6}:each(Value => sum += Value) -- sum is now 15 (1+2+3+4+5) ``` The callback receives a single argument: the current value in the iteration sequence. **Early Termination** The callback can return `false` to terminate iteration early: ```lua sum = 0 r = {1 to 100} r:each(Value => do if Value > 5 then return false end sum += Value end) -- sum is 15 (iteration stopped after 5) ``` Returning `true` or `nil` (no return) continues iteration normally. **Method Chaining** The `each()` method returns the original range object, enabling method chaining: ```lua r = {1 to 6} r:each(Value => print(Value)) :each(Value => process(Value)) ``` **Usage with Constructor and Literals** The `each()` method works with both range literals and constructor-based ranges: ```lua -- With constructor (can chain directly) range(0, 10, false, 2):each(Value => print(Value)) -- With literal (must store in variable first due to parser limitations) r = {0 to 10} r:each(Value => print(Value)) ``` ### Functional Range Methods Ranges provide several functional programming methods for transforming, filtering, and querying data. These methods offer a declarative style for working with numeric sequences. #### filter(Predicate) Returns an array containing only values for which the predicate function returns `true`: ```lua -- Get even numbers from 1 to 10 evens = {1 to 11}:filter(i => i % 2 is 0) -- evens = {2, 4, 6, 8, 10} -- Filter with more complex logic scores = {0 to 100}:filter(function(score) return score >= 60 and score <= 80 end) ``` #### reduce(Initial, Reducer) Folds all values in the range into a single accumulated result: ```lua -- Sum numbers 1 through 5 sum = {1 into 5}:reduce(0, (acc, i) => acc + i) -- sum = 15 -- Calculate factorial of 5 factorial = {1 into 5}:reduce(1, (acc, i) => acc * i) -- factorial = 120 -- Build a comma-separated string csv = {1 to 4}:reduce("", function(acc, i) if acc is "" then return tostring(i) end return acc .. "," .. tostring(i) end) -- csv = "1,2,3" ``` #### map(Transform) Returns an array with each value transformed by the given function: ```lua -- Double each value doubled = {1 to 6}:map(i => i * 2) -- doubled = {2, 4, 6, 8, 10} -- Convert to strings with formatting labels = {1 to 4}:map(i => "Item " .. tostring(i)) -- labels = {"Item 1", "Item 2", "Item 3"} -- Calculate squares squares = {1 into 5}:map(i => i * i) -- squares = {1, 4, 9, 16, 25} ``` #### take(Count) Returns a table containing the first `Count` values from the range: ```lua -- Get first 3 values first3 = {1 to 100}:take(3) -- first3 = {1, 2, 3} -- Works with reverse ranges last3 = {10 to 0}:take(3) -- last3 = {10, 9, 8} -- If count exceeds range length, returns all available values all = {1 to 4}:take(10) -- all = {1, 2, 3} ``` #### any(Predicate) Returns `true` if any value in the range satisfies the predicate (short-circuits on first match): ```lua -- Check if any value is greater than 5 hasLarge = {1 to 10}:any(i => i > 5) -- hasLarge = true -- Check if any value is negative hasNegative = {0 to 100}:any(i => i < 0) -- hasNegative = false -- Efficient: stops at first match found = {1 to 1000000}:any(i => i is 42) -- Only checks values 1 through 42 ``` #### all(Predicate) Returns `true` if all values in the range satisfy the predicate (short-circuits on first failure): ```lua -- Check if all values are positive allPositive = {1 to 10}:all(i => i > 0) -- allPositive = true -- Check if all values are less than 5 allSmall = {1 to 10}:all(i => i < 5) -- allSmall = false (fails at 5) -- Empty ranges return true (vacuous truth) emptyCheck = {5 to 5}:all(i => false) -- emptyCheck = true ``` #### find(Predicate) Returns the first value that satisfies the predicate, or `nil` if none found: ```lua -- Find first value greater than 5 first = {1 to 10}:find(i => i > 5) -- first = 6 -- Find first even number firstEven = {1 to 10}:find(i => i % 2 is 0) -- firstEven = 2 -- Returns nil when not found notFound = {1 to 10}:find(i => i > 100) -- notFound = nil -- Works with reverse ranges fromEnd = {10 to 0}:find(i => i < 5) -- fromEnd = 4 ``` **Method Return Types:** | Method | Returns | |-|-| | `filter()` | Table | | `reduce()` | Single value (type depends on reducer) | | `map()` | Table | | `take()` | Table | | `any()` | Boolean | | `all()` | Boolean | | `find()` | Value or `nil` | **Note:** Methods that return tables (`filter`, `map`, `take`) cannot be chained with other range methods since tables are not ranges. Use `reduce()` directly on a range for aggregation, or store intermediate results: ```lua -- Sum of squares of even numbers from 1 to 20 sum = {1 to 21}:reduce(0, function(acc, i) if i % 2 is 0 then return acc + (i * i) end return acc end) -- sum = 2**2 + 4**2 + 6**2 + ... + 20**2 = 1540 ``` ### String Slicing with Ranges Strings support range objects as indices to perform slicing. Indexing is zero‑based and inclusive of the start position; the end index is interpreted according to the range's `inclusive` flag. ```lua s = "Hello, World!" assert(s[{0 to 5}] is "Hello") -- Exclusive: indices 0..4 assert(s[{7 to 12}] is "World") -- Exclusive: indices 7..11 assert(s[{0 into 4}] is "Hello") -- Inclusive: indices 0..4 assert(s[{-6 into -1}] is "World!") -- Negative inclusive: indices 7..12 assert(s[{-6 to -1}] is "World") -- Negative exclusive: indices 7..11 ``` Out‑of‑bounds and empty ranges behave predictably: - Ranges entirely beyond the string length return an empty string. - Exclusive ranges where `start` equals `stop` yield an empty string. - Inclusive single‑element ranges return the corresponding character. ### Table Slicing with Ranges Tables support range objects as indices to extract subsequences. Indexing is zero‑based and follows the same inclusive/exclusive semantics as string slicing. Table slices always return a new table containing copies of the selected elements. ```lua t = {10, 20, 30, 40, 50} subset = t[{1 to 4}] -- Returns {20, 30, 40} (exclusive) subset = t[{1 into 3}] -- Returns {20, 30, 40} (inclusive) subset = t[{0 to 5}] -- Returns {10, 20, 30, 40, 50} (full table) subset = t[{-3 into -1}] -- Returns {30, 40, 50} (negative inclusive) subset = t[{-3 to -1}] -- Returns {30, 40} (negative exclusive) ``` **Negative Indices:** Negative indices count backwards from the end of the table. The range operator still controls the stop point: `...` includes the resolved stop index and `..` excludes it. ```lua t = {10, 20, 30, 40, 50} t[{-2 into -1}] -- Returns {40, 50} (inclusive) t[{-2 to -1}] -- Returns {40} (exclusive) t[{1 into -1}] -- Returns {20, 30, 40, 50} (inclusive) t[{1 to -1}] -- Returns {20, 30, 40} (exclusive) t[{-3 into 4}] -- Returns {30, 40, 50} (inclusive) ``` **Reverse Slicing:** Ranges where the start index is greater than the stop index produce reversed subsequences. The direction is auto-detected based on the resolved indices. ```lua t = {10, 20, 30, 40, 50} t[{4 to 0}] -- Returns {50, 40, 30, 20} (reverse exclusive) t[{4 into 0}] -- Returns {50, 40, 30, 20, 10} (reverse inclusive) t[{3 to 1}] -- Returns {40, 30} (reverse partial) ``` **Step Support:** Stepped ranges can use either the range literal `by` clause or the `range()` constructor. The explicit step is used exactly as written, so reverse stepped ranges should use a negative step. ```lua t = {10, 20, 30, 40, 50, 60, 70} r = {0 into 6 by 2} -- Every 2nd element t[r] -- Returns {10, 30, 50, 70} r = {6 into 0 by -2} -- Reverse with step t[r] -- Returns {70, 50, 30, 10} r = range(0, 7, true, 2) -- Constructor form remains available ``` **Out-of-Bounds and Empty Ranges:** - Indices beyond the table length are clipped to valid bounds - Exclusive ranges where `start` equals `stop` return an empty table - Ranges entirely beyond the table length return an empty table ```lua t = {10, 20, 30} t[{0 to 10}] -- Returns {10, 20, 30} (end clipped) t[{-10 to 3}] -- Returns {10, 20, 30} (start clipped to 0) t[{2 to 2}] -- Returns {} (empty exclusive range) t[{5 to 10}] -- Returns {} (beyond table length) ``` **Metatable Interaction:** Tables with custom `__index` metamethods will use the custom handler instead of the base table slicing. Tables with other metamethods (like `__len`, `__tostring`, etc.) but no `__index` will still support range slicing through the base metatable fallback. ```lua t = {10, 20, 30} setmetatable(t, { __len = function() return 100 end }) t[{0 to 3}] -- Returns {10, 20, 30} (slicing still works) ``` ### Membership Testing Tiri extends the `in` operator to work with ranges outside of `for` loops. Membership tests are implemented in terms of the range's `contains` method, and always return a boolean. ```lua r = {0 to 10} -- Exclusive: 0–9 ri = {0 into 10} -- Inclusive: 0–10 assert(5 in r) assert(not (10 in r)) -- 10 excluded assert(10 in ri) assert(not (11 in ri)) ``` `in` can be used anywhere a boolean expression is expected, including conditionals: ```lua if 5 in {0 to 10} then print("in range") end if not (11 in {0 to 10}) then print("out of range") end ``` Membership tests work with both literal ranges and ranges stored in variables. The semantics follow the `contains` method exactly, including step handling and inclusive/exclusive behaviour. --- ### Choose Expressions The `choose ... from` syntax provides pattern matching for selecting a result value (or executing a branch) without verbose `if`/`elseif` chains. Basic form: ```lua status_text = choose status from 200 -> 'OK' 404 -> 'Not Found' else -> 'Unknown' end ``` **Semantics:** - The scrutinee expression (`status`) is evaluated exactly once. - Cases are tested in order; the first match wins. - `else` is optional. If omitted and no case matches, the result is `nil` (expression context) and no action is taken (statement context). - `else` must be the final case. An `else`-only `choose` is valid and always matches. **Patterns:** - **Literal patterns**: numbers, strings, booleans, and `nil` match using `is` semantics (no implicit type coercion). - **Wildcard**: `_` matches anything and is typically used as a catch-all. - **Relational patterns**: `< Expr`, `<= Expr`, `> Expr`, `>= Expr` use normal relational operators. - **Table patterns**: `{ key = value, ... }` match tables using open-record semantics: - Extra keys on the scrutinee are ignored - Matching is shallow and compares values using `is` - `{}` matches any table value (but does not match non-table values) - **Tuple patterns**: `(p0, p1, ...)` match tuples created from multiple scrutinee values: - `choose (a, b) from ... end` evaluates `a` and `b` once and matches by arity - Tuple arity mismatches are compile-time errors - `(x)` is a parenthesised expression; a tuple requires a comma **Guards:** Cases may include a `when` clause to add a conditional guard: ```lua icon = choose notification from { type = 'message', unread = true } -> 'icon-inbox-unread' { type = 'message' } when notification.priority > 5 -> 'icon-priority' else -> 'icon-default' end ``` The guard is evaluated only after the pattern has matched. Guard failure proceeds to the next case. **Desugaring (conceptual):** `choose ... from` lowers to an `if`/`elseif` chain using a temporary to ensure the scrutinee is evaluated once. In complex expression positions, the compiler may wrap the `choose` in a small function to yield a value. **Examples:** Tuple scrutinee with wildcard patterns: ```lua movement = choose (dx, dy) from (0, 0) -> 'standing' (0, _) -> 'vertical' (_, 0) -> 'horizontal' else -> 'diagonal' end ``` Wildcard as a catch-all (including `NaN`): ```lua label = choose value from nil -> 'unset' _ -> 'set' end ``` Nesting `choose` expressions: ```lua msg = choose status from 200 -> 'OK' else -> choose retry_count from 0 -> 'Failed (no retry)' else -> 'Failed (will retry)' end end ``` Conditionals with `when` guards, against a table with pattern filtering: ```lua notification = { type = 'message', unread = true, priority = 7 } icon = choose notification from { type = 'message' } when notification.unread -> 'icon-inbox-unread' { type = 'message' } -> (notification.priority > 5 ? 'icon-priority' :> 'icon-inbox') else -> 'icon-default' end ``` Free-standing `choose` statements for flexible assignments and control flow: ```lua choose state from 'save' -> result = 'saved' 'load' -> result = 'loaded' else -> error('Invalid state') end ``` **Gotchas:** - Order matters, especially with relational patterns: place more specific cases first (`< 30` before `< 60`). - `NaN` never matches literal numeric patterns (`nan is nan` is false). Use `_` or a guard if you need to handle it. - Table patterns are shallow: nested tables must be checked via guards or nested `choose` expressions. --- ## Script Management ### include 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. ### import `import 'name' [as namespace]` The `import` statement loads and inlines a Tiri script file at parse time. Inlining means that the requested script is loaded directly into the call site during compilation, which in turn improves the parser's efficacy at producing optimised code. Scripts loaded via `import` are often referred to as 'libraries' and are typically designed to be shared. In that spirit, if a library is imported more than once, all subsequent imports do not result in reloading. Existing libraries are available in the `scripts:` volume, which is the default search location for the import process. For instance, `import 'gui'` would load the script at `scripts:gui.tiri`. In order to ensure that libraries cannot be imported multiple times, import's naming conventions are strict. Do not include the file extension, nor prefix the file with a full path. Only alpha-numeric characters are allowed for naming. **Namespaces:** Libraries will typically declare a default namespace that matches the library name. For instance, the `gui` library declares a `gui` namespace. To use a custom namespace, add the `as namespace` clause. For example: ```lua import 'gui' as myGui ``` **Application Specific Libraries:** Complex applications can benefit from using `import` to create custom libraries and split the code across multiple files. The `import` statement will search the local folder if the reference is prefixed with `./`. Standard path rules still apply, e.g. `./lib/customlib` would be valid, `./../../h4cklib!3.tiri` is not. **Behavioural Notes:** - **Top-level only**: The `import` statement can only appear in the outside scope of a script, not inside functions. Attempting to use `import` inside a function results in a parse error. - **Parse-time inlining**: The imported code becomes part of the importing script's bytecode. This means imported code executes in the same context as if it were written directly in the importing file. - **Global exports**: Global variables and functions declared with the `global` keyword in the imported file become accessible to the importing script. - **Private locals**: Local variables in the imported file remain private and do not pollute the importing script's namespace. **Example - Exporting Globals:** ```lua -- helpers.tiri global function formatDate(Timestamp) return os.date('%Y-%m-%d', Timestamp) end global HELPER_VERSION = '1.0' -- Private to helpers.tiri local internal_cache = {} ``` ```lua -- main.tiri import './helpers' print(formatDate(os.time())) -- Uses the imported global function print(HELPER_VERSION) -- Accesses the imported global variable -- internal_cache is not accessible here ``` **Conditional Pre-Processing:** The `import` statement works seamlessly with compile-time conditionals. Imported files can use `@if(imported=true)` to include code only when being imported, or `@if(imported=false)` for code that runs only when executed directly: ```lua -- library.tiri global function doWork() -- Always available end @if(imported=false) -- Only runs when library.tiri is executed directly print('Running library.tiri as main script') doWork() @end @if(imported=true) -- Only runs when imported print('library.tiri loaded as import') @end ``` **Library Development Pattern:** If developing a Tiri library, it is common to define a namespace and export global variables or functions under that namespace. This example based on the `gui` library shows a common pattern for defining a library with a namespace and exporting global variables: ``` namespace 'gui' _LIB[_NS] = { theme = 'light', dpi = 160, ... } gui = _LIB[_NS] gui.parseRGB = function(hex) ... end ``` Note that `_LIB` and `_NS` are special predefined variables available during import. `_LIB` is a table used to store library interfaces, and `_NS` contains the declared namespace string, in this case `gui`. ### loadFile() Use `loadFile()` to load, parse and execute a Tiri script at runtime. This feature is useful for re-using code, or breaking a large project into multiple script files. Example: `loadFile('programs:tools/project/example.tiri')` 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. Note: Code loaded via `loadFile()` will lose the efficiency gains afforded to scripts loaded via `import`. ### exec() Use `exec()` to parse a Tiri statement and execute it. `exec()` will raise an exception if the statement is unparseable or fails during execution. Example: ``` exec([[ print("Hello World") ]]) ``` --- ## Module Interface The module interface provides a means for Tiri programs to communicate with Kōtuku'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: `err, x, y = mGfx.getCursorPos()` Notice that Tiri 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| |core|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: `mGfx ?= mod.load('display')` Note that the Core module, identified by the `mSys` global variable is considered an essential API and is always available by default. ### Type Conversion When calling an API function, Tiri 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, Tiri 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. ### Buffer Handling 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: ```lua buffer = array 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 Tiri knows the size of the buffer and will use it for the second parameter if not already defined. ### Multiple Result Values 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 Tiri, 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 Tiri: `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. --- ## Object Interface 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. ### Object Creation 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: ```lua 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 defining key-values 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 the 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 Tiri 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 your calls with `try`, particularly if using classes like `File` where error management is good practice. #### object.init() 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. ### Object Relationships Kōtuku 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 Tiri 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: ```lua 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 the creation process is identical, but 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. Objects created as children are weakly referenced. #### object.children() Obtain 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. ### Accessing Objects Access the interface of an existing object by searching for its name or UID: ```lua window = obj.find('my_window') winsurface = obj.find(window.surface) ``` If the object is not found, `nil` is returned. If multiple objects exist with the same name, the most recently created object is returned first. #### With Statement The `with` statement locks one or more Kōtuku objects for the duration of a block, automatically unlocking them when the scope exits through any path (normal exit, `break`, `continue`, `return`, or exception unwinding). ```lua with object do -- object is locked here end -- object is automatically unlocked ``` Variables declared inside the `with` block are not visible outside it, following the same rules as `do ... end` blocks. Passing a non-object value (such as a table, string, or number) to `with` will raise a runtime error. Use `with` exclusively with Kōtuku objects. **Multiple objects:** ```lua with obj1, obj2, obj3 do -- all three objects are locked end -- all three are unlocked in LIFO order (obj3 first, then obj2, then obj1) ``` **Why lock objects?** Locking serves two purposes: 1. **Thread safety**: In multi-threaded programs, locking prevents data corruption when objects are shared across threads. 2. **Performance**: Locking an object keeps it in an accessible state, avoiding repeated access/release cycles for each field read or write. Basic read/write field accesses are approximately 2x faster when an object is pre-locked. **Performance example:** ```lua xml = obj.new('xml', { source = myfile }) with xml do for i = 0, 100000 do val = xml.source -- Immediate access granted to the source field end end ``` #### object.exists() If an object is loosely coupled to a script (a weak reference), the `exists()` method can be called at any time to confirm it is not destroyed. The result is a boolean value. #### object.detach() It is not uncommon that a script may need to create an object that outlives its scope. Calling `detach()` will unlink the object from the script and the garbage collector, demoting it to a weak reference. Once detached, the object can only be removed if its parent object is destroyed, or the `free()` method is called. Note: If an object has been created as the child of another, e.g. `viewport.new('VectorRectangle', { ... })` then the object is already weakly referenced and detach() will do nothing. #### object.free() Call `free()` to terminate an object resource immediately without removing the interface. If the object is weakly referenced then `free()` is the most practical means of removal. Calling `free()` is discouraged in general usage - a safer pattern is to set object references to `nil` and either wait for garbage collection or call `collectgarbage()`. ### Garbage Collection The 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: ```lua file = obj.new('File') ... file = nil ``` ### Action and Method Calls 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: ```lua 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. ### Field Interface Object fields are accessible by conventional means, i.e. `value = my_object.field_name` and `my_object.field_name = value` for get and set respectively. There are `get()` and `set()` methods that are equivalent to these, which allow strings to be used for dynamic field access. Some classes also support key-values via `getKey()` and `setKey()` methods, which can be used for accessing dynamic field names. #### object.getKey(), object.setKey() 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: ```lua surface.setKey('Title', 'My Window') title = surface.getKey('Title', 'UNDEFINED') ``` Some classes treat keys as parseable strings that are backed by built-in functionality. For instance, the following statement retrieves the content from slot 10 of an `item` array: `item = view.getKey('item[10]')` #### object._state() The `_state()` method is a special accessor that attaches a 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. --- ## Action & Method Subscriptions Kōtuku 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. ```lua 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. --- ## Processing Interface 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])` 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`. ### Signals If a Tiri 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: ```lua function testSignals() signal_a = obj.new('xml', { flags='NEW' }) 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. async.script([[ msg('Thread is now in session.') ]], function() msg('Thread has been executed.') signal_a.acSignal() signal_b.acSignal() end) proc = processing.new({ timeout=1.0, signals = { signal_a, signal_b } }) msg('Sleeping....') err = proc.sleep() assert(err is 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. #### signal() 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. #### flush() The `flush()` method will clear pending signals associated with the script and processing object. Using `flush()` is rarely necessary expect in special circumstances. For instance if a prior call to `sleep()` resulted in a timeout, clearing pending signals guarantees a fresh start. #### task() The `task()` function returns a Tiri object that references the current task: `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: ```lua task = processing.task() if task then task.set('Priority', 15) -- Set high priority for time-critical operations end ``` #### delayedCall() 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. ```lua processing.delayedCall(function print('This message is appearing after being delayed.') end) ``` ### Garbage Collector #### processing.collect() `processing.collect([Mode], [Options])` Controls the garbage collector. The optional `Mode` parameter specifies the collection mode: |Mode|Description| |-|-| |`full`|Performs a full garbage collection cycle (default).| |`step`|Performs an incremental collection step.| The optional `Options` table supports the following fields: |Field|Type|Description| |-|-|-| |`stepSize`|integer|Step size for incremental collection (only used with `"step"` mode).| Returns an integer result from the underlying GC operation. The meaning varies by mode: - For `"step"` mode: returns `1` if collection is not finished, `0` if finished - For other modes: returns `0` on success **Examples:** ```lua -- Full collection (default) processing.collect() processing.collect("full") -- Incremental step collection result = processing.collect("step", { stepSize = 100 }) ``` #### processing.startCollector() Starts the automatic garbage collector if it is not already running. The garbage collector runs in the background and will periodically reclaim memory used by unreferenced objects. #### processing.stopCollector() Stops the automatic garbage collector. When called, it will cease to reclaim memory until `startCollector()` is called again. #### processing.gcStats() `stats = processing.gcStats()` Returns a table containing garbage collector statistics. This is useful for monitoring memory usage and GC behaviour. |Field|Type|Description| |-|-|-| |`memoryKB`|integer|Memory usage in kilobytes.| |`memoryBytes`|integer|Remainder bytes (total bytes = memoryKB × 1024 + memoryBytes).| |`memoryMB`|number|Total memory usage in megabytes (convenience field).| |`isRunning`|boolean|`true` if the garbage collector is currently running.| |`pause`|integer|Current pause multiplier (controls GC frequency, default 200).| |`stepMul`|integer|Current step multiplier (controls GC speed, default 200).| **Example:** ```lua stats = processing.gcStats() print(f"Memory: {stats.memoryMB} MB") print(f"GC Running: {stats.isRunning}") print(f"Pause: {stats.pause}, StepMul: {stats.stepMul}") -- Monitor memory before and after allocation before = processing.gcStats().memoryMB t = {} for i in {0 to 10000} do t[i] = string.rep("x", 100) end after = processing.gcStats().memoryMB print(f"Allocated: {after - before} MB") -- Clean up and verify t = nil processing.collect() final = processing.gcStats().memoryMB print(f"After collection: {final} MB") ``` --- ## Math Interface ### math.round() `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`. --- ## Strings Interface The Lua-based strings interface is extended with a number of useful functions that are commonly required in programming. The following functions are included: ### string.alloc() `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 Kōtuku interfaces. ### string.cap() `str:cap()` Recreate a string with the first character in upper case. ### string.count() `count = str:count(Keyword)` Count the number of non-overlapping occurrences of `Keyword` in the string. Returns `0` if either string is empty or if `Keyword` is not found. ```lua "hello world hello":count("hello") -- 2 "aaa":count("aa") -- 1 (non-overlapping) ``` ### string.decap() `str:decap()` Recreate a string with the first character in lower case. ### string.escXML() `str:escXML()` Escape `str` for an XML attribute value or content. ### string.hash() `hash = str:hash([CaseSensitive])` Return a hash value for the string. The `CaseSensitive` parameter is optional and defaults to `false`. ### string.join() `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. ### string.pop() `str:pop([Count])` Returns the string with `Count` characters removed from the end. If `Count` is not specified, it defaults to `1`. If `Count` is greater than or equal to the string length, an empty string is returned. If `Count` is zero or negative, the original string is returned unchanged. ```lua "hello":pop() -- "hell" "hello":pop(2) -- "hel" "hello":pop(10) -- "" "hello":pop(0) -- "hello" ``` ### string.replace() `result, count = str:replace(Keyword, Replacement, [Limit])` Replace `Keyword` with `Replacement`, without pattern matching. `Limit` will restrict the total number of replacements if specified. Returns the modified string and the count of replacements made. ```lua -- Replace all occurrences result, count = "hello world":replace("o", "0") -- result = "hell0 w0rld", count = 2 -- Replace only the first occurrence result, count = "aaa":replace("a", "b", 1) -- result = "baa", count = 1 ``` ### string.trim() `str:trim()` Trims whitespace from the left and right sides of a string. ### string.rtrim() `str:rtrim()` Trims whitespace from the right side of a string. ### string.split() `str:split([Separator])` Split a string into an array of substrings. If `Separator` is a single character, each occurrence splits the string. If `Separator` is a multi-character string, it is matched as a whole delimiter. If `Separator` is not specified, the string is split on any individual whitespace character (space, tab, newline, carriage return). ### string.startsWith() `str:startsWith(Cmp)` Returns `true` if the string starts with `Cmp`. ### string.substr() `str:substr(Start, [End])` Extract a substring from `Start` up to but not including `End` (exclusive semantics). If `End` is not specified, the substring extends to the end of the string. Negative indices count from the end. This is the preferred alternative to `string.sub()`. ```lua "hello world":substr(0, 5) -- "hello" "hello world":substr(6) -- "world" "hello world":substr(-5) -- "world" ``` ### string.endsWith() `str:endsWith(Cmp)` Returns `true` if the string ends with `Cmp`. ### string.unescapeXML() `str:unescapeXML()` Unescape XML entities (`<`, `>`, `&`, `"`, `'`) in `str`, returning the decoded string. This is the reverse of `escXML()`. ```lua "<div>Hello & World</div>":unescapeXML() -- Returns: "
Hello & World
" ``` --- ## Async Interface Tiri supports a simplified threading model so as to minimise the potential problems occurring from their use. The functionality is as follows: ### async.script(Statement, Callback) 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. ``` async.script([[ print('Thread is now in session.') ]], function() print('Thread has finished executing.') end) ``` ### async.action|method(Object, Action, Callback, Key, Args...) The `async.action()` and `async.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) async.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 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. --- ## Structure Interface The structure interface is provided so that Tiri can use C/C++ structures declared in the Kōtuku API. You will find that many class and module API's use structures for returning condensed information. ### Creating a Structure 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 Kōtuku 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. ### Additional Functionality 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`. ### Custom Structures 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: ```lua 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 Tiri 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]`. --- ## Regular Expression Interface 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. For more information on our regex implementation, please refer to our [Regex Manual](./Regex-Manual). ### Creating a Regex To create a compiled regex object, use the `new()` method with the following prototype: `rx = 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 behaviour using the flag constants described below. ```lua -- Simple pattern digits = regex.new('\\d+') -- Case insensitive pattern 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 `try` when creating regex objects that use untested patterns, e.g. from user input. ### Compilation Flags The following flags can be combined to modify regex compilation: |Flag|Description| |-|-| |`regex.ICASE`|Case insensitive matching| |`regex.MULTILINE`|^ and $ match line boundaries| |`regex.DOT_ALL`|. matches newlines| Flags can be combined: `regex.ICASE | regex.MULTILINE`. ### Match Flags The following `RMATCH` flags can be used with the optional `Flags` parameter in `test()`, `match()`, `search()`, `replace()`, and `split()` methods: |Flag|Description| |-|-| |`regex.NOT_BEGIN_OF_LINE`|Do not treat the beginning of text as start of line| |`regex.NOT_END_OF_LINE`|Do not treat the end of text as end of line| |`regex.NOT_BEGIN_OF_WORD`|Do not treat the beginning of text as start of word| |`regex.NOT_END_OF_WORD`|Do not treat the end of text as end of word| |`regex.NOT_NULL`|Do not match empty sequences| |`regex.CONTINUOUS`|Only match at the beginning of text| |`regex.PREV_AVAILABLE`|Previous character is available for look-behind| |`regex.REPLACE_NO_COPY`|Do not copy non-matching parts in replace operations| |`regex.REPLACE_FIRST_ONLY`|Replace only the first occurrence| Example usage: ```lua wordRegex = regex.new('\\w+') -- Replace only the first word result = wordRegex.replace('hello world', 'goodbye', regex.REPLACE_FIRST_ONLY) -- Result: 'goodbye world' ``` ### Regex Properties Compiled regex objects provide the following read-only properties: - `pattern`: The original pattern string - `flags`: The compilation flags used - `error`: Error message if compilation failed (only available in internal error states) ```lua rx = regex.new('\\d+') print('Pattern: ' .. rx.pattern) print('Flags: ' .. rx.flags) ``` **Note**: Invalid regex patterns will raise an error during construction. Use `try` to handle potential compilation errors: ```lua local rx try rx = regex.new('[invalid') except ex print('Regex compilation failed: ' .. ex.message) end ``` ### Performance Considerations Regex objects are compiled once and can be reused many times, making them significantly more efficient than traditional string matching for complex patterns. Store regex objects in deferred expressions to create them on demand and avoid recreating them: ```lua global emailValidator = <{ regex.new('^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$') }> for email in values(emailList) do if emailValidator.test(email) then processEmail(email) end end ``` ### regex.escape() `escaped = regex.escape(Text)` Use the static `regex.escape()` method to escape all regex metacharacters in a string so it can be used as a literal pattern. This is essential when constructing patterns from user input or dynamic data that may contain special regex characters. ```lua -- Escape special characters for literal matching escaped = regex.escape('hello.world') -- 'hello\\.world' escaped = regex.escape('a+b*c?d') -- 'a\\+b\\*c\\?d' escaped = regex.escape('(group)[class]') -- '\\(group\\)\\[class\\]' -- Safe pattern from user input userInput = '[user@example.com]' safePattern = regex.new(regex.escape(userInput)) ``` ### regex.test() `result = rx.test(Text, [RMATCH])` Use the `test()` method to test a pattern against a string. It returns `true` if the pattern matches anywhere in the text, `false` otherwise. ### regex.findFirst() `start, stop, captures = rx.findFirst(Text, [Offset], [RMATCH])` The `findFirst()` method offers the fastest way to locate the first match within a `Text` string. The optional `Offset` parameter specifies where to begin searching. The start and stop positions of the match are returned, or `nil, nil` if no match is found. If captures are used, they are returned as a string array in the third result. ```lua digits = regex.new('\\d+') -- Find first occurrence pos, len = digits.findFirst('abc123def456') -- pos = 3, len = 3 (matches '123') -- Start searching from a specific position pos, len = digits.findFirst('abc123def456', 6) -- pos = 9, len = 3 (matches '456') ``` This method is optimised for speed when you only need the position of a match, not the matched text or capture groups. For quickly extracting captures, `regex.extract()` is often preferred. ### regex.findAll() `iter = rx.findAll(Text, [Offset], [RMATCH])` Use the `findAll()` method to iterate over all matches in a string: ```lua for start, stop, captures in rx.findAll(Text, [Offset], [RMATCH]) do ... end ``` Each iteration yields: - `pos`: The position of the match - `len`: The length of the match - `captures`: An array of capture group strings (or `nil` if no bracketed captures were defined) ```lua -- Simple iteration without captures digits = regex.new('[0-9]+') for pos, len in digits.findAll('abc123def456ghi789') do print(f'Found at {pos}, length {len}') end -- Iteration with capture groups pattern = regex.new('([a-z]+)([0-9]+)') for pos, len, caps in pattern.findAll('abc123def456') do print(f'Match: {caps[0]}, letters: {caps[1]}, digits: {caps[2]}') end -- Extract emails with structured data emailRegex = regex.new('([^@\\s]+)@([^.\\s]+)\\.([^\\s]+)') for pos, len, caps in emailRegex.findAll('Contact alice@foo.com or bob@bar.org') do print(f'User: {caps[1]}, Domain: {caps[2]}, TLD: {caps[3]}') end ``` **Notes:** - If no bracketed capture groups `()` are defined in the pattern, `captures` will be `nil`. - Zero-width matches advance by at least one character to prevent infinite loops. ### regex.extract() `capture, ... = rx.extract(Text, Offset, [RMATCH]) The `extract()` method provides a convenient interface when using patterns that are designed for capturing. Rather than emitting an array of captures as in `findFirst()`, captured results are instead returned in sequence, as individual string values. The downside to this convenience is that the start and end points of the match are omitted. ### regex.match() `matches = rx.match(Text, [RMATCH])` Use the `match()` method to perform a whole-string match and return capture groups. Returns an array containing the full match and any capture groups, or `nil` if no match is found. Array indices start at 0 (full match), with capture groups at indices 1, 2, etc. ```lua rx_url = regex.new('(https?)://([^/]+)(.*)') matches = rx_url.match('https://example.com/path') -- matches[0] = 'https://example.com/path' (full match) -- matches[1] = 'https' (first capture group) -- matches[2] = 'example.com' (second capture group) -- matches[3] = '/path' (third capture group) ``` ### regex.search() `all_matches = rx.search(Text, [RMATCH])` Use the `search()` method to find all matches in the text. Returns an array where each element is a match array (as returned by `match()`), or `nil` if no matches are found. ```lua rx_word = regex.new('(\\w+)') all_words = rx_word.search('hello world test') -- all_words[0][0] = 'hello', all_words[0][1] = 'hello' -- all_words[1][0] = 'world', all_words[1][1] = 'world' -- all_words[2][0] = 'test', all_words[2][1] = 'test' ``` ### regex.replace() `result = rx.replace(Text, Replacement, [RMATCH])` Use the `replace()` method to replace all occurrences of a pattern. Replacement strings support backreferences using `$1`, `$2`, etc., to reference capture groups. ```lua rx_phone = regex.new('(\\d{3})-(\\d{3})-(\\d{4})') formatted = rx_phone.replace('555-123-4567', '($1) $2-$3') -- Result: '(555) 123-4567' ``` ### regex.split() `parts = rx.split(Text, [RMATCH])` Use the `split()` method to split text using the regex pattern as a delimiter. Returns an array containing the split parts (empty strings are excluded). ```lua csvRegex = regex.new('\\s*,\\s*') -- Split on commas and eliminate whitespace fields = csvRegex.split('apple, banana, cherry') -- fields = { 'apple', 'banana', 'cherry' } ``` --- ## Array Interface Tiri includes a native array interface that is fully integrated with the JIT compiler. We strongly recommend its use for the storage of sequentially arranged data, and that tables are avoided for that use case. Arrays are typed containers that store elements of a single primitive type. They provide efficient storage and direct memory access, making them ideal for numerical computations, buffer management, and interoperability with C/C++ APIs. ### Table Compatibility Arrays are largely compatible with tables so that they may be used interchangeably in code. In particular the `ipairs()`, `pairs()` and `values()` functions behave identically for both types. Numeric indexes on tables and arrays function identically. ### Array Syntax Tiri provides concise syntax for creating typed arrays using the `array` pattern: ```lua arr = array -- Empty array of specified type arr = array -- Pre-allocated array with size elements arr = array { v1, v2, ... } -- Array initialised with values arr = array { v1, v2, ... } -- Pre-allocated with initial values ``` **Examples:** ```lua -- Create typed arrays with concise syntax numbers = array { 1, 2, 3, 4, 5 } names = array -- Pre-allocate 100 string slots points = array { {x=0, y=0}, {x=1, y=1} } -- Chain methods directly on the result result = array { 1, 2, 3 }:concat(', ', '%d') -- Returns '1, 2, 3' -- Pre-allocated with initial values (size > values count) buffer = array { 1, 2, 3 } -- {1, 2, 3, 0, 0, 0, 0, 0, 0, 0} -- Dynamic size expression with variables and functions n = 8 arr = array { 100, 200 } -- {100, 200, 0, 0, 0, 0, 0, 0} ``` When both a size and initial values are provided, the array is first initialised with the values, then resized to the specified size. If the size is larger than the number of values, the remaining elements are zero-initialised (or `nil` for reference types like strings and tables). If the size is smaller than the number of values, the values take precedence and the size is effectively ignored. **Supported Element Types:** |Type|Size|Description| |-|-|-| |`byte`|1|Unsigned 8-bit integer (0-255)| |`int16`|2|Signed 16-bit integer| |`int`|4|Signed 32-bit integer| |`int64`|8|Signed 64-bit integer| |`float`|4|32-bit floating point| |`double`|8|64-bit floating point| |`string`|8|String reference| |`object`|varies|Object reference| |`struct`|varies|Structure reference| |`table`|varies|Table reference| |`array`|varies|Array reference (use for creating multi-dimensional arrays)| |`any`|16|Any type from the above (note: convenience comes at a cost of efficiency)| |`pointer`|8|Memory pointer (unavailable for client use)| ### array.new() ```lua arr = array.new(Size, Type) arr = array.new(String) ``` The traditional `array <>` syntax is syntactic sugar that desugars to `array.new()` and `array.of()` calls. We recommend against calling `array.new()` directly unless an edge case makes it necessary. There is however, one unique feature in that passing a string to `array.new()` will create a byte array from the string's contents. **Examples:** ```lua -- Create a 100-element integer array int_arr = array.new(100, 'int') -- Create an array from a string (each byte becomes an element) byte_arr = array.new('Hello World') -- Create an empty array (useful for dynamic filling) empty_arr = array.new(0, 'double') ``` ### array.of() ```lua arr = array.of(Type, Value1, Value2, ...) ``` As for `array.new()`, calling `array.of()` is not recommended unless an edge case requires it. Calling `array.of()` will populate a new array with the given values. `Type` is the element type. The remaining arguments are the values to populate the array with. At least one value must be provided. **Examples:** ```lua -- Create a string array with two domain names domains = array.of('string', 'google.com', 'amazon.co.uk') -- Mixed types arr = array.of('any', 42, 'hello', true, nil, { x = 10 }) ``` ### Array Indexes Array elements are accessed using zero-based indexing: ```lua arr = array arr[0] = 100 -- Set first element arr[9] = 999 -- Set last element value = arr[5] -- Read element at index 5 ``` The `#` operator returns the array length: ```lua total = #arr -- Returns 10 ``` ### array.type() Returns the element type of the array as a string. ```lua arr = array print(arr:type()) -- Prints 'float' byte_arr = array.new('test') print(byte_arr:type()) -- Prints 'char' ``` ### array.readOnly() Returns `true` if the array is read-only, `false` otherwise. Read-only arrays are typically created by the system when wrapping external memory buffers. ### array.table() Returns a copy of the array in table format. ### Array Manipulation Methods ### array.push() `length = arr:push(Value, ...)` Appends one or more elements to the end of the array, growing capacity as needed. Returns the new length of the array. Arrays grow automatically when pushing beyond current capacity. External arrays (wrapping C/C++ memory) cannot grow and will raise an error. Type validation is performed; pushing incompatible types raises an error. Calling `push()` with no arguments returns the current length without modification. ### array.pop() `value = arr:pop([Count])` Removes and returns the last element(s) from the array. `Count` indicates the number of elements to pop (defaults to 1). Returns `nil` if the array is empty. ```lua arr = array { 1, 2, 3, 4, 5 } a, b = arr:pop(2) -- a = 5, b = 4, arr = {1, 2, 3} ``` Pop returns `nil` if used on an empty array. When popping GC-tracked types (strings, tables), the reference is cleared to allow garbage collection. Read-only arrays cannot be popped. ### array.clear() `arr:clear()` Resets the array length to zero without deallocating storage. The capacity is preserved for efficient reuse. For GC-tracked types (strings, tables), references are nullified to allow garbage collection. Read-only arrays cannot be cleared. Use `fill(0)` if the intention is to clear a newly allocated array. ### array.resize() `new_length = arr:resize(NewSize)` Resizes an array to the specified length, growing or shrinking as needed. Returns the new length of the array. When growing, new elements are zero-initialised for numeric types, or set to `nil` for reference types (strings, tables). When shrinking, excess elements are discarded and references are cleared for garbage collection. ```lua arr = array { 1, 2, 3 } array.resize(arr, 7) -- arr = {1, 2, 3, 0, 0, 0, 0} array.resize(arr, 2) -- arr = {1, 2} array.resize(arr, 0) -- arr = {} (equivalent to clear) ``` External arrays (wrapping C/C++ memory) and cached string arrays cannot grow and will raise an error. Read-only arrays cannot be resized. ### array.fill() ``` arr:fill(Value, [Start], [Count]) arr:fill(Value, Range) ``` Fills array elements with a value. `Value` is the value to fill with. `Start` is a starting index (default: 0). `Count` is the number of elements to fill (default: all remaining). `Range` is a range object specifying which elements to fill. ```lua arr = array arr:fill(0) -- Fill entire array with zeros arr:fill(99, 3, 5) -- Fill elements 3-7 with value 99 arr:fill(42, {2 to 6}) -- Fills indices 2, 3, 4, 5 ``` ### array.insert() `newLength = arr:insert(Index, Value, ...)` Inserts one or more values at the specified index, shifting subsequent elements to make room. The array grows automatically if needed. `Index` refers to the position to insert at. Must be between 0 and the array length (inclusive). `Value` is one or more values to insert, which must match the array's element type. ```lua -- Insert multiple values arr = array { 1, 5 } arr:insert(1, 2, 3, 4) -- arr = {1, 2, 3, 4, 5} ``` ### array.remove() `newLength = arr:remove(Index [, Count])` Removes one or more elements at the specified `Index`, shifting subsequent elements down. Count is the number of elements to remove (defaults to 1) and is automatically limited to available elements. ```lua -- Remove multiple elements arr = array { 1, 2, 3, 4, 5 } arr:remove(1, 3) -- arr = {1, 5}, returns 2 ``` A `Count` of 0 does nothing and returns the current length. Negative `Count` values raise an error. ### array.reverse() `arr:reverse()` Reverses the array elements in place. ```lua arr = array for i = 0, 4 do arr[i] = i end -- {0, 1, 2, 3, 4} arr:reverse() -- {4, 3, 2, 1, 0} ``` ### array.sort() `arr:sort([Descending])` Sorts the array elements in place. If `Descending` is `true`, sort in descending order (default: `false` for ascending). ```lua arr = array arr[0] = 30; arr[1] = 10; arr[2] = 50; arr[3] = 20; arr[4] = 40 arr:sort() -- {10, 20, 30, 40, 50} ``` ### Array Navigation Methods ### array.contains() `bool = arr:contains(Value)` Returns `true` if the specified value exists in the array, `false` otherwise. This is a convenience wrapper around `find()` that returns a boolean instead of an index. String comparisons are case-sensitive. ### array.first() `value = arr:first()` Returns the first element of the array, or `nil` if the array is empty. Provides bounds-safe access without risking index-out-of-bounds errors. This method is equivalent to `arr[0]` but returns `nil` for empty arrays instead of raising an error. ### array.last() `value = arr:last()` Returns the last element of the array, or `nil` if the array is empty. Provides bounds-safe access without risking index-out-of-bounds errors. This method is equivalent to `arr[#arr - 1]` but returns `nil` for empty arrays instead of raising an error. ### array.find() ``` index = arr:find(Value, [Start], [End]) index = arr:find(Value, Range) ``` Searches for a `Value` in the array. `Start` is a starting index for search (defaults to 0). `End` is an ending index, exclusive (defaults to array length). `Range` is a range object specifying the search bounds. The index where the value was found is returned, or `nil` if not found. ```lua arr = array for i = 0, 9 do arr[i] = i * 10 end -- Search starting from index 6 idx = arr:find(50, 6) -- Returns nil (50 is at index 5) -- Search within a range idx = arr:find(30, {0 to 5}) -- Returns 3 ``` ### Array Data Extraction Methods ### array.copy() `arr:copy(Source, [DestIndex], [SrcIndex], [Count])` Copies data from a source into the array. `Source` is an array, string or table. `DestIndex` is a starting index in destination array (default: 0). `SrcIndex` is a starting index in source (default: 0). `Count` is the number of elements to copy (default: all remaining). ```lua -- Copy from another array src = array dst = array dst:copy(src, 5, 0, 10) -- Copy 10 elements from src[0] to dst[5] -- Copy from a string bytes = array bytes:copy('Hello', 0) -- Copy from a table arr = array arr:copy({10, 20, 30, 40, 50}) ``` ### array.getString() `str = arr:getString([Start], [Length])` Extracts a substring from a byte/char array. `Start` is the starting byte index (0-based). `Length` is the number of bytes to extract (defaults to all). ```lua arr = array.new('Hello World') print(arr:getString(0, 5)) -- Prints 'Hello' print(arr:getString(6)) -- Prints 'World' ``` ### array.setString() `count = arr:setString(String, [Start])` Copy `String` content into a byte array. `Start` is a starting index in the array (default: 0). The number of bytes written is returned. ```lua arr = array arr:setString('Hello', 0) arr:setString(' World', 5) print(arr:getString(0, 11)) -- Prints 'Hello World' ``` ### array.slice() `new_arr = arr:slice(Range)` Creates a new array containing a subset of elements. `Range` is a range object specifying which elements to extract. A new array containing the selected elements is returned. ```lua arr = array for i = 0, 9 do arr[i] = i * 10 end -- {0, 10, 20, 30, 40, 50, 60, 70, 80, 90} sub = arr:slice({2 to 5}) -- {20, 30, 40} sub = arr:slice({5 into 2}) -- {50, 40, 30, 20} Reverse slice sub = arr:slice({-3 into -1}) -- {70, 80, 90} Negative inclusive slice sub = arr:slice({-3 to -1}) -- {70, 80} Negative exclusive slice ``` ### array.concat() `str = arr:concat([Separator], [Format], [Start], [End])` Concatenates array elements into a string. `Separator` is a string to insert between elements (default: empty string). `Format` is an optional printf-style format string for each element (e.g., `'%d'`, `'%.2f'`). If `Format` is not specified, the most efficient conversion path is used. `Start` and `End` are optional zero-based, inclusive indexes that limit the elements to concatenate. ```lua arr = array for i = 0, 4 do arr[i] = i * 10 end str = arr:concat(', ', '%d') -- Result: '0, 10, 20, 30, 40' farr = array { 1.5, 2.75, 3.125 } str = farr:concat(' | ', '%.2f') -- Result: '1.50 | 2.75 | 3.13' names = array { 'apple', 'banana', 'cherry' } str = names:concat(', ') -- Result: 'apple, banana, cherry' numbers = array { 1, 2, 3, 4, 5 } str = numbers:concat('-') -- Result: '1-2-3-4-5' str = numbers:concat('-', nil, 1, 3) -- Result: '2-3-4' ``` ### array.clone() `copy = arr:clone()` Creates a copy of the array. For primitive types (int, float, etc.), this is a deep copy. For reference types (strings, tables), the references are copied (shallow copy). ### Array Functional Methods ### array.each() `arr:each(Callback)` Iterates over array elements, calling the `Callback` function for each element. The callback receives `(value, index)` as arguments. Returns the original array for method chaining. ```lua -- Basic iteration arr = array { 10, 20, 30 } arr:each(function(v, i) print('Index ' .. i .. ': ' .. v) end) -- Output: Index 0: 10, Index 1: 20, Index 2: 30 -- Using arrow function syntax arr:each(v => print(v)) -- Chaining arr:each(v => print('Processing:', v)) :each(v => log('Logged:', v)) -- With statements in arrow function body total = 0 arr:each(v => do total += v end) print(total) -- 60 ``` ### array.map() `new_array = arr:map(Transform)` Returns a new array with each element transformed by the given `Transform` function. The original array is not modified. ```lua -- Double all values arr = array { 1, 2, 3, 4, 5 } doubled = arr:map(v => v * 2) -- String transformation names = array { 'hello', 'world' } upper = names:map(s => s:upper()) -- Chaining map operations result = arr:map(v => v * 2):map(v => v + 1) ``` ### array.filter() `new_array = arr:filter(predicate)` Returns a new array containing only elements that satisfy the `Predicate` function. Elements for which the predicate returns a `true` value are included. ```lua -- Filter even numbers arr = array { 1, 2, 3, 4, 5, 6 } evens = arr:filter(v => v % 2 is 0) -- evens = {2, 4, 6} -- Filter by index arr = array { 10, 20, 30, 40, 50 } odd_indices = arr:filter((v, i) => i % 2 is 1) -- odd_indices = {20, 40} -- String filtering names = array { 'apple', 'banana', 'apricot', 'cherry' } a_words = names:filter(s => s:startsWith('a')) -- a_words = {'apple', 'apricot'} -- Chained filtering large_evens = arr:filter(v => v % 2 is 0):filter(v => v > 10) ``` ### array.reduce() `result = arr:reduce(Initial, Reducer)` Folds all array elements into a single accumulated value. The reducer function is called for each element, receiving the current accumulator, the element value, and the element index. `Initial` is the initial value for the accumulator (any type). `Reducer` is a function receiving `(accumulator, value, index)` and returning the new accumulator. ```lua -- Sum all elements arr = array { 1, 2, 3, 4, 5 } sum = arr:reduce(0, (acc, v) => acc + v) -- sum = 15 -- Product of elements product = arr:reduce(1, (acc, v) => acc * v) -- product = 120 -- String concatenation parts = array { 'a', 'b', 'c' } result = parts:reduce('', (acc, v) => acc .. v) -- result = 'abc' -- Build a table arr = array. { 1, 2, 3 } squares = arr:reduce({}, function(acc, v, i) acc[i] = v * v return acc end) -- squares = {[0]=1, [1]=4, [2]=9} -- Find maximum arr = array { 3, 1, 4, 1, 5, 9, 2, 6 } max = arr:reduce(arr:first(), (acc, v) => v > acc ? v :> acc) -- max = 9 ``` ### array.any() `bool = arr:any(Predicate)` Returns `true` if any element in the array satisfies the `Predicate` function. Short-circuits on the first match, returning immediately without checking remaining elements. The `Predicate` arguments are `(value, index)` and must return a boolean. ```lua -- Check if any element is even arr = array { 1, 3, 5, 6, 7 } has_even = arr:any(v => v % 2 is 0) -- has_even = true (found 6) -- Check if any element is negative arr = array { 1, 2, 3, 4, 5 } has_negative = arr:any(v => v < 0) -- has_negative = false -- String array names = array { 'apple', 'banana', 'cherry' } has_long = names:any(s => #s > 6) -- has_long = true (banana, cherry) ``` ### array.all() `bool = arr:all(Predicate)` Returns `true` if all elements in the array satisfy the `Predicate` function. Short-circuits on the first failure, returning immediately without checking remaining elements. The `Predicate` arguments are `(value, index)` and must return a boolean. ```lua -- Check if all elements are positive arr = array { 1, 2, 3, 4, 5 } all_positive = arr:all(v => v > 0) -- all_positive = true -- String validation names = array { 'abc', 'def', 'ghi' } all_short = names:all(s => #s is 3) -- all_short = true ``` ### Chaining Functional Methods The functional methods can be chained together to create data processing pipelines: ```lua -- Filter, then map, then reduce arr = array { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } result = arr:filter(v => v % 2 is 0) -- {2, 4, 6, 8, 10} :map(v => v * 10) -- {20, 40, 60, 80, 100} :reduce(0, (acc, v) => acc + v) -- 300 -- Check conditions on transformed data has_large = arr:map(v => v * 2):any(v => v > 15) all_small = arr:filter(v => v < 5):all(v => v < 10) -- Use each() in a chain (returns original array) arr:each(v => log('Before:', v)) :map(v => v * 2) :each(v => log('After:', v)) ``` --- ## Logging The following functions are included as standard so that messages can be logged to the console. ### msg() `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. If the log-level is not high enough to display the message, the call is silently discarded during parsing to maximise efficiency. ### print() `print()` accepts a single string parameter and prints it to `stdout`. If `stdout` is unavailable on a system like Android, the message is printed to the debug log instead. *For targeted printing, use `io.write()` and target `stderr` or `stdout` instead of the `print()` function.* --- ## Annotations Tiri uses the `@` prefix for two related parser features: parser annotation expressions and function annotations. Parser annotation expressions expand to literal values during parsing. Function annotations attach metadata to functions at parse time, providing a declarative way to mark functions with attributes that can be queried at runtime for features such as test discovery, deprecation warnings, and capability requirements. ### Parser Annotation Expressions Parser annotation expressions are compile-time expressions that resolve to literal values while the source is parsed. They can be used anywhere an expression is valid. The generated bytecode receives the resolved string or number literal, so no runtime metadata lookup is required. |Expression|Type|Resolved value| |-|-|-| |`@FunctionName`|`str`|The enclosing function name.| |`@SourceFile`|`str`|The current source file path.| |`@SourceLine`|`num`|The 1-based source line number of the annotation expression.| For `@FunctionName`, top-level code resolves to `"Main"`. Named function declarations use the declared function name. Method declarations use the method name, not the table or object path. Anonymous function expressions, anonymous thunk expressions, and arrow functions resolve to `"Anonymous"`. ```lua top_level_name = @FunctionName -- "Main" top_level_file = @SourceFile top_level_line = @SourceLine function logLocation() print(f"{@FunctionName} in {@SourceFile}:{@SourceLine}") end function widget:update() return @FunctionName -- "update" end callback = () => @FunctionName -- "Anonymous" ``` Only the parser annotation names listed above are valid in expression position. Other `@Name` forms are declaration annotations and must appear before a function declaration or in the syntax documented below. ### Annotation Syntax Annotations use the `@` prefix and are placed immediately before a function declaration: ```lua @AnnotationName function myFunction() -- function body end ``` Annotations can include named arguments in parentheses: ```lua @Test(name="My Test Case", timeout=5) function testSomething() -- test body end ``` **Supported Argument Types:** |Type|Example| |-|-| |String|`name='Test Name'`| |Number|`timeout=5.0`, `priority=1`| |Boolean|`enabled=true`, `network=false`| |Array|`labels=['unit', 'smoke', 'critical']`| |Bare Identifier|`deprecated` (equivalent to `deprecated=true`)| **Multiple Annotations:** Multiple annotations can be stacked on a single function: ```lua @Test(name='Network Test') @Requires(network=true) function testNetworkFeature() -- test body end ``` Annotations can also be placed on the same line using semicolons: ```lua @BeforeEach; @Requires(network=true) function setupNetwork() -- setup body end ``` **Function Types:** Annotations work with all function declaration styles: ```lua -- Regular functions @Test function regularFunc() end -- Local functions @Test local function localFunc() end -- Global functions @Test global function globalFunc() end ``` ### Documentation Annotation The `@Doc` annotation attaches source documentation to a function. It is primarily intended for tooling such as the Tiri LSP and API documentation extraction. When `debug.validate(Source, "symbols")` is used, the parser returns a `symbols` table that includes the function signature, source span, annotations, parameter types, result types, and structured documentation parsed from `@Doc(text=...)`. ```lua source = [=[ @Doc(text=[[ Returns a greeting. -INPUT- Name: Person to greet -RESULTS- str: Greeting text -ERRORS- Args: If the name is invalid ]]) function greet(Name: str):str return "Hello " .. Name end ]=] result = debug.validate(source, "symbols") symbol = result.symbols[0] print(symbol.signature) -- greet(Name: str):str print(symbol.doc.summary) -- Returns a greeting. print(symbol.params[0].type) -- str print(symbol.params[0].doc) -- Person to greet ``` Leading whitespace is removed from each documentation line before parsing, so `@Doc` blocks may be indented with the function they document. **Long Form:** The long form uses marker sections. Text before the first marker becomes the summary/body text. Marker names are matched exactly. |Marker|Line Format|Description| |-|-|-| |`-INPUT-`|`Name: Description`|Documents a parameter. Signature names and types are merged into the symbol data.| |`-RESULTS-`|`type: Description`|Documents a result value. May be repeated for multi-result functions.| |`-ERRORS-`|`Code: Description`|Documents an error code that may be thrown or returned.| |`-EXAMPLE-`|free-form text|Provides example code until the next marker or end of the documentation block.| **Short Form:** If the first non-empty documentation line starts with `@`, the compact parser is used. Compact entries are strictly one line each; descriptions and examples cannot span multiple lines. ```lua @Doc(text=[[ @desc Returns a greeting. @input Person to greet @result Greeting text @result Additional argument @error Args: If the name is invalid @error Failed: Random failure ]]) function greetCompact(Name: str): return "Hello " .. Name, "extra" end ``` |Entry|Line Format|Description| |-|-|-| |`@desc`|`@desc Description`|Sets the function summary.| |`@input`|`@input Description`|Documents the next parameter in function declaration order.| |`@result`|`@result Description`|Documents one result value. Types are taken from the signature or inferred from the function body, falling back to `any`.| |`@error`|`@error Code: Description`|Documents one error code.| For normal script execution, large `@Doc` payloads are omitted from runtime annotation registration unless the script is configured to process documentation. Tooling that calls `debug.validate(Source, "symbols")` enables documentation processing for that validation pass. ### Compile-Time Pre-Processor The `@if ... @end` construct provides a compile-time pre-processor, allowing code to be included or excluded based on conditions evaluated during parsing. This allows you to quickly eliminate the compilation of bytecode that the program doesn't need. **Basic Syntax:** ```lua @if(condition=value) -- Code included only when condition matches @end ``` **Supported Conditions:** |Condition|Value Type|Description| |-|-|-| |`imported`|boolean|`true` when file is being imported; `false` when file is the main script.| |`debug`|boolean|`true` when log level is higher than 'warning'; `false` otherwise.| |`platform`|string|Matches against current platform: `"windows"`, `"linux"`, `"osx"`, `"native"`.| |`exists`|string|Checks for existence of a file at compile time: `exists='path/to/file'`. The path is calculated relative to the script.| **Examples:** ```lua -- Platform-specific code @if(platform="windows") path_separator = "\\" @end @if(platform="linux") path_separator = "/" @end -- Debug-only code (only included when logging is enabled) @if(debug=true) print("Debug: Entering critical section") @end ``` **Nesting:** Compile-time conditionals can be nested: ```lua @if(imported=false) @if(platform="windows") print("Running as main script on Windows") @end @end ``` ### Standard Annotations The following annotations are recommended for general-purpose code organisation: |Annotation|Arguments|Description| |-|-|-| |`@Doc`|`text:str`|Attaches parser-readable documentation to a function for LSP support and documentation extraction.| |`@Deprecated`|`message:str`, `since:str`|Marks a function as deprecated. Tools may emit warnings when deprecated functions are called.| |`@Override`|(none)|Indicates that a function overrides a parent implementation. Useful for documentation and tooling.| |`@SuppressWarnings`|``|Suppresses specific warnings. Flags are bare identifiers: `@SuppressWarnings(unused, deprecated)`| **Examples:** ```lua @Deprecated(message='Use newApi() instead', since='2.0') function oldApi() return newApi() end @Override function customBehaviour() -- Override parent implementation end @SuppressWarnings(unused, experimental) function internalHelper() -- Implementation end ``` ### Test Framework Annotations The Flute test framework recognises the following annotations for test discovery and configuration: |Annotation|Arguments|Description| |-|-|-| |`@Test`|`name:str`, `timeout:num`, `priority:num`, `labels:array`|Marks a function as a test case| |`@BeforeEach`|(none)|Runs before each test in the file| |`@AfterEach`|(none)|Runs after each test in the file| |`@BeforeAll`|(none)|Runs once before all tests in the file| |`@AfterAll`|(none)|Runs once after all tests in the file| |`@Disabled`|`reason:str`|Skips the test with an optional reason| |`@Requires`|`display:bool`, `network:bool`, `audio:bool`|Specifies runtime requirements; test is skipped if requirements are not met| **Test Argument Reference:** |Argument|Type|Default|Description| |-|-|-|-| |`name`|string|function name|Display name for the test| |`timeout`|number|3.0|Maximum execution time in seconds| |`priority`|number|0|Execution order (lower runs first)| |`labels`|array|`[]`|Tags for filtering tests (e.g., `['unit', 'smoke']`)| **Requirements Reference:** |Requirement|Description| |-|-| |`audio`|Requires working audio module| |`display`|Requires working display module| |`font`|Requires working font module| |`network`|Requires working network module| |`ssl`|Requires SSL to be built-in to network module| **Examples:** ```lua @Test(name='User Login', labels=['integration', 'auth']) @Requires(network=true) function testUserLogin() -- Test implementation end @BeforeEach function setupTestEnvironment() -- Runs before each test end @Disabled(reason='Pending implementation') @Test function testFutureFeature() -- Will be skipped end ``` ### debug.fileSources Interface The `debug.fileSources()` function provides access to the file source tracking system, which maintains metadata about all source files involved in the current script execution. This is particularly useful for debugging, tooling, and understanding import hierarchies. #### debug.fileSources() `array = debug.fileSources()` Returns an array of all registered file sources for the current script execution. Each entry in the returned array contains the following fields: |Field|Type|Description| |-|-|-| |`index`|number|File index (0 = main file, 255 = overflow fallback)| |`path`|string|Full resolved path to the source file| |`filename`|string|Short filename for error display| |`namespace`|string|Declared namespace (empty string if none)| |`firstLine`|number|First line number in unified line space| |`sourceLines`|number|Total number of lines in the source file| |`parentIndex`|number|Index of the file that imported this one (0 for main)| |`importLine`|number|Line number in parent where import occurred (0 for main)| |`isOverflow`|boolean|True if this is the overflow fallback entry (index 255)| **Example - List All File Sources:** ```lua sources = debug.fileSources() print("File sources:") for i = 0, #sources - 1 do src = sources[i] print(f" [{src.index}] {src.filename}") print(f" path: {src.path}") if src.namespace != "" then print(f" namespace: {src.namespace}") end if src.parentIndex != 0 or src.importLine != 0 then print(f" imported from [{src.parentIndex}] at line {src.importLine}") end end ``` **Example - Correlate with debug.getInfo():** The `debug.getInfo()` function returns a `fileIndex` field when the 'S' option is used. This index can be used to look up the corresponding file source: ```lua sources = debug.fileSources() info = debug.getInfo(1, "S") -- Get info about current function if info.fileIndex then local src = sources[info.fileIndex] print(f"Current function is in: {src.filename}") print(f"Full path: {src.path}") end ``` **Notes:** - The main script file is always at index 0 - Imported files are assigned indices 1-254 in the order they are first encountered - Index 255 is reserved as an overflow fallback when the file limit is exceeded - File sources are deduplicated; importing the same file multiple times returns the same index - The `fileIndex` field in `debug.getInfo()` results corresponds to indices in this array ### debug.anno Interface The `debug.anno` interface provides programmatic access to function annotations at runtime. #### debug.anno.set(func, annotations, [source], [name]) Registers annotations for a function. Returns the created entry table. **Parameters:** |Parameter|Type|Description| |-|-|-| |`func`|function|The function to annotate| |`annotations`|string or table|Annotation data (see below)| |`source`|string|Source file path (default: `''`)| |`name`|string|Function name (default: inferred or `''`)| **Annotation Formats:** *Table format:* ```lua debug.anno.set(myFunc, { { name = 'Test', args = { name = 'My Test', labels = { 'unit' } } }, { name = 'Requires', args = { network = true } } }, 'myfile.tiri', 'myFunc') ``` *String format (parsed):* ```lua debug.anno.set(myFunc, '@Test(name='My Test'); @Requires(network=true)') ``` **Returns:** Entry table with fields: - `name`: Function name - `source`: Source file path - `annotations`: Array of annotation tables #### debug.anno.get(func) Retrieves the annotation entry for a function. **Parameters:** |Parameter|Type|Description| |-|-|-| |`func`|function|The function to query| **Returns:** Entry table if the function is annotated, `nil` otherwise. ```lua @Test(name='Example') function exampleFunc() end entry = debug.anno.get(exampleFunc) if entry then print('Function: ' .. entry.name) print('Source: ' .. entry.source) for i, anno in ipairs(entry.annotations) do print('Annotation: ' .. anno.name) end end ``` #### debug.anno.list() Returns a shallow copy of all registered annotations. **Returns:** Table mapping function references to their entry tables. ```lua all = debug.anno.list() for func, entry in pairs(all) do print(entry.name .. ' has ' .. #entry.annotations .. ' annotations') end ``` **Entry Table Structure:** ```lua { name = 'functionName', -- Function name source = 'path/to/file.tiri', -- Source file annotations = { -- Array of annotations { name = 'Test', -- Annotation name args = { -- Named arguments name = 'Test Name', timeout = 5, labels = { 'unit', 'smoke' } } } } } ``` --- ## Event Subsystem Tiri 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 Tiri 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 Kōtuku 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)` --- ## Credits Tiri is developed by Paul Manias and runs on Mike Pall's [LuaJIT](https://luajit.org/) framework, which in turn is based on the [Lua](https://lua.org/) 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.