Skip to content

Commit c6c1e60

Browse files
jedudenclaude
andauthored
feat(cuelite): phase 2 — schema and query on the façade, in-house flip (plan 238) (#555)
* feat(cuelite): add LookupPath/Fields/Exists/String/Decode accessors Extend the CUE-backed cuelite façade with the read methods surfaces A (schema) and B (query) need before they can migrate off cuelang.org/go. A LookupPath/Fields result keeps rebuildable provenance (root source plus path), so a section lookup against a cached schema crosses contexts without mutating the shared value. Each method ships a dedicated unit test (100% statement coverage held) and a differential arm in cuelitetest comparing it against a direct-CUE oracle. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * refactor(schema,query,requiredstructure): adopt cuelite façade Migrate the schema validator (MDS020), the requiredstructure rule's CUE checks, and the query/where matcher off cuelang.org/go onto the cue/cuelite façade. No non-test file in the three packages imports cuelang.org/go anymore. RunCache shape: cache the source-retaining cuelite.Value and fix the Unify operand order so the shared schema is the operand and the per-file data is the receiver (dataVal.Unify(schemaVal)). The receiver context is the one rebuilt into, so the shared cached value is read (never mutated) and parallel workers stay race-clean while the compile-once cache contract holds. CompiledCUE dropped its Ctx field. MDS020 diagnostics stay byte-identical: the error walkers consume []*cuelite.PathError via cuelite.Errors, same .Path() route. Added Value.Err() to the façade (compile/bottom status without the concreteness check Validate applies). https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * docs(plan-238): record task 1+2 done, task 3+4 remaining https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * docs(plan-238): record parser-frontend decision and the test-contract blocker Task 3 (flip the value engine in-house) is paused for a decision. Record in the plan: - Parser frontend: reuse cuelang's syntax-only parser (cue/parser → cue/ast) in phase 2, with a fully in-house evaluator; phase 4 swaps in a hand-rolled parser and drops cuelang.org/go. - Blocker: several existing cue/cuelite tests pin CUE-specific behavior a context-free pure-Go Value cannot reproduce (cross-context bottom semantics, cueerrors.Error errors.As checks, the exact CUE conflict message, and internal_test.go's CUE-only helper tests). Per plan 218 these are interim scaffolding the flip erases, but they are pinned, so the flip cannot land green without sign-off to rewrite them to the post-flip in-house contract. No engine code changed and no test was weakened. Status stays 🔳. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * feat(cuelite): flip Compile/Unify/Validate onto the in-house engine Add the pure-Go value model (engine.go), AST→value compiler over cuelang's parser frontend (compile.go), lattice-meet Unify (unify.go), concreteness Validate (validate_engine.go), direct map/JSON lifters (lift.go), and the ported strict-JSON duplicate-key scanner (dupkeys.go). Value becomes a context-free immutable struct; operand order no longer matters. Rewrite the four pinned interim test classes to the post-flip single-context contract per the coordinator authorization recorded in plan/238, and align the differential oracle's deliberate-divergence rows. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * feat(cuelite): direct map validation + thunk eval for cross-field schemas Wire schema validate.go, query.go, and requiredstructure onto the in-house engine's direct map validation (CompileMap/LiftMap), dropping the json.Marshal -> CompileJSON round-trip on the MDS020 hot path. Add a deferred-thunk evaluator (eval.go) for the release-channels proto.md ternary idiom — list index, if-comprehension, == comparison, and sibling-field references resolved once data fixes them. Handle defaulted disjunctions (bool | *false) and optional absent fields so a field with a default or an absent optional is not reported missing. mdsmith check . is clean. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * feat(cuelite): comparisons, !~, real-schema sweep, FuzzValidate Treat a binary relational (A != "", mechanism == "push") as a scoped comparison rather than dropping the left operand; add !~ regex non-match; reject a thunk referencing an undeclared name at compile (reference X not found). Lift invalid-UTF-8 and trailing-data JSON to data-compile errors. Add the real-repo-schema differential sweep (realschemas_test.go) and the FuzzValidate schema x data differential fuzzer with documented tolerances for CUE's duplicate-leaf quirk, eager-regex strictness, and the close+missing superset. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): align edge cases with CUE found by FuzzValidate Reject definition/hidden field labels, standalone * default, unary + on non-numbers, and a top-level free reference; keep concrete int and float distinct (0 != 0.0); treat an open list [...int] as concrete (empty default); dedupe equal concrete disjuncts (0|0 -> 0); reject invalid-UTF-8 and trailing-data JSON; recover the oracle from cuelang parser panics. Fuzzer tolerates CUE's lone-surrogate data rejection. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelite): subset/eval/decode unit coverage; fix freeRefs field labels Add engine_test.go and eval_test.go covering the compiler subset, every unify/bound rule, comparisons inside forced thunks, decode targets, the lift branches, and the describe/String renderers. Fix freeRefs to skip field-label and selector-member identifiers so a comprehension body's struct field (e.g. {title: ...}) is not mistaken for a sibling reference. Reject a comparison of non-concrete types (_ > 0) at compile. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelite): unify-rule matrix and branch coverage to 93% Add unify_test.go (one accept/reject per lattice-meet rule, both operand orders) and coverage_test.go (nested error propagation, call arities, unsupported constructs, string comparisons, number-overflow lift). Raises cue/cuelite statement coverage from 57% to 93%. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(schema): cover closed-struct extra-field diagnostic https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * docs(cuelite,plan-238): rewrite concurrency/stability for the in-house engine Update doc.go: Value is now a context-free immutable struct, safe for concurrent use with no synchronization and no per-document context growth; the evaluator is in-house with cuelang only as the AST frontend. Record task 3 done and task 4's benchmark numbers in plan 238, and trim the resolved blocker section under the line cap. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): reject the bare top token _ as a field label The 300s FuzzValidate run found {_: int}: _ is a valid VALUE (top) but CUE rejects it as a field label. Reject it in fieldLabel so the schema arm agrees with the oracle. The full 300s fuzz then passes clean (483k execs, no divergence). https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * refactor(cuelite): one scope-threaded builder for struct/list/disjunction Collapse eval.go's duplicate struct/list/disjunction/ident builders into compile.go's via a single scope-threaded evalExpr. compileExpr is now the unscoped face of evalExpr: it evaluates with a nil scope and defers an index/relational construct over an unresolved sibling to a kThunk, while a bare reference stays a hard "reference X not found" error. evalChild routes field/element/branch positions through that deferral at compile time. Deletes compileIdent/compileStruct/compileList/compileBinary/compileDisjunction. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelite): drive engine helpers and compiler error paths to 97.6% https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelite): drive statement coverage to 100% Close the residual statement-coverage gaps left by the scope-threading dedup: engine-level helpers and compiler error positions driven red/green, the kind-exhaustive switch defaults exercised with constructed values, and two structurally-impossible LabelName-error branches removed (the parser always yields an *ast.Ident selector member, read directly via selectorName). https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): cover normalizePath and extractJSONSafely panic recovery Drive internal/cuelitetest to 100% statement coverage: normalizePath's nil-input and quote-unwrap branches, and extractJSONSafely's panic-to-error recovery on a malformed cuejson input. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelite): cover per-kind unify discriminants for gobco branch coverage Drive the kBytes/kFloat/kBool/kNull discriminant disjuncts of the unify switches and the isReferenceName/isDeferrable operator disjuncts. Lifts gobco -branch from 1253/1278 to 1276/1278; the two residual conditions are the same path.go sepBracket switch-arm and multiline.go walk-back bound recorded as structural in plan 237. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * perf(cuelitetest): tighten factor-gate budgets to 1.0x at the 238 flip The in-house engine must never be slower than the CUE oracle it replaced, so HotFactorBudget and ColdFactorBudget drop from 2.5x/2.0x to 1.0x — the tightening plans 218/236/240 intended, realized at the 238 flip rather than deferred. Rework the interim hot-looser-than-cold guard to assert both <= 1.0x. Armed gate passes with margin (hot ~0.26x, cold ~0.38x). Update plans 236/238: status 238 -> done, coverage 100%/100%, gobco 1276/1278, alloc + factor notes. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): align engine semantics with CUE for defaults, bounds, lift, thunks Seven empirically-confirmed divergences from CUE v0.16.1, each pinned by a red/green unit test: - Disjunction defaults follow CUE's default-of-meet rule: multiple * marks in one disjunction are ambiguous (non-concrete); the default of a meet is the meet of the defaults; a parenthesized nested default is carried up the flatten. The engine now tracks a set of default disjuncts, not one pointer. - Equal concrete disjuncts collapse at build time ("x"|"x" is "x"). - An empty numeric/string bound interval reduces to bottom at compile time (>=10 & <=5), restoring schema/extend.go checkUnifiable's conflict detection. != , =~ , !~ are not folded, matching CUE. - Relational == / != compare numbers across kinds (2 == 2.0 is true). - A float64 always lifts to a float leaf — no integral coercion — so the CompileMap and CompileJSON lift paths agree with each other and with CUE. - An all-bottom disjunction is a compile error (empty disjunction). - A thunk nested in a list element, open-list tail, or disjunction branch is forced against its sibling scope (the real scope-threading fix), instead of false-rejecting with an *ast leak; an undeclared nested reference stays a compile-time "reference not found". cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): reject out-of-subset comparison/literal constructs eagerly; restore lone-surrogate-key rejection; narrow fuzz hatches Engine subset alignment (each a red/green unit test, surfaced by the cross-engine fuzzer): - A lone-surrogate-ESCAPE object KEY is rejected at compile (restoring the pre-flip CompileJSON contract) — two distinct keys collapsing to U+FFFD must not last-wins merge. A literal U+FFFD key and a lone-surrogate VALUE stay accepted, matching CUE. - An int/float literal outside int64/float64, a bound over a type (>string), unary +/- on a non-number, and an ordered/equality compare with a non-concrete TYPE operand (A > _, a == string) now report a documented out-of-subset / invalid-operation class at schema compile, matching CUE (which defers but also rejects). An undeclared reference inside an embedded disjunction branch ({A > "" | ""}) now rejects via the recursive thunk-ref scan. Harness honesty (cuelitetest): - Both fuzz tolerance hatches re-justified and SCOPED to their documented class: hatch 1 (strict-subset schema compile) fires only when the in-house error names a documented class and is now checked BEFORE the oracle, which also guards against cuelang.org/go non-termination on cyclic schemas; hatch 2 (lone-surrogate VALUE) requires a surrogate escape in the data. The leaf superset tolerance is bounded to one extra leaf. - Seeded every round-1 / newly-found minimized divergence as a corpus row (P0 agreement cases) and a FuzzValidate f.Add seed (strict-subset cases). - doc.go and plan 238 wording: "identical sets of rejecting leaf paths (deduplicated)"; TestValue_Unify_singleContextOracle backs the single-context claim with a direct one-context cuecontext oracle. cue/cuelite and internal/cuelitetest hold 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): reject non-bool if-conditions and bare type-keyword field labels; drop dead helpers Two more empirically-confirmed subset divergences from CUE, each fixed red/green and surfaced by the cross-engine fuzzer: - An `if` comprehension whose condition is not a concrete bool now rejects at schema compile. A concrete non-bool (`if ""`, `if 1`) is a type error; a non-concrete type/top condition (`if string`, `if _`) can never resolve, so compileExpr no longer indexes an empty free-reference slice (the prior behavior panicked). - A bare type-keyword field label (`int:`, `string:`) is rejected out-of-subset. CUE resolves a same-named reference in the field value as a self-reference rather than the type, so `{int: {int}}` accepts `{}` where a quoted label rejects; the in-house engine cannot model the shadowing. A field literally named `int` stays expressible quoted (`"int":`). Both classes are covered by the fuzzer's strict-subset hatch and seeded. Dead-code cleanup (P2): removed the trivial evalUnary scope-only wrapper (call compileUnary directly), the bytesReader wrapper (use bytes.NewReader as dupkeys.go already does), and containsByte (strings.IndexByte). The kThunk describe case is live and covered, kept. lift.go comments corrected: lifted data structs are OPEN on purpose so closedness stays a schema property. plan 238 wording scoped: per-field diagnostics byte-identical, the unreachable front-matter-shape diagnostic re-worded for the round-trip removal. cue/cuelite and internal/cuelitetest hold 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): reject misplaced * default marks statically; add hatch for self-cycle leaf paths; rewrite plans 239/240 Engine: a `*` default mark is valid only as a disjunction branch. CUE rejects a misplaced mark at parse — even in a list element the evaluator never forces (`[if c {}, (*"")][0]`). The in-house engine only reached compileUnary on a forced element, so it silently accepted the unreached mark. A new static checkNoMisplacedDefault pass walks the whole AST after parse and rejects every misplaced mark up front, matching CUE. Harness: a structural self-cycle (a field referencing its own label, `{a: [if a {}][0]}`) is rejected by both arms but at different leaf paths — CUE at the root ("cycle with field"), the in-house engine at the field. Hatch 3 detects the self-cycle via the AST and tolerates that leaf-path-only difference when the oracle's sole rejecting leaf is the root, never masking a wrong accept or a stage mismatch. Both classes are seeded. Plans (P2): plan 239 records what phase-2 eval.go already provides (the scope-threaded evalExpr and its node coverage) and what surface C must add (interpolation, for-clauses, scoped selector eval, a builtin registry), with tasks reframed as "extend evalExpr". Plan 240 gains the real task 1 — replace the cue/parser+ast+literal+token syntax frontend with a hand-rolled CUE-subset parser — fixes the stale test-file inventory, and moves model sonnet to opus with a sizing note. cue/cuelite and internal/cuelitetest hold 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): propagate a hard comparison-operand error over a deferred reference `A > !0` pairs an unresolved reference (A) with an unsupported construct (!0). evalComparison returned the reference's errUnresolved first, deferring the whole comparison to a thunk and silently accepting the schema; CUE rejects "invalid operation !0" at compile. The operand evaluation now propagates any hard (non-errUnresolved) error from either position before the deferral, so an operand that can never resolve rejects the comparison eagerly, matching CUE. Seeded both operand positions; cue/cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): reject an undeclared reference hidden in a top-level disjunction or list `0x0 | 0 < A` and `[0 < A]` carry an undeclared reference (A) in a disjunction branch or list element with no enclosing struct to bind it. compileSource only checked a BARE top-level thunk, so the reference buried in a disjunction or list slipped through and the schema was accepted; CUE rejects "reference A not found" at compile. The top-level check now uses the recursive checkThunkRefsIn against an empty declared set, descending the same positions the force pass reaches. Seeded both forms; cue/cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * docs(cuelite): fix stale compileDisjunction comment references compileDisjunction was merged into evalDisjunction; update the two comments that still named it. Cross-reference the * default check (compileUnary catches a reached misplaced mark; checkNoMisplacedDefault catches an unreached one). https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): reject a hard error in an unreached list element eagerly A list element with an unsupported construct (`[if c {}, (string*"")][0]`) went uncaught: evalListElems returned on the first element's errUnresolved deferral, never evaluating the `(string*"")` element, so the in-house engine accepted the schema. CUE rejects "invalid operand string ('*' requires concrete value)" at compile regardless of whether `[…][0]` reaches the element. evalListElems now evaluates every element, returning a HARD (non-errUnresolved) error from any position immediately while still deferring the list when the only errors are unresolved references. Seeded both forms; cue/cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): catch a hard error in a deferred comprehension body at compile A comprehension body with an invalid construct (`[if mechanism {string != ""}][0]`) went uncaught when the condition deferred: evalComprehension returned errUnresolved without compiling the body, so the in-house engine accepted the schema. CUE rejects the body's invalid operand at compile regardless of whether the condition selects it. evalComprehension now compiles the body on both deferral paths (unresolved-reference and non-concrete-type condition), returning a hard body error while still deferring when the body's own references merely await data. Seeded both deferral paths; cue/cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): defer a non-indexed list field; reject a paren-wrapped default mark Two more CUE-subset alignments: - A non-indexed list field whose comprehension references a sibling (`xs: [if c {1}, 2]`) was rejected "reference c not found" — isDeferrable did not include a bare list literal, so the list never became a thunk. CUE accepts and resolves it against data; a list literal is now deferrable, so the comprehension resolves once the sibling binds. An undeclared reference still rejects at compile. - A `*` default mark wrapped in its own parens (`(*0) | 1`, `1 | (*0)`) was accepted; CUE rejects it ("preference mark not allowed at this position") — the mark must be the OUTERMOST operator of a disjunct. checkNoMisplacedDefault now passes a non-mark position through a ParenExpr, so a paren-wrapped mark is rejected while `*(a|b)` and a `(a|b)` sub-disjunction's own direct marks stay valid. Verified against CUE across the full paren/mark matrix. Seeded all forms; cue/cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): extend the self-cycle hatch to the compile-vs-validate stage divergence Making a bare list literal deferrable surfaced a self-cycle shape the leaf-path hatch did not cover: `{a: [if a {}]}` uses the field `a` as its own `if` condition. CUE rejects it eagerly at schema compile ("cannot use list as type bool"); the in-house engine defers the list and rejects at validate when the self-reference cannot resolve. Both REJECT — only the stage and leaf differ. Hatch 3 now tolerates a self-cycle whose in-house arm rejects at validate while CUE rejects at schema compile, in addition to the both-reject root-leaf shape. The hatch never fires when the in-house engine ACCEPTS, so a wrong accept of a self-cycle still fails the fuzzer. Seeded both shapes. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): skip cyclic schemas before the oracle instead of hatching self-cycles A disjunction-hidden self-cycle (`{mechanism: "" | [if mechanism {}]}`) produced a WRONG ACCEPT — the in-house engine accepted data CUE rejects — which the leaf-path/stage self-cycle hatch correctly did not tolerate, so the fuzzer flagged it. Matching CUE's cycle handling exactly would require implementing its eager cycle detection (a disjunction defers the cycle to validate, a bare field rejects it at compile), which is out of scope: a field reference cycle is outside the documented front-matter subset, and no real schema cross-references its own fields. Replace the per-shape self-cycle hatch with a single pre-oracle guard: schemaHasReferenceCycle builds the top-level field-reference graph and skips any schema with a cycle (self, mutual, or disjunction-hidden) before consulting the oracle — the same treatment as an out-of-subset construct. An acyclic schema never trips it, so a genuine divergence is never masked. This is more honest than tolerating a wrong-accept on a cyclic schema. Both packages hold 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): widen the regex hatch to a non-string operand; split seed table `({0!~""|0})` pairs a regex `!~` on an int operand (`0 !~ ""`) with a `0` branch. The in-house engine rejects the non-string regex operand eagerly at schema compile ("!~ requires strings"); CUE drops the bottom branch in the disjunction and accepts. This is the same eager-strictness as the regex-pattern hatch — in-house is stricter on a regex construct, never a wrong-accept — so hatch 1's regex class now also matches "requires strings". The non-disjunction form (`{a: 0 !~ ""}`) rejects at compile in both arms, so the pre-oracle skip stays sound. Split extraFuzzSeeds into baseFuzzSeeds (the well-formed grammar) and edgeFuzzSeeds (the documented subset edges) to stay under the funlen budget. Both packages hold 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): classify a non-list index as an invalid operation for the hatch `({A: "" | "0"[0]})` indexes a string ("0"[0]). The in-house engine rejects it eagerly at schema compile; CUE rejects it standalone but drops the bottom branch in a disjunction and accepts. The index errors (non-list target, non-integer index) are now worded "invalid operation" — the class CUE itself reports — so hatch 1 recognizes the documented eager-strictness. The standalone form rejects in both arms, so the pre-oracle skip stays sound. Seeded both forms; cue/cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): tolerate top-level-disjunction leaf granularity A bare top-level disjunction (`{m: "0"} | ""`) is the one shape where the rejecting-leaf SET differs in granularity, not content: CUE adds a root-path "does not satisfy disjunction" summary and reports the fields of the branch it tried, while the in-house engine enumerates every failing field. Both REJECT — the accept/reject decision agrees — and a bare top-level disjunction is an edge schema shape (real front matter declares a struct). Hatch 3 tolerates the leaf-set difference for a top-level disjunction both arms reject at validate; a stage mismatch or a wrong accept on such a schema still fails. Seeded the single- and multi-field forms; both packages hold 100% coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): generalize the leaf hatch to CUE's root-summary path Replace the top-level-disjunction-only leaf hatch with a precise root-summary hatch. CUE attributes a failure to the ROOT [] (a "does not satisfy disjunction" / "incomplete value" summary) in two cases: a top-level disjunction that matches no branch, and a deferred thunk referencing a non-concrete field (`{mechanism: "" | "0", A: [if mechanism {}][0]}`). The in-house engine instead names the precise field whose thunk could not resolve. CUE emits the bare root [] leaf ONLY for these summaries — a plain field mismatch carries the field path in both arms, and a genuine root-scalar mismatch carries [] in both — so the hatch fires only when CUE reports the root [], the in-house engine does not, and the in-house engine still covers every NON-root field leaf CUE reports. A dropped field leaf, a wrong accept, or a stage mismatch still fails. Replaces the schema-shape check with a leaf-set signature check, dropping schemaIsTopLevelDisjunction. Seeded the disjunction and deferred-thunk forms; both packages 100% covered. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): apply a disjunction-meet default regardless of operand order `(0 | int) & (*1 | int)` should default A to 1, but the in-house engine left it non-concrete and rejected — while the reversed `(*1 | int) & (0 | int)` worked. retainByValue only kept a concrete default when a SURVIVOR was that bare scalar, but a surviving meet can stay a disjunction (`int & (*1|int)` survives as `1|int`), so the default 1 lived inside that survivor's value set and was wrongly dropped. survivorContainsValue now descends a disjunction survivor's branches, so the default is retained from either operand order. Verified order-independent against the CUE oracle, with conflicting defaults (`(*1|int) & (*2|int)`) still leaving the field non-concrete. Seeded both orders; cue/cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): detect reference cycles through a close() wrapper The cycle guard's unwrapStruct descended parens and a bare struct but not a close({...}) wrapper, so `close({s: [(s)][0]})` — a self-cycle on s inside a closed struct — slipped past the pre-oracle cycle skip and the in-house engine (which defers the self-reference) diverged from CUE's eager cycle detection. unwrapStruct now descends a single-argument close(...) call, so a cycle inside a closed struct is skipped like any other. Seeded the closed self-cycle. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): reject a single-quoted bytes literal as out-of-subset `''` and `'x'` are CUE BYTES literals (token.STRING kind, single-quoted), a distinct type from a string with no JSON front-matter representation. The in-house engine, lacking a bytes kind, decoded them as strings via literal.Unquote and accepted string data CUE rejects (`'' ` vs `""`). compileBasicLit now rejects a single-quoted literal as an unsupported bytes literal, so the cross-engine fuzzer's strict-subset hatch covers it. Seeded both forms; cue/cuelite holds 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): accurate out-of-subset literal wording; exempt closed structs from the leaf bound Two changes: - An int/float literal Go's parser rejects is now reported "unsupported int/ float literal" without the misleading "outside int64/float64 range" suffix — it also covers CUE's SI-suffix literals (1M, 1Ki), a syntax the in-house parser does not accept, not only a range overflow. The strict-subset hatch still recognizes the "unsupported" class. - CUE's close-suppression: a CLOSED struct with an extra key reports just the close violation and suppresses every absent-required-field error, while the in-house engine reports the close violation AND each missing field. The leaf-superset hatch exempts a closed schema from the surplus bound (the suppressed missing fields are unbounded), while an OPEN schema stays bounded to one extra leaf so a leaf-count blow-up still fails. schemaIsClosed detects a top-level close({…}). Seeded the SI-suffix and close-suppression forms; both packages 100% covered. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): skip the nested-disjunction duplicate-default case (carry to round 2) The fuzzer found one disjunction-default shape the in-house engine gets wrong: a parenthesized nested disjunction whose marked default value also appears unmarked among its siblings (`{A: (*0|0)|10}`) wrongly accepts an absent A. The engine flattens the disjunction and keeps the *0 default; CUE evaluates the sub-disjunction first, so the nesting cancels the default and CUE rejects. Matching CUE needs nesting-preserving default propagation in buildDisjunction — a structural evaluator change deferred to round 2 (recorded in plan 239). schemaHasNestedDuplicateDefault skips the EXACT pattern before the oracle: a nested disjunction with an equal marked+unmarked disjunct. The flat `*0|0|1` and the non-duplicate nested `(*0|1)|10` both behave correctly and do NOT trip the detector, so no other wrong-accept is masked. Seeded the case. Both packages hold 100% statement coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * test(cuelitetest): seed string/bool nested-duplicate-default variants The nested-disjunction duplicate-default class is not numeric-only: the string (`(*"x"|"x")|"y"`) and bool (`(*true|true)|false`) forms diverge the same way, and schemaHasNestedDuplicateDefault catches them (via BasicLit text and Ident name). Seed both so the detector's literal and identifier paths stay exercised. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): bound-check list index and MinRunes in wide-integer space CodeQL flagged int64-to-int conversions before the bounds checks in list indexing and strings.MinRunes. On 32-bit targets (the plan-240 wasm build) a wide literal would truncate and could select a valid wrong element or invert the rune-count check. Compare in int64/float64 space before any narrowing. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * fix(cuelite): make the list-index narrowing provably bounded for CodeQL The int64-space length comparison was correct but CodeQL cannot connect int64(len(elems)) to the int range; bound the index by math.MaxInt32 explicitly before narrowing, which is semantically free for list literals. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: ⟨value,default⟩ pair model for disjunction defaults (P0a-c) Replace the flatten-and-mark-pointers default machinery with CUE's per-disjunct default mode threaded through build and meet: - engineValue.modes (parallel to branches) carries each disjunct's defaultMode (dfltMaybe < dfltNot < dfltIs), replacing the defaults pointer slice. combineMode is is-default-wins max. - evalDisjunction flattens by VALUE (flattenDisjunct), so a parenthesized sub-disjunction whose value collapses to one branch loses its default (nesting-sensitive cancellation): (*0|0)|10 now rejects, matching CUE. A flat *0|0|1 keeps the default. - unifyDisjunction takes the branch cross product with hasBottomLeaf pruning: a struct/list/bound branch with a NESTED bottom is pruned when any branch meets cleanly, so close({x:int})|close({y:string}) accepts {x:1}. When every branch fails, nested-dirty branches are kept so the failing field leaf still surfaces. Probed against cuelang v0.16.1. Red/green unit tests in p0_default_semantics_test.go. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: fixpoint thunk forcing, sibling-default resolution, meet thunk-ref check (P0d-f) - forceThunkFixpoint iterates soft force passes until no thunk newly resolves, then a final hard pass collapses leftovers — an acyclic chain n: [if m…], o: [if n…] resolves across passes (P0d). - evalIdent resolves a defaulted-disjunction sibling to its default for a comparison; a non-defaulted disjunction defers (P0e). - evalBinary defers an & meet carrying an unforced thunk so the struct ref check sees the undeclared reference instead of the eager meet erasing it (P0f). Probed against cuelang v0.16.1. Red/green in p0_default_semantics_test.go. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelitetest: delete nested-default carry skip, bound root-summary hatch The nested-disjunction duplicate-default class is fixed by the ⟨value, default⟩ model (P0b), so its pre-oracle skip and the schemaHasNestedDuplicateDefault / disjunctionHasMarkedUnmarkedDup / disjunctText machinery are deleted. The carry seeds become live seeds exercising the cancellation on every run. The root-summary hatch now bounds the in-house non-root leaf count by the schema's top-level field count (or maxExtraLeaves) when the oracle reports only the root, so a phantom-leaf fan-out cannot pass vacuously. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: backslash-tokenize raw surrogate scan; refresh stale post-flip comments (P1 g,i,j) - rawHasLoneSurrogateEscape consumes escaped backslashes (\\) before matching a \u escape, so a key like "<FFFD>\\ud800" (literal text \ud800, not a unicode escape) is accepted, matching CUE. A real lone-surrogate escape after \\ still rejects. Red/green unit tests + fuzz seed. - compile_cache.go, validate.go, runcache_wiring.go: rewrite the cross-context / source-retention / Ctx-field comments to the post-flip truth (immutable, context-free Value; CompileMap hot path). - lift.go liftAny: structs lift OPEN, not closed. - architecture/index.md: Compile/Unify/Validate are in-house; only the parser frontend delegates (removed in phase 4). https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: restore 100% coverage after the default-model redesign - Drop dead dedupeConcrete (branches are already deduped at build/meet time) and the unreachable non-defaulted-disjunction-in-scope branch in evalIdent; an in-scope disjunction is always concrete-with-default. - White-box tests for hasBottomLeaf list-tail/disjunction arms, the o-side final hard thunk force, and the truncated-\u raw scan bound. cue/cuelite and internal/cuelitetest both at 100% statements. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: reconcile the meet's default from operand defaults (fuzz fix) The 600s FuzzValidate run (first unrestricted pass over the default semantics) found (*0|int)&(0|*int) wrongly rejected: the raw branch-mode cross product fabricated a spurious second default {0,int}. unifyDisjunction now computes the meet default once from the OPERAND defaults (U2): a default survives when compatible with the other operand; both surviving reconcile by meet (0&int=0), one surviving stands (·(*1|2|9)&(*2|3|9)→2), build-time double marks stay ambiguous (*string|*"" rejects). Regression seeds committed (code + testdata corpus). cue/cuelite and internal/cuelitetest at 100%. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: concrete list/struct defaults (depends-on regression fix) The ⟨value,default⟩ redesign restricted default reconciliation, dedup, and branch-mode marking to concrete SCALARS, regressing concrete list/struct defaults: [...int] | *[] (the plan/ proto depends-on schema) wrongly rejected a PROVIDED empty list, breaking mdsmith check on 50 plan files. Add concreteValueEqual (scalar/list/struct deep equality) and route concreteOrNil, dedupeBranchModes, and the meet branch-mode marking through it plus isConcrete, so *[], *{x:0}, and *[1,2] deduplicate and reconcile like scalar defaults. Probed against cuelang v0.16.1. Regression tests + mdsmith check . clean. 100% coverage held. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * plan: record the ⟨value,default⟩ redesign (238); drop the carried-work item (239) Plan 238 gains a Task 5 recording the round-2 default-semantics redesign (per-disjunct mode, nesting-sensitive build, meet pruning + default reconciliation, fixpoint thunks, deleted carry). Plan 239's carried-in nested-default fix becomes a 'resolved in phase 2' note; the surface-C inventory is unchanged. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: reject ordered comparison on a non-orderable operand (fuzz fix) The unrestricted 600s FuzzValidate found a pre-existing divergence: 0>0>A parses as (0>0)>A = false>A, which CUE rejects at schema compile (bool is not orderable), while the in-house engine deferred a thunk that rejected at validate with a root path. evalComparison now rejects an ordered comparison (>, >=, <, <=) on a concrete bool/null operand eagerly at compile, even when the other operand is unresolved — the 'invalid operation' class hatch 1 already tolerates. == / != and string ordering stay valid. Probed against cuelang v0.16.1. Regression test + seeds; 100% coverage. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: reject chained ordered comparison even when inner defers (fuzz fix) The run-4 fuzz found 0>A>0 = (0>A)>0: the inner 0>A is bool-typed regardless of A, so the outer ordered op is invalid, but my prior fix only caught a concrete non-orderable operand and this one deferred. evalComparison now also rejects an ordered comparison whose operand is SYNTACTICALLY a comparison expression (isComparisonExpr) — bool-typed by construction — at compile, matching CUE. Legitimate single comparisons and == in comprehensions still defer and resolve. Regression tests + seed; 100% coverage; lint clean. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: reject indexing a non-list target even when the index defers (fuzz fix) The run-5 fuzz found 0[mech] (mech unresolved): CUE rejects indexing a non-list at compile (invalid operand, want list or struct) regardless of the index, but the in-house engine deferred a thunk because the index ref was unresolved. evalIndex now checks the index TARGET is a list literal BEFORE evaluating the index, so a non-list target is a hard compile error regardless of the index. Valid list-index-by-reference still resolves. Regression test + seed; 100% coverage; lint clean. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * cuelite: seed regex-match operands into the non-orderable reject set A =~ / !~ operand is bool-typed, so an ordered comparison over it must reject at compile even when the matched operand is a deferred string. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * ci(cuelite): exploratory differential fuzz job on engine-touching PRs FuzzValidate and FuzzParsePath hunt new engine-vs-oracle divergences out-of-band (240s each, path-filtered), with minimized crashers uploaded as artifacts. Seed replay stays in the test job; review rounds no longer run long fuzz loops. Retires with the oracle at plan 240. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 * ci(cuelite): parallelize the fuzz targets via a job matrix One job per fuzzer (fail-fast off, per-target crasher artifacts) halves the wall time of the exploratory fuzz gate. https://claude.ai/code/session_01LkXAFkv3U2K7Bcmz6dDS62 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2660481 commit c6c1e60

