gum-tool-variable-references

$npx mdskill add vchelaru/Gum/gum-tool-variable-references

Bind variables across instances using Roslyn-parsed assignment syntax.

  • Synchronizes property values between different Gum instances and elements.
  • Depends on Roslyn for parsing assignment expressions and variable paths.
  • Executes logic for VariableReferenceLogic, EvaluatedSyntax, and ApplyVariableReferences.
  • Stores assignments in VariableListSave entries within StateSave objects.

SKILL.md

.github/skills/gum-tool-variable-referencesView on GitHub ↗
---
name: gum-tool-variable-references
description: Reference guide for Gum's variable reference system — Excel-like cross-instance/cross-element variable binding using Roslyn-parsed assignment syntax. Load this when working on VariableReferenceLogic, EvaluatedSyntax, ApplyVariableReferences, VariableChangedThroughReference, or the VariableReferences VariableListSave.
---

# Variable References

Variable references are Gum's system for keeping variables in sync across instances and elements, like cell references in a spreadsheet. A user writes `X = SomeOtherObject.X` and the left side stays updated whenever the right side changes.

## Storage

Variable references are stored as a `VariableListSave<string>` on a `StateSave`, with `Name` set to `"VariableReferences"` (or `"InstanceName.VariableReferences"` for instance-scoped references). Each string entry is one assignment line.

### Syntax

```
LeftProperty = RightSide
```

- **Left side:** An unqualified property name on the owning instance/element (e.g. `X`, `FontSize`, `Red`).
- **Right side:** A variable path, which can be:
  - Local: `OtherInstance.X` (same element)
  - Cross-element: `Components/MyComp.InstanceName.Width` (slash-separated element path)
  - Expressions: `OtherInstance.Width + 10`, `OtherInstance.Width * 2`, `!OtherInstance.Visible`
  - Literals: `X = 42`
- **Comments:** Lines starting with `//` are skipped. Invalid lines are auto-commented on validation failure.
- **Shorthand:** Writing just `OtherInstance.X` (no left side) auto-expands to `X = OtherInstance.X`.
- **Color expansion:** `Color = OtherInstance.Color` auto-expands to separate `Red`, `Green`, `Blue` assignments.

### Roslyn Parsing

The syntax is parsed as C# via Roslyn. Slashes in element paths are converted to `global::` qualified names before parsing (`Components/Foo` becomes `global::Components.Foo`) and converted back after. The `EvaluatedSyntax` class handles conversion (`ConvertToCSharpSyntax` / `ConvertToSlashSyntax`) and recursive evaluation of the right-side expression tree.

## Architecture

```
SetVariableLogic (variable change entry point)
  ├─ calls VariableReferenceLogic.DoVariableReferenceReaction()
  │    ├─ Validates lines (GetIndividualFailures)
  │    ├─ ElementSaveExtensions.ApplyVariableReferences() — writes hard values to StateSave
  │    ├─ Finds all elements that reference this element (via ObjectFinder.GetElementReferencesToThis)
  │    ├─ Applies references on those elements too (cascade)
  │    └─ DoVariableReferenceReactionOnInstanceVariableSet() — deep propagation for tunneled vars
  └─ calls VariableReferenceLogic.ReactIfChangedMemberIsVariableReference()
       └─ ModifyLines() — auto-expansion and qualification of newly entered references
```

### Key Classes

| Class | Location | Role |
|-------|----------|------|
| `VariableReferenceLogic` | `Gum/Plugins/InternalPlugins/VariableGrid/` | Tool-side orchestration: validation, reaction to changes, line expansion |
| `EvaluatedSyntax` | Same directory | Roslyn-based expression parser/evaluator; resolves right-side values via `RecursiveVariableFinder` |
| `ElementSaveExtensions` (partial) | `GumRuntime/ElementSaveExtensions.GumRuntime.cs` | `ApplyVariableReferences` — two overloads: one for `ElementSave` (save-class, tool-time), one for `GraphicalUiElement` (runtime) |
| `MainVariableGridPlugin` | Same directory as logic | Wires `CustomEvaluateExpression` delegate so the runtime can use Roslyn evaluation |

### Two Apply Paths

`ApplyVariableReferences` has two overloads:

1. **`ElementSave` overload (tool-time):** Iterates `VariableListSave` entries, evaluates right sides, writes hard values into the `StateSave` via `SetValue`. Fires `VariableChangedThroughReference` delegate when a value actually changes, which routes through `PluginManager.Self.VariableSet` — this triggers downstream reactions (font generation, etc.).

2. **`GraphicalUiElement` overload (runtime):** Similar iteration but calls `referenceOwner.SetProperty(left, value)` on the runtime object. Used for wireframe preview in the tool and at game runtime.

### Right-Side Evaluation

`GetRightSideValue` resolves the right side of an assignment:
- In the tool: `CustomEvaluateExpression` is set by `MainVariableGridPlugin` to use `EvaluatedSyntax` (Roslyn parsing with full expression support).
- At runtime (no tool): Falls back to `RecursiveVariableFinder` with simple dot-path lookup — no expression support, just direct variable resolution.

