Skip to content

SDK: add literal containers and variable-key lookups - #235

Open
robknight wants to merge 10 commits into
mainfrom
sdk-container-literals
Open

robknight wants to merge 10 commits into
mainfrom
sdk-container-literals

Conversation

@robknight

Copy link
Copy Markdown
Collaborator

This adds first-class container literals and variable-key lookups to the SDK, making it practical to represent lookup tables directly in Rhai scripts.

Rhai arrays and maps now convert recursively to pod2 Arrays and Dictionaries, while the new free-standing set_of function constructs Sets. Scripts can query these containers with array_get, dict_get, or obj.get, including when the key is a variable.

Key changes

  • Add recursive Array, Dictionary, and Set literal conversion.
  • Add variable-key lookups backed by containment statements.
  • Allow SDK-generated variables to be used inline.
  • Allow var to name literals without creating wildcards.
  • Snapshot statement and write arguments before later object mutations.
  • Validate container kinds, lookup key types, and literal lookups during Load where possible.
  • Report invalid field access as script errors instead of panicking.
  • Render container literals deterministically, with commitment fallbacks for sparse arrays.
  • Update the SDK documentation and regression coverage.
  • Add the nanoverse example plugin to demonstrate lookup tables, witness arithmetic, state-header access, subactions, and related SDK features.

These changes let authenticated object data select rows from literal tables without requiring a separate action for every row. Player-selected choices can remain separate actions, while data-selected behavior can be expressed compactly through constrained lookups.

Convert Rhai arrays and object maps recursively into pod2 Array and
Dictionary values, and add set_of for constructing Sets. Reject
variables inside container literals because their values are
unavailable during Load.

Render supported containers as Podlang literals, falling back to
commitments when no literal representation exists.

Add dict_get and array_get to bind entries with containment
statements. Keys may be variables, allowing a literal container to
serve as a lookup table and its returned dictionary fields to be
read directly.
Register variables returned by SDK methods as soon as they are
created, so callers can use them inline without first binding them
with var. Rename generated variables in place when a script assigns
a name, preserving wildcard order.

Treat var bindings of literal values like ordinary Rhai bindings
instead of creating wildcards. Route ArgHandle.get through dict_get
and report invalid literal field access as a script error rather
than panicking.
Statement and Intro instructions retained mutable references to
their arguments, so later object updates changed the values used
during replay.

Capture OperationArg values when each instruction is recorded
and replay those snapshots during proof construction. This keeps
containment statements and their bound values aligned with the
pre-update object state.
Check known container kinds and array index types during Load,
and resolve fully literal lookups immediately. This prevents kind
mismatches and missing rows from producing invalid predicates.

Validate field access whenever the value is available and return
script errors instead of panicking. Also promote Rhai booleans to
pod2 integer values in container literals.
Container kind metadata may identify the same raw value as multiple
container kinds. Render a literal only when its kind is unambiguous;
otherwise render its commitment.

This prevents nested container text from claiming the wrong kind
while preserving the raw value used by the verifier.
Reuse shared helpers for Rhai array conversion and Ref extraction
instead of repeating dynamic-value promotion paths.

Update the SDK documentation and tests to cover variable container
lookups, named literals, boolean elements, and ambiguous container
rendering.
Set and Update instructions retained mutable references to written
values, allowing later object mutations to change the values replayed
during proof construction. Capture OperationArg values when each
write is recorded.

Reuse the shared literal-entry resolver and remove redundant work
from literal container lookups.
set_of constructs a value and does not access action state. Register
it as a free Rhai function and return the pod2 Set directly, allowing
helper functions to build Set-containing tables without receiving
action.

Register the Set type name for clearer Rhai errors and keep invalid
field access on Set values as a script error.
Add string validation for dictionary keys while retaining integer
validation for array indexes. Represent lookup kinds explicitly so
Load reports container and key errors before generating predicates.

For raw values accepted as multiple container kinds, render the
first supported form in deterministic order: set, dictionary, then
array. Continue using commitments for sparse arrays.

Refresh the SDK documentation and tests to match these rules.
Add a compact plugin demonstrating literal containers, variable-key
lookups, native statements, witness arithmetic, state-header access,
subactions, and object field reads.

Document how authenticated data can select table rows while
player-selected choices remain separate actions. Include the generated
Podlang and current planner limitations.
@robknight
robknight requested a review from dhvanipa August 26, 2026 12:13

@ed255 ed255 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I've left a few small comments

/// prefer set, then dictionary, then array. Each representation has the same
/// raw value, which is what statement arguments use.
///
/// Podlang cannot represent sparse arrays, so they are rendered as their

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a note about 0xPARC/pod2#541 here

write!(f, "{sep}{}", LiteralFmt(element))?;
}
return write!(f, "]");
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should add an else here with a warning or an error, or a panic. Otherwise if an array with gaps arrives here, it won't be rendered, silently.

Comment thread libs/sdk/src/tests.rs
assert!(err.contains("is a var, not a literal"), "{err}");
}

/// Renders sparse arrays as commitments because Podlang has no sparse-array

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a note about 0xPARC/pod2#541

Comment thread libs/sdk/src/tests.rs

fn RevealChart(action) {
var chart = action.mutate("Chart");
chart.update("x", action.array_get([10, 20, 30], chart.code));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I guess this is assigned to an anonymous wildcard internally?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The answer is yes! Now the sdk creates anonymous wildcards automatically, and they are prefixed with _. On assignment they are renamed to whatever the script says via var foo = ...

Comment thread libs/sdk/src/lib.rs
Comment on lines +637 to +639
for var in self.vars.iter_mut().filter(|v| v.as_str() == old) {
*var = new.clone();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should expect to find old only once. What about making it more clear like this?

let old_var = self.vars.iter_mut.find(|v| v.as_str() == old).expect("old exists");
*old_var = new.clone();

Comment thread libs/sdk/src/lib.rs
let mut ctx = arg_ctx.ctx.0.borrow_mut();
// Preserve a generated variable's identity and wildcard
// position when the script assigns it a name.
if is_anon_var(&old_name) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my understanding of the code is that if a user defines a var _foo then a o reassign it would be detected as anon var and renamed?
There was a comment in is_anon_var related to this that I didn't fully understand:

/// Script-provided names may also begin with this prefix. Treating them as
/// generated allows a subsequent binding to rename the handle instead of
/// creating an alias.

The comment explains this behavior as a feature, but it seems more like a side effect? Would you say it's advisable for users to not prefix a var with _?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants