hecks 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/d1.rb +38 -14
- data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
- data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
- data/lib/hecks/adapters/driven/heki.rb +40 -9
- data/lib/hecks/adapters/driven/lambda.rb +16 -2
- data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
- data/lib/hecks/adapters/driven/postgres.rb +76 -29
- data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
- data/lib/hecks/adapters/driven/sqlite.rb +30 -9
- data/lib/hecks/adapters/driven.rb +19 -1
- data/lib/hecks/behaviors/dsl.rb +29 -0
- data/lib/hecks/behaviors/expectations.rb +62 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
- data/lib/hecks/bluebook/assembly/marks.rb +4 -3
- data/lib/hecks/bluebook/assembly.rb +14 -1
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
- data/lib/hecks/bluebook/chapter.rb +21 -11
- data/lib/hecks/bluebook/command.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
- data/lib/hecks/bluebook/dsl/command_builder.rb +91 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
- data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
- data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
- data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
- data/lib/hecks/bluebook/expression/projection.json +48 -0
- data/lib/hecks/bluebook/expression/resolver.rb +161 -10
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
- data/lib/hecks/bluebook/meta_validator.rb +103 -13
- data/lib/hecks/bluebook/model_check.rb +132 -5
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/value_object.rb +9 -1
- data/lib/hecks/doc/reference.rb +22 -1
- data/lib/hecks/facade/cli_door.rb +6 -3
- data/lib/hecks/facade/json_door.rb +16 -4
- data/lib/hecks/forms/app.rb +47 -6
- data/lib/hecks/forms/command_form_renderer.rb +1 -1
- data/lib/hecks/forms/field_renderer.rb +11 -4
- data/lib/hecks/forms/html.rb +31 -0
- data/lib/hecks/forms/params.rb +30 -1
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/record_renderer.rb +6 -2
- data/lib/hecks/forms/record_table.rb +6 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
- data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
- data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
- data/lib/hecks/fuzzing/properties.rb +52 -6
- data/lib/hecks/fuzzing/replay.rb +51 -18
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
- data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
- data/lib/hecks/fuzzing/value_generator.rb +25 -2
- data/lib/hecks/grammar/evolve.rb +33 -0
- data/lib/hecks/grammar/expression.bluebook +8 -5
- data/lib/hecks/grammar/expression_operators.json +497 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
- data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
- data/lib/hecks/language/bluebook/command.bluebook +42 -8
- data/lib/hecks/language/bluebook/entity.bluebook +86 -10
- data/lib/hecks/language/bluebook/policy.bluebook +21 -4
- data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
- data/lib/hecks/language/bluebook/projection.bluebook +6 -6
- data/lib/hecks/language/bluebook/query.bluebook +4 -4
- data/lib/hecks/language/bluebook/shape.bluebook +6 -6
- data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +30 -2
- data/lib/hecks/naming.rb +54 -1
- data/lib/hecks/ports/access_control.port +7 -2
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.port +6 -2
- data/lib/hecks/ports/agent.rb +1 -1
- data/lib/hecks/ports/authentication.port +4 -2
- data/lib/hecks/ports/authentication.rb +1 -1
- data/lib/hecks/ports/authorization.port +5 -2
- data/lib/hecks/ports/authorization.rb +14 -11
- data/lib/hecks/ports/clock.port +3 -2
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.port +3 -2
- data/lib/hecks/ports/extraction.rb +1 -1
- data/lib/hecks/ports/identity_assignment.port +3 -2
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.port +3 -2
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.port +3 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/execution.rb +6 -1
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
- data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence.rb +1 -1
- data/lib/hecks/ports/projection.rb +20 -0
- data/lib/hecks/projections/diagrams.rb +230 -1
- data/lib/hecks/projections/shape.rb +15 -2
- data/lib/hecks/projector/cli_projector.rb +10 -1
- data/lib/hecks/projector/exporter.rb +19 -2
- data/lib/hecks/query_ir.rb +19 -0
- data/lib/hecks/query_specification/common/null_policy.rb +13 -1
- data/lib/hecks/query_specification/field_path.rb +20 -2
- data/lib/hecks/query_specification/hop_path.rb +7 -5
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +19 -3
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
- data/lib/hecks/runtime/command_interpreter.rb +97 -13
- data/lib/hecks/runtime/command_rules/admissibility.rb +64 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +7 -1
- data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
- data/lib/hecks/runtime/command_rules/references.rb +27 -19
- data/lib/hecks/runtime/dependency_planning.rb +14 -0
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +85 -14
- data/lib/hecks/runtime/errors.rb +22 -0
- data/lib/hecks/runtime/identity.rb +30 -2
- data/lib/hecks/runtime/instance.rb +59 -4
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/loader.rb +59 -18
- data/lib/hecks/runtime/query_interpreter.rb +36 -4
- data/lib/hecks/runtime/reaction_invocation.rb +9 -1
- data/lib/hecks/runtime/read_model_interpreter.rb +76 -1
- data/lib/hecks/runtime/refusal_wording.rb +2 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
- data/lib/hecks/runtime/registry/verification.rb +88 -0
- data/lib/hecks/runtime/registry.rb +69 -8
- data/lib/hecks/runtime/saga_interpreter.rb +215 -13
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/value/admission.rb +19 -1
- data/lib/hecks/runtime/value/coercion.rb +75 -10
- data/lib/hecks/runtime.rb +17 -5
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +7 -2
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +48 -37
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
- /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
require_relative "../bluebook/expression/resolver"
|
|
2
|
+
require_relative "../bluebook/expression/evaluator"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Fuzzing
|
|
6
|
+
# Phase 7 (equivalence-gap plan) — the one place in this codebase a
|
|
7
|
+
# genuine "true for all inputs," not merely "true for every input we
|
|
8
|
+
# happened to sample," claim is realistically achievable:
|
|
9
|
+
# `Hecks::Bluebook::Expression::{Evaluator,Resolver}`. It's finite-
|
|
10
|
+
# grammar and side-effect-free (Evaluator::PROJECTION/Resolver's own
|
|
11
|
+
# `interpret` never touch a database, a clock, or randomness), but NOT
|
|
12
|
+
# small enough for naive enumeration — ~20 node-producing constructs
|
|
13
|
+
# across two mutually-recursive grammar layers, unbounded recursion
|
|
14
|
+
# through dotted `Lookup` paths, `Addition`, and nested block
|
|
15
|
+
# predicates (`Resolver → Evaluator → Resolver → …`). A flat "every
|
|
16
|
+
# string up to length N" or "every node combination up to depth N with
|
|
17
|
+
# no type filter" explodes well past anything runnable long before
|
|
18
|
+
# depth 3 (two bugs this session lived in exactly this sublanguage —
|
|
19
|
+
# the `formerly_known_as`/`unmark_scalar` family, and the
|
|
20
|
+
# `canonical_form.rb` aliasing bug — neither would have been caught by
|
|
21
|
+
# sampling alone, which is the whole reason this phase exists).
|
|
22
|
+
#
|
|
23
|
+
# TYPE-DIRECTED, not exhaustive-over-strings: this generator only ever
|
|
24
|
+
# recurses into a sub-expression whose OWN type the surrounding
|
|
25
|
+
# construct actually accepts (never `"x".modulo(true)`) — mirroring,
|
|
26
|
+
# by hand, the same admitted-receiver-class tables `Resolver`'s own
|
|
27
|
+
# `interpret` enforces at runtime (`SizedType`/`ToStringType`/
|
|
28
|
+
# `IncludeHaystack` from vocabulary.bluebook, plus the ad hoc
|
|
29
|
+
# `is_a?`/`respond_to?` checks `numeric`/`size_of`/`string_of`/etc.
|
|
30
|
+
# apply that vocabulary.bluebook itself doesn't declare). Restricted
|
|
31
|
+
# to a small representative literal palette (TYPE_LEAVES, below) and a
|
|
32
|
+
# small synthetic attribute set per type (SYNTHETIC_ATTRS), bounded to
|
|
33
|
+
# MAX_DEPTH — that combination is what keeps this tractable (thousands
|
|
34
|
+
# to tens of thousands of cases, not millions) while still being a
|
|
35
|
+
# real, defensible "proven for every well-typed expression up to depth
|
|
36
|
+
# 3" claim for exactly the sublanguage that already bit this project
|
|
37
|
+
# twice.
|
|
38
|
+
#
|
|
39
|
+
# WHAT "PROVEN" MEANS HERE, PRECISELY: every expression this generator
|
|
40
|
+
# produces is well-typed BY CONSTRUCTION (every sub-expression's type
|
|
41
|
+
# matches what its parent construct actually accepts, per this file's
|
|
42
|
+
# own TYPE_RULES). Interpreting a well-typed expression must never
|
|
43
|
+
# raise anything OTHER than `Hecks::Bluebook::Expression::
|
|
44
|
+
# EvaluationError` — that class alone is the sublanguage's own real
|
|
45
|
+
# refusal vocabulary (a divisor that happens to be zero, a `Lookup`
|
|
46
|
+
# this run's synthetic state doesn't happen to carry); anything else
|
|
47
|
+
# escaping (a raw `TypeError`/`NoMethodError`/`ZeroDivisionError`) is
|
|
48
|
+
# exactly the crash-signature this whole file's own header comments
|
|
49
|
+
# (resolver.rb, block_predicates.rb) name as the historical bug class
|
|
50
|
+
# this sublanguage keeps almost-but-not-quite avoiding. This does NOT
|
|
51
|
+
# prove the interpreter computes the SEMANTICALLY right answer (that
|
|
52
|
+
# needs a second, independent oracle — out of scope here, exactly the
|
|
53
|
+
# way rust_conformance_fuzz_spec.rb is the cross-RUNTIME half of this
|
|
54
|
+
# same idea) — only that it never crashes on well-typed input.
|
|
55
|
+
module BoundedExhaustiveExpressions
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
# `:numeric` deliberately covers BOTH Integer and Float — the
|
|
59
|
+
# resolver's own `numeric`/`require_number` never distinguish them
|
|
60
|
+
# (resolver.rb) — so a generator that kept them as separate types
|
|
61
|
+
# would be modeling a distinction the grammar itself doesn't make.
|
|
62
|
+
TYPES = %i[numeric string boolean array nil_type].freeze
|
|
63
|
+
|
|
64
|
+
# THE PALETTE — deliberately small (this file's own header: "a small
|
|
65
|
+
# representative literal palette," matching the plan's own `{0, 1,
|
|
66
|
+
# -1}` example). Widening it doesn't test a different SHAPE of
|
|
67
|
+
# expression, only more values through the same shapes — real
|
|
68
|
+
# boundary-value coverage (Bignum, NaN, empty string, unicode) is
|
|
69
|
+
# PRD 05's job (spec/runtime/numeric_boundary_spec.rb,
|
|
70
|
+
# ValueGenerator's own edge-case tables), not this generator's.
|
|
71
|
+
TYPE_LEAVES = {
|
|
72
|
+
numeric: ["0", "1", "-1"].freeze,
|
|
73
|
+
string: ['""', '"a"'].freeze,
|
|
74
|
+
boolean: %w[true false].freeze,
|
|
75
|
+
nil_type: %w[nil].freeze
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
# TWO SYNTHETIC ATTRIBUTES PER TYPE (the plan's own "2-3 fake
|
|
79
|
+
# attributes per type") — real names are irrelevant to `Resolver
|
|
80
|
+
# .parse` (it never touches state/attrs at all, confirmed directly:
|
|
81
|
+
# only `Lookup#interpret` does) and irrelevant to `interpret` beyond
|
|
82
|
+
# needing to resolve against WHATEVER synthetic state this file
|
|
83
|
+
# supplies (`synthetic_state`, below) — so any non-colliding,
|
|
84
|
+
# non-suffix-shaped name works. Never named the same as a
|
|
85
|
+
# `BLOCK_PARAM` (below) — `Resolver#fetch`'s own `attrs`-wins-over-
|
|
86
|
+
# `state` precedence means a block parameter SHADOWS a same-named
|
|
87
|
+
# top-level attribute for the span of its own predicate, a real
|
|
88
|
+
# hazard this file sidesteps by construction rather than exercising
|
|
89
|
+
# it here (a real, cited resolver.rb hazard, not a gap in THIS
|
|
90
|
+
# generator's own coverage claim).
|
|
91
|
+
SYNTHETIC_ATTRS = {
|
|
92
|
+
numeric: %w[num_a num_b].freeze,
|
|
93
|
+
string: %w[str_a str_b].freeze,
|
|
94
|
+
boolean: %w[bool_a bool_b].freeze,
|
|
95
|
+
array: %w[arr_num arr_str].freeze
|
|
96
|
+
}.freeze
|
|
97
|
+
|
|
98
|
+
# `arr_num`/`arr_str` — ELEMENT type per array attribute, needed so
|
|
99
|
+
# a block predicate's own bound parameter (`BLOCK_PARAM`, below) is
|
|
100
|
+
# generated against the right leaf/production set for whatever it's
|
|
101
|
+
# actually bound to.
|
|
102
|
+
ARRAY_ELEMENT_TYPE = { "arr_num" => :numeric, "arr_str" => :string }.freeze
|
|
103
|
+
|
|
104
|
+
BLOCK_PARAM = "el"
|
|
105
|
+
|
|
106
|
+
MAX_DEPTH = 3
|
|
107
|
+
|
|
108
|
+
# `state`/`attrs` — a plain Hash satisfies BOTH (confirmed directly:
|
|
109
|
+
# `Resolver#known?` degrades to a bare index check when `state`
|
|
110
|
+
# doesn't `respond_to?(:key?)`, and `fetch` only ever needs
|
|
111
|
+
# `attrs.key?`/`attrs[]`.). Split ACROSS both, matching a real
|
|
112
|
+
# dispatch's own shape (some names come from the record's stored
|
|
113
|
+
# state, some from the command's own args) — not load-bearing for
|
|
114
|
+
# THIS proof (attrs wins regardless), but keeps the synthetic input
|
|
115
|
+
# closer to what `Admissibility` actually builds, in case anything
|
|
116
|
+
# here is reused for a future deeper check.
|
|
117
|
+
#
|
|
118
|
+
# THE VO-VS-SCALAR UNWRAP CASE, DELIBERATELY EXERCISED — half of
|
|
119
|
+
# `num_a`/`str_a`/`bool_a`/one array attribute's elements are bare
|
|
120
|
+
# scalars, the other half (`num_b`/`str_b`/`bool_b`/the other
|
|
121
|
+
# array's elements) are wrapped `{value: X}` — the single-field
|
|
122
|
+
# Value Object shape `Resolver#unwrap_scalar` auto-collapses.
|
|
123
|
+
# resolver.rb's own most serious historical bug in this file (the
|
|
124
|
+
# "UPDATE 2026-08-18" comment: a dotted walk that landed on an
|
|
125
|
+
# un-unwrapped VO compared `false` against every literal, SILENTLY,
|
|
126
|
+
# never raising) lived exactly at this boundary — a generator that
|
|
127
|
+
# only ever supplied bare scalars would never exercise the code
|
|
128
|
+
# path that bug lived in at all.
|
|
129
|
+
# `SingleFieldVO` — NOT a plain Hash, deliberately: `Resolver
|
|
130
|
+
# #unwrap_scalar`'s own guard is `value.respond_to?(:to_h) &&
|
|
131
|
+
# !value.is_a?(Hash) && !value.is_a?(Array)` — it exists
|
|
132
|
+
# specifically to collapse a real hydrated `Runtime::Value`
|
|
133
|
+
# instance (which responds to `#to_h` but is never itself a bare
|
|
134
|
+
# Hash) down to its lone scalar field, and just as deliberately
|
|
135
|
+
# leaves an ACTUAL Hash alone (a genuinely un-hydrated, multi-
|
|
136
|
+
# field record has no single scalar to collapse to). A first
|
|
137
|
+
# version of this generator's own synthetic state used plain
|
|
138
|
+
# `{value: X}` Hashes to stand in for a single-field VO — which
|
|
139
|
+
# `is_a?(Hash)` is true for, so `unwrap_scalar` correctly left them
|
|
140
|
+
# WRAPPED, and every VO-typed synthetic attribute then failed
|
|
141
|
+
# `Addition`/`Compare`/every scalar-typed operation with "expects a
|
|
142
|
+
# number, got {\"value\":5}" — a bug in THIS generator's own
|
|
143
|
+
# synthetic state, not in `Resolver`, caught only by noticing that
|
|
144
|
+
# `num_b == 5` (`num_b` a plain `{value: 5}` Hash) evaluated to
|
|
145
|
+
# `false` instead of `true` before this fix.
|
|
146
|
+
SingleFieldVO = Struct.new(:value) do
|
|
147
|
+
def to_h = { value: value }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def synthetic_state
|
|
151
|
+
{
|
|
152
|
+
num_a: 3,
|
|
153
|
+
num_b: SingleFieldVO.new(5),
|
|
154
|
+
str_a: "hello",
|
|
155
|
+
str_b: SingleFieldVO.new("world"),
|
|
156
|
+
bool_a: true,
|
|
157
|
+
bool_b: SingleFieldVO.new(false),
|
|
158
|
+
arr_num: [1, SingleFieldVO.new(2), 3],
|
|
159
|
+
arr_str: ["x", SingleFieldVO.new("y"), "z"]
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def synthetic_attrs = {}
|
|
164
|
+
|
|
165
|
+
# `leaves(type, depth)` — every TERMINAL (non-recursive) expression
|
|
166
|
+
# of `type`: the fixed literal palette plus every synthetic
|
|
167
|
+
# attribute NAME declared for that type (never their VALUES — this
|
|
168
|
+
# generates TEXT, the same source a real `given`/`invariant` author
|
|
169
|
+
# would write; `synthetic_state`, above, is what gives those names
|
|
170
|
+
# meaning at `interpret` time).
|
|
171
|
+
def leaves(type)
|
|
172
|
+
(TYPE_LEAVES[type] || []) + (SYNTHETIC_ATTRS[type] || []) + Array(bound_leaves[type])
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A STACK, not a single slot — a NESTED block predicate (the real
|
|
176
|
+
# corpus already does this two levels deep, roster.bluebook's own
|
|
177
|
+
# `seats.any? { |s| assignments.none? { |a| … } }`) pushes a second
|
|
178
|
+
# bound leaf while the outer one is still active. Both share the
|
|
179
|
+
# SAME `BLOCK_PARAM` spelling ("el") — real Ruby block-parameter
|
|
180
|
+
# shadowing (the inner `el` simply shadows the outer one within its
|
|
181
|
+
# own predicate text), which this generator treats as legal on
|
|
182
|
+
# purpose: `resolver.rb`'s own `interpret_with_element` binds fresh
|
|
183
|
+
# `attrs.merge(param => element)` per level regardless of what the
|
|
184
|
+
# outer level already bound, so a shadowed name still interprets
|
|
185
|
+
# correctly — this generator is proving "does it crash," not "is
|
|
186
|
+
# every generated predicate semantically distinct."
|
|
187
|
+
def bound_leaves = @bound_leaves ||= Hash.new { |h, k| h[k] = [] }
|
|
188
|
+
|
|
189
|
+
def with_element_leaf(element_type)
|
|
190
|
+
bound_leaves[element_type] << BLOCK_PARAM
|
|
191
|
+
cache.delete_if { |(type, _depth), _| type == element_type }
|
|
192
|
+
yield
|
|
193
|
+
ensure
|
|
194
|
+
bound_leaves[element_type].pop
|
|
195
|
+
cache.delete_if { |(type, _depth), _| type == element_type }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# `productions(type, depth)` — every expression of `type` reachable
|
|
199
|
+
# in AT MOST `depth` recursive steps, MEMOIZED (the same sub-
|
|
200
|
+
# expression set is reused at every enclosing recursion, so without
|
|
201
|
+
# memoizing, cost would compound multiplicatively per level instead
|
|
202
|
+
# of additively). `depth` 0 is exactly `leaves(type)`; each
|
|
203
|
+
# increment adds every construct THIS FILE'S OWN TYPE_RULES (below)
|
|
204
|
+
# says can produce `type`, built from `depth - 1` sub-expressions.
|
|
205
|
+
def productions(type, depth)
|
|
206
|
+
cache[[type, depth]] ||= begin
|
|
207
|
+
base = leaves(type)
|
|
208
|
+
depth <= 0 ? base : (base + recursive_productions(type, depth)).uniq
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def cache = @cache ||= {}
|
|
213
|
+
|
|
214
|
+
def recursive_productions(type, depth)
|
|
215
|
+
case type
|
|
216
|
+
when :numeric then numeric_productions(depth)
|
|
217
|
+
when :string then string_productions(depth)
|
|
218
|
+
when :boolean then boolean_productions(depth)
|
|
219
|
+
when :array then array_productions(depth)
|
|
220
|
+
when :nil_type then [] # no recursive producer of nil in this grammar — Find's "not found" is a runtime OUTCOME, not a distinct construct to render as source text
|
|
221
|
+
else raise ArgumentError, "no production rule for type #{type.inspect}"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# EVERY internal use of a sub-expression LIST (as opposed to the
|
|
226
|
+
# single final list `productions(type, depth)` returns to its own
|
|
227
|
+
# caller) goes through this, not `productions` directly — the
|
|
228
|
+
# actual thing that made an early version of this generator explode
|
|
229
|
+
# past a million cases by depth 3 wasn't `pairs`' own cross product
|
|
230
|
+
# (already sampled) but the dozen-plus LINEAR `flat_map`/`map`
|
|
231
|
+
# passes `boolean_productions` alone makes over `str`/`num`/`sub` —
|
|
232
|
+
# each individually harmless, but an unbounded few-thousand-item
|
|
233
|
+
# list run through a dozen of them, feeding the NEXT depth's own
|
|
234
|
+
# dozen passes, compounds fast. Bounding every INPUT list (not the
|
|
235
|
+
# final output) keeps the shape diversity `sample`'s even-spacing
|
|
236
|
+
# already preserves while keeping growth roughly linear in depth
|
|
237
|
+
# instead of combinatorial.
|
|
238
|
+
def bounded(type, depth) = sample(productions(type, depth))
|
|
239
|
+
|
|
240
|
+
# NUMERIC ← Addition(numeric, numeric) | Modulo(numeric, numeric) |
|
|
241
|
+
# Size(sized) | First/Last(numeric array). `Size` returns an
|
|
242
|
+
# Integer for a String OR an Array receiver alike (`SizedType` —
|
|
243
|
+
# `size_of`, resolver.rb) — both sides generated here.
|
|
244
|
+
def numeric_productions(depth)
|
|
245
|
+
sub = bounded(:numeric, depth - 1)
|
|
246
|
+
# `Modulo`'s own RECEIVER (not its argument — that side already
|
|
247
|
+
# goes through `Resolver#matching_paren`'s own fresh, self-
|
|
248
|
+
# contained re-parse, confirmed safe for any numeric shape
|
|
249
|
+
# including another `Addition`/`Modulo`) has the identical
|
|
250
|
+
# "`Addition` mis-parsed as a suffix receiver" hazard
|
|
251
|
+
# `resolver_numeric_leaves`'s own comment documents for `.to_s`/
|
|
252
|
+
# `.positive?` — `Resolver.parse` tries `split_addition` BEFORE
|
|
253
|
+
# `match_call`, so `"0 + 0.modulo(1)"` (meant as `(0 + 0)
|
|
254
|
+
# .modulo(1)`) actually parses as `0 + (0.modulo(1))`. Restricted
|
|
255
|
+
# to `resolver_numeric_leaves` on the RECEIVER side only — the
|
|
256
|
+
# argument stays the full, unrestricted numeric set.
|
|
257
|
+
pairs(sub).map { |a, b| "#{a} + #{b}" } +
|
|
258
|
+
cross(resolver_numeric_leaves(depth - 1), sub).map { |a, b| "#{a}.modulo(#{b})" } +
|
|
259
|
+
bounded(:string, depth - 1).map { |s| "#{s}.size" } +
|
|
260
|
+
bounded(:array, depth - 1).map { |a| "#{a}.size" } +
|
|
261
|
+
sample(numeric_array_productions(depth - 1)).flat_map { |a| ["#{a}.first", "#{a}.last"] }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# A "boolean" IN RESOLVER'S OWN SENSE — safe to embed as the
|
|
265
|
+
# RECEIVER of a trailing Resolver-level suffix (`.to_s`, and
|
|
266
|
+
# anywhere else a boolean-typed VALUE, as opposed to a boolean-
|
|
267
|
+
# typed EXPRESSION, is wanted). FOUND LIVE, the same way the
|
|
268
|
+
# nested-`.modulo` bug was: `Resolver.parse` has NO KNOWLEDGE of
|
|
269
|
+
# `==`/`<`/`&&`/`||`/leading `!`/`.include?` AT ALL — those are
|
|
270
|
+
# `Evaluator`'s OWN, entirely separate parsing layer, stripped off
|
|
271
|
+
# BEFORE anything reaches `Resolver.parse` at all (confirmed
|
|
272
|
+
# directly: `Resolver.parse("str_b < str_a")` — no `.` anywhere in
|
|
273
|
+
# that text for any suffix regex to anchor on — falls through
|
|
274
|
+
# every leaf regex to the `Lookup` catch-all, exactly like the
|
|
275
|
+
# nested-modulo bug did). `bounded(:boolean, depth)`'s FULL set
|
|
276
|
+
# includes `Compare`/`Include`/`Or`/`And`/`Not` — genuinely boolean-
|
|
277
|
+
# TYPED at `interpret` time, but `Evaluator`-level SYNTAX, not
|
|
278
|
+
# something `Resolver.parse` can ever recognize as a receiver no
|
|
279
|
+
# matter how it's parenthesized (confirmed directly too: `Resolver
|
|
280
|
+
# .parse` never strips parens at all — `"(3)"` alone already fails
|
|
281
|
+
# to resolve). This is a REAL, PERMANENT boundary of the actual
|
|
282
|
+
# grammar (this whole sublanguage's own two-layer split, not a
|
|
283
|
+
# limitation to work around) — a `given`/`invariant` author simply
|
|
284
|
+
# cannot write `(a < b).to_s` in this language, ever, no matter how
|
|
285
|
+
# they punctuate it. So this generator doesn't either: only
|
|
286
|
+
# RESOLVER-LEVEL boolean-producing constructs (bare literals/
|
|
287
|
+
# lookups, `SignTest`, `Empty`, `Presence`, `MatchesRegex`,
|
|
288
|
+
# `StartsWith`/`EndsWith`, `BlockPredicate` — every one of them
|
|
289
|
+
# parsed via a suffix regex INSIDE `Resolver.parse` itself, per
|
|
290
|
+
# this generator's own design report) are eligible here.
|
|
291
|
+
def resolver_boolean_leaves(depth)
|
|
292
|
+
leaves(:boolean) +
|
|
293
|
+
resolver_numeric_leaves(depth).flat_map { |n| ["#{n}.positive?", "#{n}.negative?", "#{n}.zero?"] } +
|
|
294
|
+
bounded(:string, depth).flat_map { |s| ["#{s}.present?", "#{s}.blank?", "#{s}.match?(/a/)", "#{s}.start_with?(\"a\")", "#{s}.end_with?(\"a\")"] } +
|
|
295
|
+
(sample(numeric_array_productions(depth)) + sample(string_array_productions(depth)) + bounded(:array, depth)).map { |x| "#{x}.empty?" } +
|
|
296
|
+
sample(block_predicate_productions(depth))
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# A "numeric" IN RESOLVER'S OWN SENSE — `Addition`'s twin of
|
|
300
|
+
# `resolver_boolean_leaves`'s own restriction, found the identical
|
|
301
|
+
# way: `"0 + 0.to_s"` (meant as `(0 + 0).to_s`) actually parses as
|
|
302
|
+
# `0 + (0.to_s)`, because `Resolver.parse` tries `split_addition`
|
|
303
|
+
# BEFORE `.to_s`'s own suffix regex in its dispatch order — the `+`
|
|
304
|
+
# "wins" the split before the suffix ever gets a chance to anchor
|
|
305
|
+
# on its own receiver boundary. Confirmed to have zero real-corpus
|
|
306
|
+
# precedent either (`grep`, no `bluebook` file anywhere chains a
|
|
307
|
+
# method or sign-test onto a parenthesized addition) — the same
|
|
308
|
+
# verdict as `resolver_boolean_leaves`'s own comparisons/`&&`/`||`:
|
|
309
|
+
# a real permanent grammar boundary (`Resolver.parse` never strips
|
|
310
|
+
# parens, confirmed directly, so no amount of punctuation rescues
|
|
311
|
+
# `(a + b).to_s`), not a bug to fix in the resolver for a shape
|
|
312
|
+
# nothing has ever needed. `Modulo`/`Size`/`First`/`Last` stay IN
|
|
313
|
+
# (each is its own trailing `.method(...)`/`.method` call, so
|
|
314
|
+
# `Resolver.parse`'s greedy `(.+)\.suffix\z` regexes correctly
|
|
315
|
+
# isolate them as a receiver regardless of what precedes them —
|
|
316
|
+
# only bare top-level `+` has this problem).
|
|
317
|
+
def resolver_numeric_leaves(depth)
|
|
318
|
+
return leaves(:numeric) if depth <= 0
|
|
319
|
+
|
|
320
|
+
# SELF-referential on purpose, one depth down — `Modulo`'s own
|
|
321
|
+
# receiver needs the SAME restriction `resolver_numeric_leaves`
|
|
322
|
+
# exists to express in the first place (see its own header
|
|
323
|
+
# comment); the argument stays the full, unrestricted set, same
|
|
324
|
+
# as `numeric_productions`' identical split right above.
|
|
325
|
+
leaves(:numeric) +
|
|
326
|
+
cross(resolver_numeric_leaves(depth - 1), bounded(:numeric, depth - 1)).map { |a, b| "#{a}.modulo(#{b})" } +
|
|
327
|
+
bounded(:string, depth).map { |s| "#{s}.size" } +
|
|
328
|
+
bounded(:array, depth).map { |a| "#{a}.size" } +
|
|
329
|
+
sample(numeric_array_productions(depth)).flat_map { |a| ["#{a}.first", "#{a}.last"] }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# STRING ← ToS(numeric | boolean | nil | string) | First/Last(string
|
|
333
|
+
# array). `Split` produces an ARRAY, never a String (resolver.rb's
|
|
334
|
+
# own `apply_split`) — deliberately NOT listed as a string producer
|
|
335
|
+
# here; that would be exactly the "vocabulary-legal but grammar-
|
|
336
|
+
# can't-actually-produce-it" mistake this sublanguage's own
|
|
337
|
+
# `ArrayLiteral` bug (§1 of this generator's own design report) was
|
|
338
|
+
# found from, inverted.
|
|
339
|
+
def string_productions(depth)
|
|
340
|
+
sample(resolver_numeric_leaves(depth - 1)).map { |n| "#{n}.to_s" } +
|
|
341
|
+
sample(resolver_boolean_leaves(depth - 1)).map { |b| "#{b}.to_s" } +
|
|
342
|
+
bounded(:nil_type, depth - 1).map { |n| "#{n}.to_s" } +
|
|
343
|
+
bounded(:string, depth - 1).map { |s| "#{s}.to_s" } +
|
|
344
|
+
sample(string_array_productions(depth - 1)).flat_map { |a| ["#{a}.first", "#{a}.last"] }
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# ARRAY ← Split(string, separator) | ArrayLiteral[same-type
|
|
348
|
+
# elements] | the synthetic array attributes (already in `leaves`).
|
|
349
|
+
# Kept deliberately small relative to numeric/string/boolean —
|
|
350
|
+
# `array` is overwhelmingly a RECEIVER type in this grammar (`.size`
|
|
351
|
+
# /`.any?`/`.include?`/…), rarely a produced VALUE; the two real
|
|
352
|
+
# producers are enough to exercise every array-typed consumer
|
|
353
|
+
# elsewhere in this file at least once via a non-leaf path.
|
|
354
|
+
def array_productions(depth)
|
|
355
|
+
return [] if depth <= 0
|
|
356
|
+
|
|
357
|
+
bounded(:string, depth - 1).map { |s| "#{s}.split(\",\")" } +
|
|
358
|
+
[numeric_array_literal(depth - 1), string_array_literal(depth - 1)]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def numeric_array_literal(depth) = "[#{bounded(:numeric, depth).first(2).join(', ')}]"
|
|
362
|
+
def string_array_literal(depth) = "[#{bounded(:string, depth).first(2).join(', ')}]"
|
|
363
|
+
|
|
364
|
+
# Arrays KNOWN (by construction, not merely by type) to hold numeric
|
|
365
|
+
# elements — the two synthetic array attributes (`ARRAY_ELEMENT_TYPE`
|
|
366
|
+
# tags `arr_num`) plus any numeric-array LITERAL this same depth
|
|
367
|
+
# budget can build. `.first`/`.last`/block predicates need to know
|
|
368
|
+
# the ELEMENT type, which plain `array_productions` doesn't carry —
|
|
369
|
+
# this (and `string_array_productions`, its twin) is how that extra
|
|
370
|
+
# bit of type information flows without inventing a second, richer
|
|
371
|
+
# AST just to carry it.
|
|
372
|
+
def numeric_array_productions(depth)
|
|
373
|
+
["arr_num", numeric_array_literal(depth)]
|
|
374
|
+
# a Split of a string never yields numeric elements — no third
|
|
375
|
+
# entry here, deliberately, not omitted by oversight (see
|
|
376
|
+
# `string_array_productions`, its non-empty twin, right below).
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def string_array_productions(depth)
|
|
380
|
+
["arr_str", string_array_literal(depth)] + bounded(:string, depth).map { |s| "#{s}.split(\",\")" }
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# BOOLEAN ← every comparison/predicate construct in the grammar.
|
|
384
|
+
# This is where almost all of the sublanguage's OWN real surface
|
|
385
|
+
# lives — a `given`/`invariant`/`ensures` body is ALWAYS boolean-
|
|
386
|
+
# typed at its own top level (`Evaluator.truthy?`), so this is also
|
|
387
|
+
# the set `all_predicates` (below) draws its top-level cases from
|
|
388
|
+
# directly.
|
|
389
|
+
def boolean_productions(depth)
|
|
390
|
+
sub = bounded(:boolean, depth - 1)
|
|
391
|
+
num = bounded(:numeric, depth - 1)
|
|
392
|
+
str = bounded(:string, depth - 1)
|
|
393
|
+
|
|
394
|
+
pairs(num).map { |a, b| "#{a} == #{b}" } +
|
|
395
|
+
pairs(num).map { |a, b| "#{a} > #{b}" } +
|
|
396
|
+
pairs(str).map { |a, b| "#{a} == #{b}" } +
|
|
397
|
+
pairs(str).map { |a, b| "#{a} < #{b}" } +
|
|
398
|
+
# `resolver_numeric_leaves`, not the raw `num` `==`/`>` above
|
|
399
|
+
# safely use — SignTest's own suffix match
|
|
400
|
+
# (`match_suffix`/`.positive?` et al.) is a plain trailing-
|
|
401
|
+
# string strip, not `Evaluator`'s comparison-scanning, so an
|
|
402
|
+
# `Addition` in `num` would hit the exact "`0 + 0.to_s`" mis-
|
|
403
|
+
# split this generator's own `resolver_numeric_leaves` comment
|
|
404
|
+
# documents (`"0 + 0.positive?"` would parse as `0 + (0
|
|
405
|
+
# .positive?)`, not `(0 + 0).positive?`).
|
|
406
|
+
resolver_numeric_leaves(depth - 1).flat_map { |n| ["#{n}.positive?", "#{n}.negative?", "#{n}.zero?"] } +
|
|
407
|
+
(str + sample(numeric_array_productions(depth - 1)) + sample(string_array_productions(depth - 1)) + bounded(:array, depth - 1))
|
|
408
|
+
.map { |x| "#{x}.empty?" } +
|
|
409
|
+
str.map { |s| "#{s}.match?(/a/)" } +
|
|
410
|
+
str.flat_map { |s| ["#{s}.present?", "#{s}.blank?"] } +
|
|
411
|
+
str.flat_map { |s| ["#{s}.start_with?(\"a\")", "#{s}.end_with?(\"a\")"] } +
|
|
412
|
+
pairs(sub).flat_map { |a, b| ["#{a} && #{b}", "#{a} || #{b}"] } +
|
|
413
|
+
sub.map { |b| "!#{b}" } +
|
|
414
|
+
block_predicate_productions(depth - 1) +
|
|
415
|
+
include_productions(depth - 1)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# `.all?`/`.any?`/`.none?` over EACH known-element-typed array
|
|
419
|
+
# source, with a predicate body drawn from `boolean_productions` at
|
|
420
|
+
# ONE LESS depth, evaluated against `BLOCK_PARAM` bound to the
|
|
421
|
+
# array's own element type — this is the ONLY place `Resolver` and
|
|
422
|
+
# `Evaluator` are truly mutually recursive (a `BlockPredicate`'s own
|
|
423
|
+
# `predicate` field is a full `Evaluator` AST, not a `Resolver`
|
|
424
|
+
# leaf — confirmed directly), and the only construct in this whole
|
|
425
|
+
# generator that can nest into ANOTHER block predicate (the real
|
|
426
|
+
# corpus already does this two levels deep —
|
|
427
|
+
# `examples/roster/bluebook/roster.bluebook`'s own `seats.any? { |s|
|
|
428
|
+
# assignments.none? { |a| … } }`).
|
|
429
|
+
def block_predicate_productions(depth)
|
|
430
|
+
return [] if depth.negative?
|
|
431
|
+
|
|
432
|
+
[
|
|
433
|
+
["arr_num", :numeric], [numeric_array_literal(depth), :numeric],
|
|
434
|
+
["arr_str", :string], [string_array_literal(depth), :string]
|
|
435
|
+
].flat_map do |array_text, element_type|
|
|
436
|
+
predicate_bodies(element_type, depth).flat_map do |body|
|
|
437
|
+
%w[all? any? none?].map { |mode| "#{array_text}.#{mode} { |#{BLOCK_PARAM}| #{body} }" }
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# The predicate body a block gets — `boolean_productions`, but with
|
|
443
|
+
# `BLOCK_PARAM` (bound to `element_type`) ALSO admitted as a leaf,
|
|
444
|
+
# since inside the block it is exactly as usable as any other
|
|
445
|
+
# `Lookup` name (resolver.rb's own `interpret_with_element`: the
|
|
446
|
+
# bound element joins `attrs` for the span of one evaluation, no
|
|
447
|
+
# different from a top-level attribute — confirmed directly).
|
|
448
|
+
# `sample`d for the same reason every other internal list is — this
|
|
449
|
+
# feeds THREE more constructs per body (`all?`/`any?`/`none?`) times
|
|
450
|
+
# FOUR array sources, so an unbounded body list here is exactly the
|
|
451
|
+
# kind of multiplier this file's own `bounded` comment warns about.
|
|
452
|
+
def predicate_bodies(element_type, depth)
|
|
453
|
+
sample(with_element_leaf(element_type) { boolean_productions(depth) })
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# `haystack.include?(needle)` — String haystack needs a String
|
|
457
|
+
# needle (raises otherwise, evaluator.rb's own `includes?`); Array
|
|
458
|
+
# haystack admits ANY needle type (compared via `equal?`, itself
|
|
459
|
+
# numeric-coerced-first). Both sides generated here, matching
|
|
460
|
+
# `Vocabulary::IncludeHaystack` exactly.
|
|
461
|
+
def include_productions(depth)
|
|
462
|
+
str = bounded(:string, depth)
|
|
463
|
+
pairs(str).map { |haystack, needle| "#{haystack}.include?(#{needle})" } +
|
|
464
|
+
sample(numeric_array_productions(depth)).product(bounded(:numeric, depth)).map { |arr, needle| "#{arr}.include?(#{needle})" } +
|
|
465
|
+
sample(string_array_productions(depth)).product(bounded(:string, depth)).map { |arr, needle| "#{arr}.include?(#{needle})" }
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# SAMPLED, NOT A FULL CROSS PRODUCT — a full `list.product(list)`
|
|
469
|
+
# is what actually explodes this generator (numeric productions
|
|
470
|
+
# alone hit 8000+ by depth 2; squaring THAT for `&&`/`==` pairs is
|
|
471
|
+
# where "tens of thousands" becomes tens of millions). The
|
|
472
|
+
# combinatorics genuinely don't buy proof coverage: proving `Or`/
|
|
473
|
+
# `And`/`Not` themselves never crash needs ONE representative pair
|
|
474
|
+
# per depth (`evaluator.rb`'s own `interpret` does zero type-
|
|
475
|
+
# dependent work for those three — `interpret(left) || interpret
|
|
476
|
+
# (right)`, plain Ruby, no coercion, no receiver-type check at
|
|
477
|
+
# all — so a crash there could only come from LEFT or RIGHT
|
|
478
|
+
# themselves, already covered by testing every operand on its own
|
|
479
|
+
# elsewhere in this same predicate set). `Compare`/`Include`/
|
|
480
|
+
# `Addition`/`Modulo` genuinely DO real per-pair type coercion
|
|
481
|
+
# (`Evaluator.apply`/`less_than`/`Resolver.add`/`apply_modulo`), so
|
|
482
|
+
# THOSE stay covered across every construct SHAPE at every depth —
|
|
483
|
+
# just sampled evenly across each side's own operand set (leaves
|
|
484
|
+
# AND deep productions alike, not just whichever the list happens
|
|
485
|
+
# to enumerate first) rather than every possible pairing of them.
|
|
486
|
+
SAMPLE_CAP = 14
|
|
487
|
+
|
|
488
|
+
def sample(list) = list.size <= SAMPLE_CAP ? list : list.each_slice(list.size.fdiv(SAMPLE_CAP).ceil).map(&:first)
|
|
489
|
+
|
|
490
|
+
def pairs(list)
|
|
491
|
+
sampled = sample(list)
|
|
492
|
+
sampled.product(sampled)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# `pairs`' own two-different-lists twin — used wherever the LEFT
|
|
496
|
+
# and RIGHT of a construct have genuinely different safety
|
|
497
|
+
# requirements (`Modulo`'s own receiver vs. argument, below) and
|
|
498
|
+
# squaring the SAME sampled list wouldn't be correct.
|
|
499
|
+
def cross(left, right) = sample(left).product(sample(right))
|
|
500
|
+
|
|
501
|
+
# THE FULL SET — every boolean-typed expression up to `MAX_DEPTH`,
|
|
502
|
+
# deduplicated (many shorter expressions are also produced, re-
|
|
503
|
+
# wrapped, at every deeper level — `.uniq` inside `productions`
|
|
504
|
+
# already collapses most of that; this is the final pass over the
|
|
505
|
+
# complete depth-`MAX_DEPTH` set specifically).
|
|
506
|
+
def all_predicates(max_depth = MAX_DEPTH)
|
|
507
|
+
productions(:boolean, max_depth).uniq
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Interprets one predicate against the shared synthetic
|
|
511
|
+
# state/attrs, returning `{ok: true, result: ...}` on any outcome
|
|
512
|
+
# `Evaluator.call` itself can express (a real true/false answer, OR
|
|
513
|
+
# a clean `EvaluationError` — both are the sublanguage WORKING
|
|
514
|
+
# correctly, never a finding) and `{ok: false, error: ...}` only
|
|
515
|
+
# for anything else escaping — the one shape this whole file exists
|
|
516
|
+
# to prove never happens for well-typed input.
|
|
517
|
+
def check(expr)
|
|
518
|
+
result = Hecks::Bluebook::Expression::Evaluator.call(expr, synthetic_state, synthetic_attrs)
|
|
519
|
+
{ ok: true, result: result }
|
|
520
|
+
rescue Hecks::Bluebook::Expression::EvaluationError => e
|
|
521
|
+
{ ok: true, result: :refused, message: e.message }
|
|
522
|
+
rescue StandardError => e
|
|
523
|
+
{ ok: false, error: e }
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|