Repository navigation
libexpr: builtins.tecnixQueryFile, and fingerprints that record observations - #87
Open
joshheinrichs-shopify wants to merge 2 commits into
Open
joshheinrichs-shopify wants to merge 2 commits into
joshheinrichs-shopify wants to merge 2 commits into
Conversation
…vations Replace the three World-specific manifest builtins from #72 (tectonixManifestEntry, tectonixManifestKeys, tectonixManifestIdToPath) and their `.meta/manifest.json#` synthetic keys with one generic builtin that queries any JSON or TOML file in the repository, and with a fingerprint format that records what was asked and what the answer was. Tecnix no longer knows anything about World. builtins.tecnixQueryFile { path = ../../.meta/manifest.json; # format inferred from the extension op = "get"; # get | has | keys | find at = [ "//areas/tools/dev" ]; # object keys, root down # find only: where = "id"; value = "W-123456"; } The model. A closure maps a repo path to a fingerprint, and a fingerprint records what evaluation observed about the path: `git:<oid>;mode=…` is the object observed there, `absent` that none was. A target that only queried a data file observed less than the whole object, so it records the answers it got: ".meta/manifest.json": "query:json\tget\t//areas/tools/dev\t{\"id\":\"W-c10e84\"}\njson\thas\t//areas\ttrue" one line per distinct query, sorted. The key stays the repo path, so everything that only knows paths keeps working (tec's affected-targets stays conservatively correct with no change), and the TXDC row format is untouched. A target that also read the file whole records the oid, which subsumes the queries. #72 had moved the observation into the key instead, which is why it needed a key grammar, a `#` special case in validation, and a tec change. How it works. The builtin reads the file unrecorded through the repo accessor, parses it once per evaluation, evaluates the op, and records an access `<file>\0<line>` through the ordinary recordAccess, so labels, tecnixMemoize replay and thread confinement apply unchanged; the line is built in thread-local scratch and interned on first sight like a path. Flattening groups accesses by file into one `query:` fingerprint. Validation (`dependencyFingerprintHolds`, replacing the string equality in candidateMatches) splits a stored `query:` fingerprint into views, re-asks each line of the file as the current tree serves it (parsed once per thread per run, read unrecorded), and memoizes per (path, line) in the existing thread-local memo. Evaluation and validation run the same evaluateDataQuery on the same parsed tree, so they cannot disagree. A line that does not parse, an unknown op or format, or a file that no longer parses does not hold: a miss, never a stale hit. A missing file answers every query as an empty document would, and that answer is recorded. Format: a line is `<format>\t<op>\t<fixed args…>\t<at segment…>\t<answer>`, the answer last so a variable number of segments is unambiguous. Fields never contain tab, newline or NUL: the builtin rejects string arguments that do, and answers are `absent`, compact JSON when at most 64 bytes (which escapes control characters), or `sha256:<hex>` of it. Parsing a fingerprint is splitting, with no unescaping. Outside the repo accessor the builtin is fromJSON (readFile path) plus the selection; a path pure evaluation forbids throws as readFile would (lstat, not maybeLstat, which reports forbidden paths as absent). Removed: the three builtins, getTecnixManifestJson and its EvalData fields, manifestPathFingerprint and the per-thread parsed manifest in eval-cache.cc. The unsafeTectonixInternalManifest* builtins stay; they record the whole file, correctly. Old cache rows (keys `.meta/manifest.json#…`) read as paths that do not exist and miss; no blob version bump is needed. Tests: src/libexpr-tests/tecnix-data-query.cc (27: op semantics, line round-trips, rejected characters, answer hashing, TOML and JSON of one tree agreeing, every hold/miss case including unparseable and unknown lines, grouping and subsumption in dependencyFingerprints); tests/functional/tecnix/data-query.sh (replaces manifest-tracking.sh, keeping its memoize cases): exact fingerprints per target; across ten commits, adding a zone misses only `keys` and the whole-file reader, `has b` survives b's renumbering, a comment-only TOML edit misses only the whole-file reader, a file whose absence was observed misses when it appears, a dirty data file is queried from disk and its observation follows; argument validation; the builtin outside tracking; the older-rev read. The tecnix suite and the Tecnix unit tests pass. Docs: explainer §4.2 and the fingerprint vocabulary; a guardrail on fingerprints as observations.
…h path
EvalSettings::getDefaultNixPath adds ~/.nix-defexpr/channels and root's
channels to the search path when they exist, asking with the throwing
std::filesystem::exists. A stat that fails with anything but ENOENT or
ENOTDIR therefore aborts the construction of every impure EvalState:
filesystem error: in posix_stat: failed to determine attributes for
the specified path: Operation not permitted
["/nix/var/nix/profiles/per-user/root/channels/nixpkgs"]
That is what a strict Darwin sandbox answers, since /nix/var/nix/profiles
is not among the paths it exposes, so a `nix` run inside a sandboxed build
on macOS cannot evaluate impurely at all. It is how Tecnix's own test
suite fails when built with `sandbox = true` on a Mac (macOS defaults the
sandbox off, which is why nobody saw it): every libexpr test fixture, the
C API flake tests, and the inner `nix build` of tests/functional/recursive.sh.
On Linux the directory is simply absent inside the chroot and the ENOENT
is swallowed, as intended.
Ask with the non-throwing overload. A directory the filesystem will not
even stat is not a default search path, which is the answer for one that
does not exist, and no reason to refuse to evaluate.
The test makes root's channels directory unreadable through a parent
without search permission and checks that getDefaultNixPath neither
throws nor lists it. It is skipped as root, who is never refused a stat.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace the three World-specific manifest builtins from #72 (tectonixManifestEntry, tectonixManifestKeys, tectonixManifestIdToPath) and their
.meta/manifest.json#synthetic keys with one generic builtin that queries any JSON or TOML file in the repository, and with a fingerprint format that records what was asked and what the answer was. Tecnix no longer knows anything about World.The model. A closure maps a repo path to a fingerprint, and a fingerprint records what evaluation observed about the path:
git:<oid>;mode=…is the object observed there,absentthat none was. A target that only queried a data file observed less than the whole object, so it records the answers it got:one line per distinct query, sorted. The key stays the repo path, so everything that only knows paths keeps working (tec's affected-targets stays conservatively correct with no change), and the TXDC row format is untouched. A target that also read the file whole records the oid, which subsumes the queries. #72 had moved the observation into the key instead, which is why it needed a key grammar, a
#special case in validation, and a tec change.How it works. The builtin reads the file unrecorded through the repo accessor, parses it once per evaluation, evaluates the op, and records an access
<file>\0<line>through the ordinary recordAccess, so labels, tecnixMemoize replay and thread confinement apply unchanged; the line is built in thread-local scratch and interned on first sight like a path. Flattening groups accesses by file into onequery:fingerprint. Validation (dependencyFingerprintHolds, replacing the string equality in candidateMatches) splits a storedquery:fingerprint into views, re-asks each line of the file as the current tree serves it (parsed once per thread per run, read unrecorded), and memoizes per (path, line) in the existing thread-local memo. Evaluation and validation run the same evaluateDataQuery on the same parsed tree, so they cannot disagree. A line that does not parse, an unknown op or format, or a file that no longer parses does not hold: a miss, never a stale hit. A missing file answers every query as an empty document would, and that answer is recorded.Format: a line is
<format>\t<op>\t<fixed args…>\t<at segment…>\t<answer>, the answer last so a variable number of segments is unambiguous. Fields never contain tab, newline or NUL: the builtin rejects string arguments that do, and answers areabsent, compact JSON when at most 64 bytes (which escapes control characters), orsha256:<hex>of it. Parsing a fingerprint is splitting, with no unescaping.Outside the repo accessor the builtin is fromJSON (readFile path) plus the selection; a path pure evaluation forbids throws as readFile would (lstat, not maybeLstat, which reports forbidden paths as absent).
Removed: the three builtins, getTecnixManifestJson and its EvalData fields, manifestPathFingerprint and the per-thread parsed manifest in eval-cache.cc. The unsafeTectonixInternalManifest* builtins stay; they record the whole file, correctly. Old cache rows (keys
.meta/manifest.json#…) read as paths that do not exist and miss; no blob version bump is needed.Tests: src/libexpr-tests/tecnix-data-query.cc (27: op semantics, line round-trips, rejected characters, answer hashing, TOML and JSON of one tree agreeing, every hold/miss case including unparseable and unknown lines, grouping and subsumption in dependencyFingerprints); tests/functional/tecnix/data-query.sh (replaces manifest-tracking.sh, keeping its memoize cases): exact fingerprints per target; across ten commits, adding a zone misses only
keysand the whole-file reader,has bsurvives b's renumbering, a comment-only TOML edit misses only the whole-file reader, a file whose absence was observed misses when it appears, a dirty data file is queried from disk and its observation follows; argument validation; the builtin outside tracking; the older-rev read. The tecnix suite and the Tecnix unit tests pass.Docs: explainer §4.2 and the fingerprint vocabulary; a guardrail on fingerprints as observations.
Also, a second commit:
EvalSettings::getDefaultNixPathno longer throws when a channels directory cannot be stat'ed. It used the throwingstd::filesystem::exists, so the EPERM a strict Darwin sandbox gives for/nix/var/nix/profilesaborted every impure EvalState: every libexpr test fixture, the C API flake tests and the innernixofrecursive.sh, which is how this pin failed to build its own test suites on a Mac withsandbox = true(macOS defaults it off). World's Darwin build of this pin passes all suites with it (unit tests and 200/200 functional).