55 files changed

Lines changed: 8710 additions & 982 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/cuelite-fuzz.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: cuelite-fuzz
2+
3+
# Differential fuzzing for the in-house CUE-subset engine (plan 218).
4+
# The two fuzzers compare the cuelite engine against the direct-CUE
5+
# oracle in internal/cuelitetest; they exist until plan 240 deletes
6+
# the oracle, then this workflow retires with the harness.
7+
#
8+
# This job carries the EXPLORATORY budget. The regression layer — every
9+
# previously-found divergence replayed as f.Add seeds and committed
10+
# testdata corpus entries — already runs in the ordinary `test` job on
11+
# every push. Review rounds deliberately do not run long fuzz loops;
12+
# this workflow is where new divergences are hunted, out-of-band.
13+
14+
on:
15+
pull_request:
16+
paths:
17+
- "cue/cuelite/**"
18+
- "internal/cuelitetest/**"
19+
- ".github/workflows/cuelite-fuzz.yml"
20+
21+
permissions:
22+
contents: read
23+
24+
jobs:
25+
fuzz:
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 12
28+
strategy:
29+
fail-fast: false
30+
matrix:
31+
target: [FuzzValidate, FuzzParsePath]
32+
env:
33+
GOTOOLCHAIN: local
34+
steps:
35+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
36+
with:
37+
persist-credentials: false
38+
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
39+
with:
40+
go-version-file: go.mod
41+
- name: Fuzz ${{ matrix.target }} against the CUE oracle
42+
run: go test -run='^$' -fuzz='${{ matrix.target }}$' -fuzztime=240s ./internal/cuelitetest/
43+
# A failing fuzz run writes the minimized input under testdata/fuzz/.
44+
# Upload it so the find survives the ephemeral runner and can be
45+
# committed as a regression seed.
46+
- name: Upload minimized crashers
47+
if: failure()
48+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
49+
with:
50+
name: fuzz-crashers-${{ matrix.target }}
51+
path: internal/cuelitetest/testdata/fuzz/
52+
if-no-files-found: ignore

