Skip to content

[WIP]Track matrix options

Rick Guo edited this page Jul 16, 2026 · 1 revision

Discover Formula Matrix

Summary

LLAR needs to collect the complete matrix set of each formula so it can know which selections affect that formula and which build variants may exist. The matrix tracker evolved through three designs to provide that information: explicit declaration, XGo AST rewrite, and finally Go SSA call insertion. The goal was to make executable formula logic the source of truth without requiring a second matrix schema or a general static analyzer.

Explicit Declaration

The original design asked each formula to declare its complete matrix:

matrix {
    Require: {
        "os": ["linux"],
        "arch": ["amd64", "arm64"],
    },
    Options: {
        "zlib": ["ON", "OFF"],
        "debug": ["ON", "OFF"],
    },
}

This assumes the formula maintainer knows the full matrix supported by the upstream library. That is often false. Formula maintainers are not necessarily library authors, and support for platforms, ABIs, toolchains, and options is often learned by building, patching, and fixing edge cases over time. Requiring the complete set up front makes matrix correctness depend on manual guesses.

It also duplicates formula logic:

if slices.contains(target.options["zlib"], "ON") {
    deps.require "madler/zlib", "v1.3.1"
}

The explicit declaration was therefore removed. The keys actually read through target.require and target.options define the formula's matrix surface.

Static Analysis

Static analysis was also rejected. Simple reads are easy:

target.options["zlib"]

But formula code can use helpers, branches, loops, and dynamic keys:

key := pickOption()
target.options[key]

A static scanner cannot reliably infer the complete matrix from normal XGo control flow without becoming another interpreter.

XGo AST Rewrite

The next design observed those reads by rewriting XGo AST:

target.options[key]

became:

trackOptions(target.options, key)

The tracker recorded the key and returned the original map value, so formula behavior remained unchanged.

This worked for direct reads, but normal formula code can move the map before reading it:

func read(values map[string][]string, key string) []string {
    return values[key]
}

options := target.options
_ = read(options, "shared")

The lookup no longer contains target.options in its AST. Covering this case would require following aliases, parameters, returns, closures, and interfaces. AST rewriting would also need to distinguish maps from slices or arrays, preserve comma-ok map lookup semantics, and maintain expression evaluation order. The rewrite was becoming a type and data-flow analyzer.

SSA Call Insertion

After XGo generates Go source, ixgo LoadFile builds typed Go SSA. At that point:

  • target accessor calls have resolved method identities;
  • map reads are normalized as ssa.Lookup;
  • aliases and helper arguments preserve the same runtime map identity.

The tracker now runs in this pipeline:

_llar.gox source
  -> generated Go source
  -> ixgo LoadFile builds Go SSA
  -> tracker inserts calls
  -> ixgo interpreter

It inserts two kinds of observation:

  1. After matrixTarget.Options() or matrixTarget.Require(), register the returned map identity and its category.
  2. Before each matching ssa.Lookup, report the map and key.

The helper example becomes:

options = target.Options()
__internalOptionsTracker(options)

read(options, "shared"):
    __internalLookupTracker(options, "shared")
    value = lookup options["shared"]

Assignments and helper calls keep the same map identity, so the lookup remains associated with Options. An ordinary map passed to the same helper has a different identity and is ignored. The original lookup is not replaced.

Probe

LoadFS temporarily injects fresh non-nil empty Require and Options maps, then executes OnRequire and OnBuild. Empty values let formula code run without first constructing a valid matrix, while each executed lookup still reveals its key.

Filter is not executed during probe. The original target is restored after probe, and the tracker returns keys only:

Matrix{
    Require: map[string][]string{"os": nil},
    Options: map[string][]string{
        "shared": nil,
        "ssl":    nil,
    },
}

Later module loading fills same-named selected values into Require or Options, merges defaults, injects the real target, and then executes Filter and the real OnRequire.

discover keys
  -> fill selected values by key
  -> inject target
  -> Filter
  -> OnRequire

The tracker discovers matrix ownership by key. It does not infer every value accepted by a formula.

Clone this wiki locally