Expression Evaluation
Expression Evaluation
The expression evaluator is the engine at the heart of every MUSH server. It transforms strings containing literal text, function calls, and substitution codes into their final output. Understanding how evaluation works is essential for writing correct softcode.
When Evaluation Happens
Not every string is evaluated. The server evaluates strings in specific contexts:
- Action lists on attributes like
ACONNECT,AHEAR,ADESC, and user-defined$-commands. - Function arguments inside
[]brackets. - The right-hand side of commands like
@pemit,@emit,think. - Lock evaluation keys of type
/(eval locks).
Some commands deliberately do not evaluate their arguments. @wait, @switch, and @trigger defer evaluation to the queued command. This prevents injection attacks where a player’s input containing ; or ] could break out of the intended command.
Special Characters
The evaluator recognizes these characters:
| Character | Meaning |
|---|---|
% | Begins a substitution code (%r, %#, %0, etc.) |
[ | Begins a function call. Everything between [ and the matching ] is evaluated and the result is substituted in place. |
] | Ends a function call. |
{ | Begins a literal group. Contents are passed without further evaluation (braces are stripped). Used to protect commas and spaces in function arguments. |
} | Ends a literal group. |
( | Begins a function’s argument list after the function name. |
) | Ends a function’s argument list. |
, | Separates function arguments. |
\ | Escapes the next character, preventing it from being interpreted specially. |
; | Separates commands in an action list (outside of evaluation). |
Evaluation Order
When the evaluator processes a string, it scans left to right:
- Literal text is copied directly to the output.
%-codes are recognized and replaced with their values (see Substitution).[triggers recursive evaluation: the evaluator finds the matching], evaluates the contents, and substitutes the result. This is how function calls work:[strlen(hello)]evaluatesstrlen(hello)which produces5.{}protect their contents from evaluation at the current level. The braces are stripped, but the contents are passed through as-is. This is critical for functions likeswitch()anditer()where you want to delay evaluation of some arguments.\causes the next character to be treated as literal text.
Nesting and Recursion
Function calls can nest to arbitrary depth:
[add(1,[mul(2,3)])]
The evaluator processes this inside-out: mul(2,3) evaluates to 6, then add(1,6) evaluates to 7.
Each level of nesting consumes stack space. TinyMUX enforces a function nesting limit (default 500 levels) to prevent runaway recursion from crashing the server.
Space Compression
By default, the evaluator compresses runs of multiple spaces into a single space. This is a historical behavior from early MUSH servers. To preserve exact spacing, use %b (explicit space) instead of literal spaces, or use functions like space().
Braces and Delayed Evaluation
Braces are one of the most important concepts in softcode. Consider:
@switch [get(me/score)]=
0, {think You have no points.},
*, {think You have [get(me/score)] points.}
Without braces, both think commands would be evaluated immediately when @switch processes its arguments, before the switch comparison happens. With braces, only the matching branch is evaluated.
The AST Cache
TinyMUX optimizes evaluation performance by caching parsed expressions as Abstract Syntax Trees (ASTs). When the same attribute is evaluated repeatedly (common for $-commands and ^listen patterns), the server reuses the cached parse tree instead of re-parsing the string. This provides significant performance improvement for frequently-executed code.
Function Invocation Limits
To prevent denial-of-service through expensive softcode, the server tracks the number of function invocations per command. If the count exceeds the configured limit, evaluation is halted and an error is returned. This limit applies per top-level command, not per function call, so deeply nested expressions share the same budget.
Common Pitfalls
Double evaluation: If you store a string containing [ ] in an attribute and then evaluate it with get(), the brackets will be evaluated. Use xget() or v() when you want the raw value.
Comma in arguments: A bare comma inside a function call is treated as an argument separator. Use braces to protect literal commas: [setq(0,{red, white, and blue})].
Semicolons: A semicolon in an action list separates commands. Inside function arguments, semicolons are harmless, but in @trigger or @force arguments, they can cause command injection. Use @trigger instead of @force when passing untrusted data.
Related Topics: Substitution, Command, Functions.