Gotchas & Edge Cases
This page documents important Jyro behaviours and the correct way to handle them.
1. Empty arrays and objects are truthy
Jyro treats all arrays and objects as truthy - even empty ones. This means an if check on a collection always passes, regardless of whether it contains any elements.
Check the length explicitly instead:
# INCORRECT - always enters the block, even when Data.items is []
if Data.items then ... end
# CORRECT - tests whether the array actually has elements
if Length(Data.items) > 0 then ... end
2. ToNumber() returns null for invalid input
When ToNumber cannot parse a string as a number it returns null, not 0. Code that assumes a numeric result without checking will propagate null through subsequent calculations.
Always test the return value before using it:
var num = ToNumber(Data.input)
if num is null then
fail "Invalid number: " + Data.input
end
3. Division by zero terminates the script
Division or modulo by zero throws an immediate runtime error that terminates the script.
Guard every division with a zero check:
if count != 0 then
Data.average = total / count
else
Data.average = 0
end
4. All array functions return new arrays
Every standard library function that operates on an array returns a new array. The original is never mutated. Assign the return value to capture the result.
Assign the result back to the variable:
var items = [3, 1, 2]
Sort(items) # items is STILL [3, 1, 2] - result discarded
var sorted = Sort(items) # sorted is [1, 2, 3]
var items = [1, 2]
Append(items, 3) # items is STILL [1, 2] - result discarded
items = Append(items, 3) # NOW items is [1, 2, 3]
5. Cannot modify a collection during iteration
Reassigning the array that a foreach loop is iterating over causes a runtime error. The iterator holds a reference to the original collection and detects the mutation.
Collect changes into a separate array, then apply them after the loop:
# INCORRECT - runtime error: modifying the iterated collection
foreach item in Data.items do
Data.items = Append(Data.items, item)
end
# CORRECT - accumulate into a separate array, merge afterwards
var additions = []
foreach item in Data.items do
if item.shouldDuplicate then
additions = Append(additions, item)
end
end
foreach addition in additions do
Data.items = Append(Data.items, addition)
end
6. Nested properties need intermediate objects
Assigning to a deeply nested path like Data.a.b.c throws a runtime error if the intermediate objects (Data.a, Data.a.b) do not already exist. Property access on a missing key returns null, and attempting to set a property on null (or any non-object type) is a runtime error. Jyro does not auto-create parent objects on assignment (i.e. no autovivification).
Create each level explicitly, or assign a nested literal:
# INCORRECT - runtime error: cannot set property on null
Data.deep.nested.value = "x"
# CORRECT - build the path one level at a time
Data.deep = {}
Data.deep.nested = {}
Data.deep.nested.value = "x"
# ALSO CORRECT - assign the whole structure as a literal
Data.deep = {"nested": {"value": "x"}}
7. Missing then after if/elseif/case/default
The then keyword is required after every if condition, elseif condition, case value list, and default keyword. Omitting it causes a parse error.
# INCORRECT - parse error
if x > 0
...
end
# CORRECT
if x > 0 then
...
end
8. Missing do after while/for/foreach
Loop statements require the do keyword after the condition or iteration clause. Omitting it causes a parse error.
# INCORRECT - parse error
while x > 0
...
end
# CORRECT
while x > 0 do
...
end
9. Missing end for blocks
Jyro is explicit about block delimiters. Every if, while, for, foreach, and switch block must be closed with end. Inside a switch, each case and default block is implicitly terminated by the next case, default, or the closing end. A missing end causes a parse error at the end of the script or at the start of the next block.
10. No string interpolation
Jyro does not support template syntax like ${name} or {name} inside strings. These are treated as literal characters, not variable references. Use the + operator to concatenate values into strings:
# INCORRECT - produces the literal text "${name}", not the variable's value
"Hello ${name}"
"Hello {name}"
# CORRECT - concatenation converts the variable to a string
"Hello " + name
11. Min()/Max()/Sum()/Average() take arrays, not multiple arguments
The aggregate math functions each accept a single array argument, not variadic arguments.
Wrap the values in an array literal:
# INCORRECT - too many arguments
Sum(1, 2, 3)
Min(a, b)
# CORRECT - pass a single array
Sum([1, 2, 3])
Min([a, b])
12. Merge() takes an array of objects
Like the aggregate math functions, Merge accepts a single array of objects to merge together. It does not accept multiple object arguments.
# INCORRECT - too many arguments
Merge(obj1, obj2)
# CORRECT - pass an array of objects (later entries override earlier ones)
Merge([obj1, obj2])
13. GroupBy() takes a field name string, not a lambda
GroupBy groups objects by a named field and expects a plain string, not a lambda expression. This is consistent with other field-based query functions like WhereByField and SortByField.
# INCORRECT - GroupBy does not accept lambdas
GroupBy(arr, x => x.category)
# CORRECT - pass the field name as a string
GroupBy(arr, "category")
14. Regex patterns - no \d, \w, \s
Jyro’s string lexer only recognises the escape sequences \n, \r, \t, \\, \", \', \0, and \uXXXX. Any other backslash sequence - including \d, \w, and \s - causes a parse error. Use equivalent character classes instead:
# INCORRECT - parse error: \d is not a recognised escape sequence
RegexMatch(text, "\d+")
# CORRECT - character class reaches the regex engine intact
RegexMatch(text, "[0-9]+")
See Strings - Regex Pattern Escaping for the full substitution table.
15. exit/fail message must be on the same line
The exit and fail keywords accept an optional message, but it must appear on the same line. A string on the next line is parsed as a separate expression statement, not as the message. The same rule applies to return inside functions.
# INCORRECT - "message" is a standalone expression, not the exit message
exit
"message"
# CORRECT - message on the same line
exit "message"
16. for loop bounds are exclusive
The upper bound of to and the lower bound of downto are exclusive, similar to < and > respectively. The loop variable never reaches the boundary value. To include the boundary, adjust it by one:
for i in 0 to 5 do # iterates: 0, 1, 2, 3, 4 (NOT 5)
...
end
for i in 5 downto 0 do # iterates: 5, 4, 3, 2, 1 (NOT 0)
...
end
# To include 5:
for i in 0 to 5 + 1 do # iterates: 0, 1, 2, 3, 4, 5
...
end
17. Function names are PascalCase and case-sensitive
All standard library functions use PascalCase naming (e.g. ToUpper, WhereByField). Function lookup is case-sensitive, so toupper or toUpper will fail with an “unknown function” error.
# INCORRECT - case mismatch
toupper("hello")
toUpper("hello")
# CORRECT
ToUpper("hello")
Return values on empty or missing data
| Function | Condition | Returns |
|---|---|---|
First(arr) | Empty array | null |
Last(arr) | Empty array | null |
IndexOf(arr, val) | Not found | -1 |
WhereByField(...) | No matches | [] (empty array) |
FindByField(...) | No match | null |
Find(arr, fn) | No match | null |
AnyByField(...) | Empty array | false |
AllByField(...) | Empty array | true (vacuous truth) |
Any(arr, fn) | Empty array | false |
All(arr, fn) | Empty array | true (vacuous truth) |
Select(...) | Empty array | [] |
SelectMany(...) | Empty array | [] |
Project(...) | Empty array | [] |
Omit(...) | Empty array | [] |
Merge(arr) | Empty array | {} |
Min(arr) | No numbers | null |
Max(arr) | No numbers | null |
Average(arr) | No numbers | null |
Median(arr) | No numbers | null |
Mode(arr) | No numbers | null |
Sum(arr) | No numbers | 0 |
ToNumber(s) | Invalid string | null |
RegexMatch(...) | No match | null |
RegexMatchAll(...) | No matches | [] |
RegexMatchDetail(...) | No match | null |
RandomChoice(arr) | Empty array | null |
Case sensitivity
- String comparisons (
==,!=,<,>) are case-sensitive Contains,StartsWith,EndsWithare case-sensitive- Query function comparisons are case-sensitive
- Use
ToLower()orToUpper()for case-insensitive matching
Sort order for mixed types
Sort groups elements by type, then sorts within each group:
nullvalues (first)- Numbers (ascending)
- Strings (lexicographic)
- Booleans (
falsebeforetrue)
Split() preserves empty strings
The Split function preserves empty strings produced by adjacent or leading/trailing delimiters:
Split("a,,b", ",") # ["a", "", "b"]
Split(",a,b,", ",") # ["", "a", "b", ""]
18. return is only valid inside functions
return exits a user-defined function. Using return at the script’s top level is a compiler error. Use exit for clean script termination and fail for error termination:
# INCORRECT - compiler error: return outside a function
return "done"
# CORRECT - use exit at the top level
exit "done"
19. Functions cannot access Data
User-defined functions are pure - they cannot read or write Data. This is enforced by the compiler. Pass the values you need as arguments:
# INCORRECT - compiler error: cannot access Data inside a function
func Bad()
return Data.value
end
# CORRECT - pass the value as a parameter
func Good(value)
return value * 2
end
Data.result = Good(Data.value)
20. Functions cannot capture variables (no closures)
Functions only see their own parameters and locally declared variables. Variables from the enclosing script are not visible:
var multiplier = 3
# INCORRECT - compiler error: 'multiplier' is not declared
func Scale(x)
return x * multiplier
end
# CORRECT - pass it as a parameter
func Scale(x, multiplier)
return x * multiplier
end
Lambdas do capture variables from the surrounding scope. See Lambda Expressions for the comparison.
21. Functions and unions must be declared at the top level
func and union declarations must appear at the script’s top level - not inside if blocks, loops, or other functions:
# INCORRECT - compiler error
if Data.mode == "advanced" then
func Helper(x)
return x * 2
end
end
# CORRECT - declare at the top level
func Helper(x)
return x * 2
end
22. Variant names are globally unique
Each variant name in a union must be unique across all unions in the script. Variant names also cannot collide with built-in or user-defined function names, because constructors are registered as functions:
# INCORRECT - 'Round' collides with built-in function
union Shape
Round(radius: number) # OK - no collision
end
# INCORRECT - 'Circle' in two unions
union Shape2D
Circle(r: number)
end
union Shape3D
Circle(r: number) # Error: duplicate variant name
end
23. match has no default - exhaustiveness is required
Unlike switch, match has no default case. Every variant of the union must be handled. If you forget a variant, the compiler reports an error:
union Direction
North()
South()
East()
West()
end
# INCORRECT - compiler error: missing variants South, West
match heading do
case North() then ...
case East() then ...
end
24. match bindings are positional, not by name
In a match case, bindings map to fields by position, not by name. The names you use in the case can differ from the field names in the union declaration:
union Rect
Rect(width: number, height: number)
end
# 'w' binds to the first field (width), 'h' to the second (height)
match shape do
case Rect(w, h) then w * h
end