-
Notifications
You must be signed in to change notification settings - Fork 4
[WIP]Track matrix options
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.
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 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.
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.
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:
- After
matrixTarget.Options()ormatrixTarget.Require(), register the returned map identity and its category. - 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.
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.