PLAN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ footer: |
156156
| 235 || sonnet | [Playwright end-to-end tests for the website, runnable by CI and agents](plan/235_playwright-site-e2e.md) |
157157
| 236 || opus | [cuelite phase 0 — package, façade, and differential harness](plan/236_cuelite-package-harness.md) |
158158
| 237 || sonnet | [cuelite phase 1 — surface D (placeholder paths)](plan/237_cuelite-surface-d.md) |
159-
| 238 | 🔲 | opus | [cuelite phase 2 — surfaces A + B (schema, query)](plan/238_cuelite-surfaces-ab.md) |
159+
| 238 | | opus | [cuelite phase 2 — surfaces A + B (schema, query)](plan/238_cuelite-surfaces-ab.md) |
160160
| 239 | 🔲 | opus | [cuelite phase 3 — surface C (row-expr evaluator)](plan/239_cuelite-surface-c.md) |
161-
| 240 | 🔲 | sonnet | [cuelite phase 4 — drop cuelang.org and enable tinygo](plan/240_cuelite-drop-cue.md) |
161+
| 240 | 🔲 | opus | [cuelite phase 4 — drop cuelang.org and enable tinygo](plan/240_cuelite-drop-cue.md) |
162162
| 241 || opus | [Schema-per-file config under `.mdsmith/schemas/`](plan/241_schema-files.md) |
163163
| 242 || opus | [proto.md schemas declare content entries via `<?content?>`](plan/242_proto-content-entries.md) |
164164
| 243 || sonnet | [`mdsmith extract` projects the document H1 as `title`](plan/243_extract-h1-title.md) |

