This document describes the complete evaluation process for Superscript expressions in the Superscript library, including parsing, variable retrieval, normalization, and transformation steps.
The Superscript evaluation system processes expressions through several key stages:
- Input Parsing - Parse JSON execution context and Superscript expression string
- Variable Normalization - Transform string values to appropriate types
- AST Transformation - Apply null-safety transformations
- Context Setup - Initialize Superscript context with variables and functions
- Property Resolution - Handle dynamic device/computed properties
- Expression Evaluation - Execute the transformed AST
Let's trace through the expression: device.daysSince("some_event") > "3.0" && computed.some_property == "true"
Input JSON:
{
"variables": {
"map": {
"some_property": {"type": "string", "value": "true"}
}
},
"expression": "device.daysSince(\"some_event\") > \"3.0\" && computed.some_property == \"true\"",
"device": {
"daysSince": [{"type": "string", "value": "event_name"}]
},
"computed": {
"some_property": []
}
}Note: The device and computed sections define available functions that the host exposes to Superscript. These are function signatures, not variable values.
Parsed Expression AST:
And(
Relation(
FunctionCall(
Member(Ident("device"), Attribute("daysSince")),
None,
[Atom(String("some_event"))]
),
GreaterThan,
Atom(String("3.0"))
),
Relation(
Member(Ident("computed"), Attribute("some_property")),
Equals,
Atom(String("true"))
)
)
The normalize_variables function (src/lib.rs:557) processes all variables:
Before normalization:
some_property:{"type": "string", "value": "true"}→ becomesBool(true)- Right-side literal
"3.0": string → becomesFloat(3.0) - Right-side literal
"true": string → becomesBool(true)
After normalization:
- Variables:
{"some_property": Bool(true)} - AST atoms transformed by
normalize_ast_variables(src/lib.rs:593)
The transform_expression_for_null_safety function (src/lib.rs:654) applies two types of transformations:
Note: computed.some_property is a variable access, not a function call, so it gets null-safety transformation.
Original:
computed.some_property
Transformed:
has(computed.some_property) ? computed.some_property : null
Device and computed function calls get wrapped with hasFn checks to ensure graceful handling of missing functions.
Original:
device.daysSince("some_event")
Transformed:
hasFn("device.daysSince") ? device.daysSince("some_event") : false
Full transformed expression:
And(
Relation(
Ternary(
FunctionCall(Ident("hasFn"), None, [Atom(String("device.daysSince"))]),
FunctionCall(
Member(Ident("device"), Attribute("daysSince")),
None,
[Atom(String("some_event"))]
),
Atom(Bool(false))
),
GreaterThan,
Atom(Float(3.0))
),
Relation(
Ternary(
FunctionCall(Ident("has"), None, [Member(Ident("computed"), Attribute("some_property"))]),
Member(Ident("computed"), Attribute("some_property")),
Atom(Null)
),
Equals,
Atom(Bool(true))
)
)
Enhanced transformations for relation expressions (comparisons) that involve has() or hasFn() wrapped expressions. These transformations provide type-appropriate default values instead of null to avoid comparison errors.
When a property access appears in a relation with an atomic right-hand side, the transformation uses type-appropriate defaults:
Original:
user.credits < 10
Transformed (when user.credits doesn't exist):
has(user.credits) ? user.credits < 10 : 0 < 10
Type-specific defaults:
Int/UInt/Float→0/0.0String→""Bool→false
For non-atomic right-hand sides, the entire relation is wrapped:
Original:
user.credits < device.limit
Transformed:
has(user.credits) ? user.credits < device.limit : false
When a device/computed function call appears in a relation, similar type-aware logic applies:
Original:
device.getDays() > 5
Transformed (when getDays function doesn't exist):
hasFn("device.getDays") ? device.getDays() > 5 : 0 > 5
Original:
device.getString() == "hello"
Transformed (when getString function doesn't exist):
hasFn("device.getString") ? device.getString() == "hello" : "" == "hello"
For non-atomic comparisons, the whole relation is wrapped:
Original:
device.getLimit() > user.credits
Transformed:
hasFn("device.getLimit") ? device.getLimit() > user.credits : false
Benefits:
- Eliminates
nullcomparison errors - Provides predictable, type-safe default behavior
- Maintains consistent evaluation semantics across different scenarios
The execute_with function (src/lib.rs:180) sets up the Superscript evaluation context:
-
Variables added to context:
some_property→Bool(true)
-
Utility functions registered from
SUPPORTED_FUNCTIONSconstant (src/lib.rs:32):maybe,toString,toBool,toInt,toFloat,hasFn,has
-
Device function map created from host-exposed functions:
device_host_properties = { "daysSince": Function("daysSince", Some(List([String("event_name")]))) }
-
Computed object created: The
computedobject contains both:- Host-exposed functions:
some_property: Function("some_property", None) - Variable properties:
some_property→Bool(true)(from variables map)
- Host-exposed functions:
-
Objects added to context:
device→Map(device_host_properties)computed→Map(computed_host_properties + variables)
For device.daysSince("some_event") (wrapped with hasFn):
hasFn("device.daysSince")is evaluated first - checks if function is available- If available: Function
daysSinceis called with args["some_event"] prop_for(PropType::Device, "daysSince", Some([String("some_event")]), host)is invoked- Host context's
device_property("daysSince", "[\"some_event\"]", callback)is called - Host returns serialized result (e.g.,
{"type": "uint", "value": 5}) - Result is deserialized and normalized to
UInt(5) - If not available: Returns
falseinstead of throwing an error
For computed.some_property:
- Null-safety check:
has(computed.some_property)evaluates first - Since
some_propertyis a variable (not a host function), it resolves toBool(true)from the variables context - No host call is made - this is a direct variable lookup
Evaluation proceeds step by step:
-
Left side:
hasFn("device.daysSince") ? device.daysSince("some_event") : false > 3.0hasFn("device.daysSince")→Bool(true)(function is available)device.daysSince("some_event")→UInt(5)(from host function call)UInt(5) > Float(3.0)→Bool(true)(implicit type conversion)
-
Right side:
has(computed.some_property) ? computed.some_property : null == truehas(computed.some_property)→Bool(true)(variable exists in context)computed.some_property→Bool(true)(from variables map)Bool(true) == Bool(true)→Bool(true)
-
Final result:
Bool(true) && Bool(true)→Bool(true)
The type-aware transformations provide more predictable behavior for missing properties and functions:
Example 1: Missing property with atomic comparison
Expression: user.credits < 10
Variables: {"map": {"user": {"type": "map", "value": {}}}} // credits missing
Transformation: has(user.credits) ? user.credits < 10 : 0 < 10
Result: false ? ... : 0 < 10 = true
Example 2: Missing function with atomic comparison
Expression: device.getDays() > 5
Device functions: {"knownFunc": []} // getDays missing
Transformation: hasFn("device.getDays") ? device.getDays() > 5 : 0 > 5
Result: false ? ... : 0 > 5 = false
Example 3: Missing function with string comparison
Expression: device.getName() == "test"
Device functions: {} // getName missing
Transformation: hasFn("device.getName") ? device.getName() == "test" : "" == "test"
Result: false ? ... : "" == "test" = false
Benefits over previous behavior:
- No
nullcomparison errors that would result in evaluation failures - Consistent, predictable results based on data types
- Graceful degradation when properties/functions are unavailable
SUPPORTED_FUNCTIONSconstant (src/lib.rs:32): Defines all built-in functions available in Superscript- Single source of truth for function availability
- Used by both AST transformation and runtime evaluation
- Functions:
"maybe","toString","toBool","toFloat","toInt","hasFn","has"
-
normalize_variables(src/lib.rs:557): Recursively converts string representations of primitives"true"/"false"→Bool- Numeric strings →
Int/UInt/Float - Nested maps/lists are processed recursively
-
normalize_ast_variables(src/lib.rs:593): Similar normalization for AST atoms
transform_expression_for_null_safety(src/lib.rs:654): Applies three types of safety transformations:- Property access: Wraps with
has()checks to preventUndeclaredReferenceerrors - Function calls: Wraps device/computed function calls with
hasFn()checks using the host-exposed function lists - Type-aware relations: Enhances relation expressions containing
has()orhasFn()wrapped expressions with type-appropriate default values instead ofnullor genericfalsereturns - Eliminates comparison errors by providing sensible defaults based on the right-hand side type (e.g.,
0for numbers,""for strings,falsefor booleans)
- Property access: Wraps with
prop_forfunction handles async property resolution from host context- Supports both device and computed host-exposed functions
- Results are JSON-serialized for transport and deserialized back
- Variables are resolved directly from context without host calls
The system gracefully handles errors by converting them to null:
UndeclaredReference→nullUnknown function→nullNull can not be compared→null
Input JSON
↓
[Parse] → ExecutionContext
↓
[Normalize Variables] → Standardized types
↓
[Parse Expression] → AST
↓
[Transform AST] → Null-safe AST
↓
[Setup Context] → Superscript Context + Variables + Functions
↓
[Register Host Functions] → Device/Computed host-exposed functions
↓
[Evaluate AST] → Property resolution (host calls for functions, direct lookup for variables)
↓
[Return Result] → Serialized PassableValue
This flow ensures robust evaluation of Superscript expressions with proper type handling, null safety, and dynamic property resolution.