### Hard Values — Runtime Implications

Variable references write **hard values** into the `StateSave`. This means at game runtime (where `ApplyVariableReferences` on the `GraphicalUiElement` runs once at load time), the referenced values are already baked into the save data. References are **not dynamically re-evaluated** at game runtime when the source value changes — they are a tool-time binding mechanism. The runtime `ApplyVariableReferences(GraphicalUiElement)` overload exists primarily for the tool's wireframe preview.

### Cross-Element References and Cascading

When a variable changes, `DoVariableReferenceReaction` finds all elements that reference the changed element via `ObjectFinder.GetElementReferencesToThis` (filtered to `ReferenceType.VariableReference`). It then applies variable references on those elements too, creating a cascade. Modified elements are auto-saved.

### Deep Propagation

`DoVariableReferenceReactionOnInstanceVariableSet` handles a subtler case: when an instance's base element has variable references internally, and the changed variable tunnels through. It walks the reference graph to find which inner-instance variables need updating and writes the values directly into the container's state.

## Validation

`GetIndividualFailures` checks each line for:
- Parseable assignment syntax
- Forbidden left-side names (`Name`, `BaseType`, `DefaultChildContainer`)
- Left-side variable existence
- Right-side evaluability
- Type compatibility (with casting support for numeric types)
- Root variable matching for unit/alignment types (prevents mixing XUnits with YUnits, etc.)

Invalid lines are auto-commented with `//` prefix and a message is shown to the user.

## Known Gaps

- **Font generation:** `CollectRequiredFonts` (in `HeadlessFontGenerationService`) and `RecursiveVariableFinder` do not resolve variable references. If a font property (Font, FontSize, etc.) is set via a variable reference, the font file may not be generated for that value. The tool-time path works because `VariableChangedThroughReference` fires `PluginManager.VariableSet`, but headless/CLI font generation could miss these. (See issue #2414)
- **Runtime dynamism:** Variable references are not re-evaluated at game runtime when source values change. This is by design currently but limits use cases.
- **Expression support varies:** The tool uses Roslyn for full expression evaluation; the runtime fallback uses `RecursiveVariableFinder` which only handles simple variable lookups, not arithmetic expressions.

More from vchelaru/Gum

SkillDescription
bump-nuget-versionBump the NuGet package versions for all 12 Gum projects (11 libraries + GumCli). Queries NuGet to check if a version exists for today, then sets the new version to YYYY.M.D.V where V increments from the latest published version today (or starts at 1). Creates a release branch named ReleaseCode_YYYY_M_D_V, commits the changes, and pushes. Run this before triggering the nuget release workflow.
gum-cliReference guide for GumCli — the headless command-line tool for Gum projects. Load this when working on gumcli commands (new, check, codegen, codegen-init), Gum.ProjectServices, HeadlessErrorChecker, ProjectLoader, HeadlessCodeGenerationService, CodeGenerationAutoSetupService, or the FormsTemplateCreator.
gum-docs-writingReference guide for writing Gum documentation in GitBook markdown. Load when writing or editing docs/ files, adding pages to SUMMARY.md, using GitBook hints/figures, linking between pages, or adding images.
gum-forms-behaviorsCovers Gum's behaviors system and the design-time → runtime Forms wrapping lifecycle. Load this when working on BehaviorSave, ElementBehaviorReference, StandardFormsBehaviorNames, FormsUtilities.RegisterFromFileFormRuntimeDefaults, DefaultFromFileXxxRuntime classes, or when investigating why Forms properties cannot be set at design time in the Gum tool.
gum-forms-controlsReference guide for Forms controls — classes inheriting from FrameworkElement. Load this when working on Button, CheckBox, ListBox, ComboBox, TextBox, ScrollViewer, or any class in Gum.Forms.Controls (or FlatRedBall.Forms.Controls). Also load when working on FrameworkElement itself, the Visual/InteractiveGue relationship, state machines, DefaultVisuals, or ReactToVisualChanged.
gum-forms-default-visualsReference guide for Forms DefaultVisuals — the code-only visual classes that back Forms controls. Load when working on ButtonVisual, any *Visual class in DefaultVisuals/, Styling, DefaultFormsTemplates registration, or building custom code-only Forms visuals.
gum-forms-itemscontrolReference guide for ItemsControl and ListBox — the Items/ListBoxItems relationship, templates, InnerPanel sync, and gotchas. Load this when working on ItemsControl, ListBox, ListBoxItem, VisualTemplate, FrameworkElementTemplate, Items collection behavior, ListBoxItems desync, or adding/removing items from a list box.
gum-layout>
gum-layout-engine>
gum-localizationReference guide for Gum's runtime localization system — ILocalizationService, CSV/RESX loading, Text vs TextNoTranslate paths, Forms control localization patterns, and gotchas.