cue/cuelite/access.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package cuelite
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
// Exists reports whether the Value names something concrete enough to read.
8+
// A bottom (a zero or error-carrying Value) does not exist; a compiled
9+
// value, or a successful [Value.LookupPath] result, does. A consumer uses
10+
// it to tell "the path resolved" from "the path was absent" without
11+
// inspecting the value further.
12+
func (v Value) Exists() bool {
13+
if _, ok := v.isBottom(); ok {
14+
return false
15+
}
16+
return true
17+
}
18+
19+
// Err reports whether the Value is a bottom — a compile failure, a zero
20+
// Value, or a value reduced to bottom by a conflicting Unify — or nil when
21+
// it names a value. Unlike [Value.Validate] it does NOT check
22+
// concreteness: a successfully compiled but non-concrete schema (a
23+
// constraint awaiting data) has no Err. A caller uses it to tell a broken
24+
// schema from one merely waiting on its document.
25+
func (v Value) Err() error {
26+
if err, ok := v.isBottom(); ok {
27+
return err
28+
}
29+
// A struct or list reduced by a conflicting Unify carries a ⊥ at the
30+
// offending leaf while the container itself is not ⊥. Err reports that
31+
// reduced-to-bottom value (matching cue.Value.Err), so checkUnifiable and
32+
// the cached-schema status check see a conflict.
33+
if b := firstBottom(v.v); b != nil {
34+
return newPathError(b.path, b.reason, nil)
35+
}
36+
return nil
37+
}
38+
39+
// firstBottom returns the first ⊥ engine value reachable in v (depth-first),
40+
// or nil when v carries none. It lets Err and Compile detect a conflict that
41+
// reduced a nested field to ⊥ without collapsing the whole container, so the
42+
// per-leaf paths survive for Validate.
43+
func firstBottom(v *engineValue) *engineValue {
44+
switch v.kind {
45+
case kBottom:
46+
return v
47+
case kStruct:
48+
for _, f := range v.fields {
49+
if b := firstBottom(f.val); b != nil {
50+
return b
51+
}
52+
}
53+
case kList:
54+
for _, el := range v.prefix {
55+
if b := firstBottom(el); b != nil {
56+
return b
57+
}
58+
}
59+
}
60+
return nil
61+
}
62+
63+
// LookupPath returns the Value at p within v, and whether it exists. A
64+
// missing leaf, or a lookup against a bottom, returns ok=false; an empty
65+
// path returns the receiver. Because a Value is context-free, the result is
66+
// immediately usable and shareable with no rebuild.
67+
func (v Value) LookupPath(p Path) (Value, bool) {
68+
if _, ok := v.isBottom(); ok {
69+
return bottom(errZeroValue), false
70+
}
71+
cur := v.v
72+
for _, seg := range p.segments {
73+
next, ok := lookupField(cur, seg)
74+
if !ok {
75+
return bottom(errZeroValue), false
76+
}
77+
cur = next
78+
}
79+
return Value{v: cur}, true
80+
}
81+
82+
// lookupField resolves one path segment within a value: a struct field by
83+
// name. A list, scalar, or any non-struct value has no string-labelled
84+
// members, so a lookup into one returns ok=false — mirroring cue.Value's
85+
// string-selector lookup, which does not index a list by a string segment.
86+
// Query and schema only ever look up struct keys.
87+
func lookupField(v *engineValue, seg string) (*engineValue, bool) {
88+
if v.kind != kStruct {
89+
return nil, false
90+
}
91+
for _, f := range v.fields {
92+
if f.name == seg {
93+
return f.val, true
94+
}
95+
}
96+
return nil, false
97+
}
98+
99+
// Field is one member of a struct Value: its label (the unquoted selector
100+
// string, usable directly with [MakePath]) and its Value.
101+
type Field struct {
102+
Selector string
103+
Value Value
104+
}
105+
106+
// Fields returns the members of a struct Value in definition order, or nil
107+
// for a non-struct or bottom Value. Each [Field.Selector] is the raw label
108+
// string, so a consumer building a path from it must use [MakePath] (not
109+
// [ParsePath]), which stores a dotted or hyphenated key verbatim.
110+
func (v Value) Fields() []Field {
111+
if _, ok := v.isBottom(); ok {
112+
return nil
113+
}
114+
if v.v.kind != kStruct {
115+
return nil
116+
}
117+
out := make([]Field, 0, len(v.v.fields))
118+
for _, f := range v.v.fields {
119+
out = append(out, Field{Selector: f.name, Value: Value{v: f.val}})
120+
}
121+
return out
122+
}
123+
124+
// String returns the Value's concrete string, or an error when it is not a
125+
// concrete string. A bottom returns its reason (wrapped, so errors.Is
126+
// reaches the sentinel).
127+
func (v Value) String() (string, error) {
128+
if err, ok := v.isBottom(); ok {
129+
return "", err
130+
}
131+
if v.v.kind != kString {
132+
return "", fmt.Errorf("cuelite: value is %s, not a concrete string", v.v.describe())
133+
}
134+
return v.v.str, nil
135+
}
136+
137+
// Decode unmarshals the Value into the Go value x points at. A bottom
138+
// returns its reason; a non-concrete value (an unresolved schema, not data)
139+
// errors rather than filling x with a zero value. Decode supports the
140+
// concrete shapes mdsmith's callers read out: a string into *string, and a
141+
// struct/list/scalar into *any.
142+
func (v Value) Decode(x any) error {
143+
if err, ok := v.isBottom(); ok {
144+
return err
145+
}
146+
goVal, err := decodeValue(v.v)
147+
if err != nil {
148+
return err
149+
}
150+
switch dst := x.(type) {
151+
case *any:
152+
*dst = goVal
153+
return nil
154+
case *string:
155+
s, ok := goVal.(string)
156+
if !ok {
157+
return fmt.Errorf("cuelite: cannot decode %s into *string", v.v.describe())
158+
}
159+
*dst = s
160+
return nil
161+
case *map[string]any:
162+
m, ok := goVal.(map[string]any)
163+
if !ok {
164+
return fmt.Errorf("cuelite: cannot decode %s into *map[string]any", v.v.describe())
165+
}
166+
*dst = m
167+
return nil
168+
default:
169+
return fmt.Errorf("cuelite: Decode does not support target type %T", x)
170+
}
171+
}
172+
173+
// decodeValue converts a concrete engine value into a plain Go value
174+
// (string, int64, float64, bool, []byte, nil, map[string]any, []any). A
175+
// non-concrete leaf (a type, bound, or multi-branch disjunction) errors,
176+
// matching cue.Value.Decode's refusal to fill from an incomplete value.
177+
func decodeValue(v *engineValue) (any, error) {
178+
switch v.kind {
179+
case kNull:
180+
return nil, nil
181+
case kString:
182+
return v.str, nil
183+
case kInt:
184+
return v.i, nil
185+
case kFloat:
186+
return v.f, nil
187+
case kBool:
188+
return v.b, nil
189+
case kBytes:
190+
return v.bytes, nil
191+
case kStruct:
192+
out := make(map[string]any, len(v.fields))
193+
for _, f := range v.fields {
194+
child, err := decodeValue(f.val)
195+
if err != nil {
196+
return nil, err
197+
}
198+
out[f.name] = child
199+
}
200+
return out, nil
201+
case kList:
202+
out := make([]any, 0, len(v.prefix))
203+
for _, el := range v.prefix {
204+
child, err := decodeValue(el)
205+
if err != nil {
206+
return nil, err
207+
}
208+
out = append(out, child)
209+
}
210+
return out, nil
211+
default:
212+
return nil, fmt.Errorf("cuelite: cannot decode incomplete value %s", v.describe())
213+
}
214+
}

0 commit comments

Comments
 (0)