hecks 2.6.0 → 2.8.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/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -1,684 +0,0 @@
|
|
|
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 generation
|
|
24
|
-
#
|
|
25
|
-
# Type-directed, not exhaustive-over-strings: this generator only ever
|
|
26
|
-
# recurses into a sub-expression whose own type the surrounding
|
|
27
|
-
# construct actually accepts (never `"x".modulo(true)`) — mirroring,
|
|
28
|
-
# by hand, the same admitted-receiver-class tables `Resolver`'s own
|
|
29
|
-
# `interpret` enforces at runtime (`SizedType`/`ToStringType`/
|
|
30
|
-
# `IncludeHaystack` from vocabulary.bluebook, plus the ad hoc
|
|
31
|
-
# `is_a?`/`respond_to?` checks `numeric`/`size_of`/`string_of`/etc.
|
|
32
|
-
# apply that vocabulary.bluebook itself doesn't declare). Restricted
|
|
33
|
-
# to a small representative literal palette (TYPE_LEAVES, below) and a
|
|
34
|
-
# small synthetic attribute set per type (SYNTHETIC_ATTRS), bounded to
|
|
35
|
-
# MAX_DEPTH — that combination is what keeps this tractable (thousands
|
|
36
|
-
# to tens of thousands of cases, not millions) while still being a
|
|
37
|
-
# real, defensible "proven for every well-typed expression up to depth
|
|
38
|
-
# 3" claim for exactly the sublanguage that already bit this project
|
|
39
|
-
# twice.
|
|
40
|
-
#
|
|
41
|
-
# ## What "PROVEN" means here, precisely
|
|
42
|
-
#
|
|
43
|
-
# Every expression this generator
|
|
44
|
-
# produces is well-typed by construction (every sub-expression's type
|
|
45
|
-
# matches what its parent construct actually accepts, per this file's
|
|
46
|
-
# own TYPE_RULES). Interpreting a well-typed expression must never
|
|
47
|
-
# raise anything other than `Hecks::Bluebook::Expression::
|
|
48
|
-
# EvaluationError` — that class alone is the sublanguage's own real
|
|
49
|
-
# refusal vocabulary (a divisor that happens to be zero, a `Lookup`
|
|
50
|
-
# this run's synthetic state doesn't happen to carry); anything else
|
|
51
|
-
# escaping (a raw `TypeError`/`NoMethodError`/`ZeroDivisionError`) is
|
|
52
|
-
# exactly the crash-signature this whole file's own header comments
|
|
53
|
-
# (resolver.rb, block_predicates.rb) name as the historical bug class
|
|
54
|
-
# this sublanguage keeps almost-but-not-quite avoiding. This does not
|
|
55
|
-
# prove the interpreter computes the semantically right answer (that
|
|
56
|
-
# needs a second, independent oracle — out of scope here, exactly the
|
|
57
|
-
# way rust_conformance_fuzz_spec.rb is the cross-runtime half of this
|
|
58
|
-
# same idea) — only that it never crashes on well-typed input.
|
|
59
|
-
module BoundedExhaustiveExpressions
|
|
60
|
-
module_function
|
|
61
|
-
|
|
62
|
-
# `:numeric` deliberately covers both Integer and Float — the
|
|
63
|
-
# resolver's own `numeric`/`require_number` never distinguish them
|
|
64
|
-
# (resolver.rb) — so a generator that kept them as separate types
|
|
65
|
-
# would be modeling a distinction the grammar itself doesn't make.
|
|
66
|
-
TYPES = %i[numeric string boolean array nil_type].freeze
|
|
67
|
-
|
|
68
|
-
# **The palette** — deliberately small (this file's own header: "a small
|
|
69
|
-
# representative literal palette," matching the plan's own `{0, 1,
|
|
70
|
-
# -1}` example). Widening it doesn't test a different shape of
|
|
71
|
-
# expression, only more values through the same shapes — real
|
|
72
|
-
# boundary-value coverage (Bignum, NaN, empty string, unicode) is
|
|
73
|
-
# PRD 05's job (spec/runtime/numeric_boundary_spec.rb,
|
|
74
|
-
# ValueGenerator's own edge-case tables), not this generator's.
|
|
75
|
-
TYPE_LEAVES = {
|
|
76
|
-
numeric: ["0", "1", "-1"].freeze,
|
|
77
|
-
string: ['""', '"a"'].freeze,
|
|
78
|
-
boolean: %w[true false].freeze,
|
|
79
|
-
nil_type: %w[nil].freeze
|
|
80
|
-
}.freeze
|
|
81
|
-
|
|
82
|
-
# Two synthetic attributes per type (the plan's own "2-3 fake
|
|
83
|
-
# attributes per type") — real names are irrelevant to `Resolver
|
|
84
|
-
# .parse` (it never touches state/attrs at all, confirmed directly:
|
|
85
|
-
# only `Lookup#interpret` does) and irrelevant to `interpret` beyond
|
|
86
|
-
# needing to resolve against whatever synthetic state this file
|
|
87
|
-
# supplies (`synthetic_state`, below) — so any non-colliding,
|
|
88
|
-
# non-suffix-shaped name works. Never named the same as a
|
|
89
|
-
# `BLOCK_PARAM` (below) — `Resolver#fetch`'s own `attrs`-wins-over-
|
|
90
|
-
# `state` precedence means a block parameter shadows a same-named
|
|
91
|
-
# top-level attribute for the span of its own predicate, a real
|
|
92
|
-
# hazard this file sidesteps by construction rather than exercising
|
|
93
|
-
# it here (a real, cited resolver.rb hazard, not a gap in this
|
|
94
|
-
# generator's own coverage claim).
|
|
95
|
-
SYNTHETIC_ATTRS = {
|
|
96
|
-
numeric: %w[num_a num_b].freeze,
|
|
97
|
-
string: %w[str_a str_b].freeze,
|
|
98
|
-
boolean: %w[bool_a bool_b].freeze,
|
|
99
|
-
array: %w[arr_num arr_str].freeze
|
|
100
|
-
}.freeze
|
|
101
|
-
|
|
102
|
-
# `arr_num`/`arr_str` — element type per array attribute, needed so
|
|
103
|
-
# a block predicate's own bound parameter (`BLOCK_PARAM`, below) is
|
|
104
|
-
# generated against the right leaf/production set for whatever it's
|
|
105
|
-
# actually bound to.
|
|
106
|
-
ARRAY_ELEMENT_TYPE = { "arr_num" => :numeric, "arr_str" => :string }.freeze
|
|
107
|
-
|
|
108
|
-
BLOCK_PARAM = "el".freeze
|
|
109
|
-
|
|
110
|
-
MAX_DEPTH = 3
|
|
111
|
-
|
|
112
|
-
# `state`/`attrs` — a plain Hash satisfies both (confirmed directly:
|
|
113
|
-
# `Resolver#known?` degrades to a bare index check when `state`
|
|
114
|
-
# doesn't `respond_to?(:key?)`, and `fetch` only ever needs
|
|
115
|
-
# `attrs.key?`/`attrs[]`.). Split across both, matching a real
|
|
116
|
-
# dispatch's own shape (some names come from the record's stored
|
|
117
|
-
# state, some from the command's own args) — not load-bearing for
|
|
118
|
-
# this proof (attrs wins regardless), but keeps the synthetic input
|
|
119
|
-
# closer to what `Admissibility` actually builds, in case anything
|
|
120
|
-
# here is reused for a future deeper check.
|
|
121
|
-
#
|
|
122
|
-
# **The VO-vs-scalar unwrap case, deliberately exercised** — half of
|
|
123
|
-
# `num_a`/`str_a`/`bool_a`/one array attribute's elements are bare
|
|
124
|
-
# scalars, the other half (`num_b`/`str_b`/`bool_b`/the other
|
|
125
|
-
# array's elements) are wrapped `{value: X}` — the single-field
|
|
126
|
-
# Value Object shape `Resolver#unwrap_scalar` auto-collapses.
|
|
127
|
-
# resolver.rb's own most serious historical bug in this file (the
|
|
128
|
-
# "UPDATE 2026-08-18" comment: a dotted walk that landed on an
|
|
129
|
-
# un-unwrapped VO compared `false` against every literal, silently,
|
|
130
|
-
# never raising) lived exactly at this boundary — a generator that
|
|
131
|
-
# only ever supplied bare scalars would never exercise the code
|
|
132
|
-
# path that bug lived in at all.
|
|
133
|
-
# `SingleFieldVO` — not a plain Hash, deliberately: `Resolver
|
|
134
|
-
# #unwrap_scalar`'s own guard is `value.respond_to?(:to_h) &&
|
|
135
|
-
# !value.is_a?(Hash) && !value.is_a?(Array)` — it exists
|
|
136
|
-
# specifically to collapse a real hydrated `Runtime::Value`
|
|
137
|
-
# instance (which responds to `#to_h` but is never itself a bare
|
|
138
|
-
# Hash) down to its lone scalar field, and just as deliberately
|
|
139
|
-
# leaves an actual Hash alone (a genuinely un-hydrated, multi-
|
|
140
|
-
# field record has no single scalar to collapse to). A plain
|
|
141
|
-
# `{value: X}` Hash cannot stand in here: `is_a?(Hash)` is true for
|
|
142
|
-
# it, so `unwrap_scalar` leaves it wrapped, and every VO-typed
|
|
143
|
-
# synthetic attribute then fails `Addition`/`Compare`/every
|
|
144
|
-
# scalar-typed operation with "expects a number, got
|
|
145
|
-
# {\"value\":5}" — a bug in this generator's own synthetic state,
|
|
146
|
-
# not in `Resolver`, that would surface only as `num_b == 5`
|
|
147
|
-
# (`num_b` a plain `{value: 5}` Hash) evaluating to `false` instead
|
|
148
|
-
# of `true`.
|
|
149
|
-
SingleFieldVO = Struct.new(:value) do
|
|
150
|
-
def to_h = { value: value }
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Builds the fixed state every generated predicate is interpreted against.
|
|
154
|
-
#
|
|
155
|
-
# @return [Hash{Symbol => Object}] symbol-keyed synthetic state: half bare
|
|
156
|
-
# scalars (`num_a`, `str_a`, `bool_a`, half of `arr_num`'s/`arr_str`'s
|
|
157
|
-
# elements), half wrapped `SingleFieldVO` instances (`num_b`, `str_b`,
|
|
158
|
-
# `bool_b`, the other half)
|
|
159
|
-
def synthetic_state
|
|
160
|
-
{
|
|
161
|
-
num_a: 3,
|
|
162
|
-
num_b: SingleFieldVO.new(5),
|
|
163
|
-
str_a: "hello",
|
|
164
|
-
str_b: SingleFieldVO.new("world"),
|
|
165
|
-
bool_a: true,
|
|
166
|
-
bool_b: SingleFieldVO.new(false),
|
|
167
|
-
arr_num: [1, SingleFieldVO.new(2), 3],
|
|
168
|
-
arr_str: ["x", SingleFieldVO.new("y"), "z"]
|
|
169
|
-
}
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# The `attrs` half of the interpret call — always empty, since every
|
|
173
|
-
# synthetic name this generator uses lives in `synthetic_state` instead.
|
|
174
|
-
#
|
|
175
|
-
# @return [Hash] always `{}`
|
|
176
|
-
def synthetic_attrs = {}
|
|
177
|
-
|
|
178
|
-
# Lists every terminal (non-recursive) expression of `type`: the fixed
|
|
179
|
-
# literal palette plus every synthetic attribute name declared for that
|
|
180
|
-
# type (never their values — this generates text, the same source a
|
|
181
|
-
# real `given`/`invariant` author would write; `synthetic_state`,
|
|
182
|
-
# above, is what gives those names meaning at `interpret` time).
|
|
183
|
-
#
|
|
184
|
-
# @param type [Symbol] one of `TYPES`
|
|
185
|
-
# @return [Array<String>] `type`'s literal palette, synthetic attribute
|
|
186
|
-
# names, and any currently-bound block-parameter leaf for `type`
|
|
187
|
-
def leaves(type)
|
|
188
|
-
(TYPE_LEAVES[type] || []) + (SYNTHETIC_ATTRS[type] || []) + Array(bound_leaves[type])
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# The per-type stack of currently-bound block-parameter leaf names.
|
|
192
|
-
#
|
|
193
|
-
# A stack, not a single slot — a nested block predicate (the real
|
|
194
|
-
# corpus already does this two levels deep, roster.bluebook's own
|
|
195
|
-
# `seats.any? { |s| assignments.none? { |a| … } }`) pushes a second
|
|
196
|
-
# bound leaf while the outer one is still active. Both share the
|
|
197
|
-
# same `BLOCK_PARAM` spelling ("el") — real Ruby block-parameter
|
|
198
|
-
# shadowing (the inner `el` simply shadows the outer one within its
|
|
199
|
-
# own predicate text), which this generator treats as legal on
|
|
200
|
-
# purpose: `resolver.rb`'s own `interpret_with_element` binds fresh
|
|
201
|
-
# `attrs.merge(param => element)` per level regardless of what the
|
|
202
|
-
# outer level already bound, so a shadowed name still interprets
|
|
203
|
-
# correctly — this generator is proving "does it crash," not "is
|
|
204
|
-
# every generated predicate semantically distinct."
|
|
205
|
-
#
|
|
206
|
-
# @return [Hash{Symbol => Array<String>}] each type's own stack of bound
|
|
207
|
-
# leaf names, usually `[]` or `["el"]`
|
|
208
|
-
def bound_leaves = @bound_leaves ||= Hash.new { |h, k| h[k] = [] }
|
|
209
|
-
|
|
210
|
-
# Runs the block with `BLOCK_PARAM` admitted as a leaf of `element_type`,
|
|
211
|
-
# for building a block predicate's own body.
|
|
212
|
-
#
|
|
213
|
-
# @param element_type [Symbol] the type `BLOCK_PARAM` is bound to for the
|
|
214
|
-
# duration of the block
|
|
215
|
-
# @yield runs with the leaf pushed and any cached `element_type` productions
|
|
216
|
-
# invalidated
|
|
217
|
-
# @yieldreturn [Object] the block's own result
|
|
218
|
-
# @return [Object] the block's own result, unchanged
|
|
219
|
-
def with_element_leaf(element_type)
|
|
220
|
-
bound_leaves[element_type] << BLOCK_PARAM
|
|
221
|
-
cache.delete_if { |(type, _depth), _| type == element_type }
|
|
222
|
-
yield
|
|
223
|
-
ensure
|
|
224
|
-
bound_leaves[element_type].pop
|
|
225
|
-
cache.delete_if { |(type, _depth), _| type == element_type }
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Lists every expression of `type` reachable in at most `depth`
|
|
229
|
-
# recursive steps, memoized (the same sub-expression set is reused at
|
|
230
|
-
# every enclosing recursion, so without memoizing, cost would compound
|
|
231
|
-
# multiplicatively per level instead of additively). `depth` 0 is
|
|
232
|
-
# exactly `leaves(type)`; each increment adds every construct this
|
|
233
|
-
# file's own production methods (below) say can produce `type`, built
|
|
234
|
-
# from `depth - 1` sub-expressions.
|
|
235
|
-
#
|
|
236
|
-
# @param type [Symbol] one of `TYPES`
|
|
237
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
238
|
-
# @return [Array<String>] every expression of `type` reachable in at most
|
|
239
|
-
# `depth` steps, deduplicated
|
|
240
|
-
def productions(type, depth)
|
|
241
|
-
cache[[type, depth]] ||= begin
|
|
242
|
-
base = leaves(type)
|
|
243
|
-
depth <= 0 ? base : (base + recursive_productions(type, depth)).uniq
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# The `productions` memo, keyed by `[type, depth]`.
|
|
248
|
-
#
|
|
249
|
-
# @return [Hash{Array(Symbol, Integer) => Array<String>}] the memoized productions cache
|
|
250
|
-
def cache = @cache ||= {}
|
|
251
|
-
|
|
252
|
-
# Dispatches to the recursive (non-leaf) production rule for `type`.
|
|
253
|
-
#
|
|
254
|
-
# @param type [Symbol] one of `TYPES`
|
|
255
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
256
|
-
# @return [Array<String>] every non-leaf expression of `type` reachable in at
|
|
257
|
-
# most `depth` steps; `[]` for `:nil_type`, which has no recursive producer
|
|
258
|
-
# @raise [ArgumentError] if `type` names anything outside `TYPES`
|
|
259
|
-
def recursive_productions(type, depth)
|
|
260
|
-
case type
|
|
261
|
-
when :numeric then numeric_productions(depth)
|
|
262
|
-
when :string then string_productions(depth)
|
|
263
|
-
when :boolean then boolean_productions(depth)
|
|
264
|
-
when :array then array_productions(depth)
|
|
265
|
-
# no recursive producer of nil in this grammar — Find's "not found" is a
|
|
266
|
-
# runtime outcome, not a distinct construct to render as source text
|
|
267
|
-
when :nil_type then []
|
|
268
|
-
else raise ArgumentError, "no production rule for type #{type.inspect}"
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Every internal use of a sub-expression list (as opposed to the
|
|
273
|
-
# single final list `productions(type, depth)` returns to its own
|
|
274
|
-
# caller) goes through this, not `productions` directly — the
|
|
275
|
-
# actual thing that made an early version of this generator explode
|
|
276
|
-
# past a million cases by depth 3 wasn't `pairs`' own cross product
|
|
277
|
-
# (already sampled) but the dozen-plus linear `flat_map`/`map`
|
|
278
|
-
# passes `boolean_productions` alone makes over `str`/`num`/`sub` —
|
|
279
|
-
# each individually harmless, but an unbounded few-thousand-item
|
|
280
|
-
# list run through a dozen of them, feeding the next depth's own
|
|
281
|
-
# dozen passes, compounds fast. Bounding every input list (not the
|
|
282
|
-
# final output) keeps the shape diversity `sample`'s even-spacing
|
|
283
|
-
# already preserves while keeping growth roughly linear in depth
|
|
284
|
-
# instead of combinatorial.
|
|
285
|
-
#
|
|
286
|
-
# @param type [Symbol] one of `TYPES`
|
|
287
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
288
|
-
# @return [Array<String>] `productions(type, depth)`, sampled down to at most
|
|
289
|
-
# `SAMPLE_CAP` entries
|
|
290
|
-
def bounded(type, depth) = sample(productions(type, depth))
|
|
291
|
-
|
|
292
|
-
# NUMERIC ← Addition(numeric, numeric) | Modulo(numeric, numeric) |
|
|
293
|
-
# Size(sized) | First/Last(numeric array). `Size` returns an
|
|
294
|
-
# Integer for a String or an Array receiver alike (`SizedType` —
|
|
295
|
-
# `size_of`, resolver.rb) — both sides generated here.
|
|
296
|
-
#
|
|
297
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
298
|
-
# @return [Array<String>] numeric expressions one construct deep: `Addition`,
|
|
299
|
-
# `Modulo`, `Size` (of a string or array), `First`/`Last` (of a numeric array)
|
|
300
|
-
def numeric_productions(depth)
|
|
301
|
-
sub = bounded(:numeric, depth - 1)
|
|
302
|
-
# `Modulo`'s own receiver (not its argument — that side already
|
|
303
|
-
# goes through `Resolver#matching_paren`'s own fresh, self-
|
|
304
|
-
# contained re-parse, confirmed safe for any numeric shape
|
|
305
|
-
# including another `Addition`/`Modulo`) has the identical
|
|
306
|
-
# "`Addition` mis-parsed as a suffix receiver" hazard
|
|
307
|
-
# `resolver_numeric_leaves`'s own comment documents for `.to_s`/
|
|
308
|
-
# `.positive?` — `Resolver.parse` tries `split_addition` before
|
|
309
|
-
# `match_call`, so `"0 + 0.modulo(1)"` (meant as `(0 + 0)
|
|
310
|
-
# .modulo(1)`) actually parses as `0 + (0.modulo(1))`. Restricted
|
|
311
|
-
# to `resolver_numeric_leaves` on the receiver side only — the
|
|
312
|
-
# argument stays the full, unrestricted numeric set.
|
|
313
|
-
pairs(sub).map { |a, b| "#{a} + #{b}" } +
|
|
314
|
-
cross(resolver_numeric_leaves(depth - 1), sub).map { |a, b| "#{a}.modulo(#{b})" } +
|
|
315
|
-
bounded(:string, depth - 1).map { |s| "#{s}.size" } +
|
|
316
|
-
bounded(:array, depth - 1).map { |a| "#{a}.size" } +
|
|
317
|
-
sample(numeric_array_productions(depth - 1)).flat_map { |a| ["#{a}.first", "#{a}.last"] }
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
# A "boolean" in resolver's own sense — safe to embed as the
|
|
321
|
-
# receiver of a trailing Resolver-level suffix (`.to_s`, and
|
|
322
|
-
# anywhere else a boolean-typed value, as opposed to a boolean-
|
|
323
|
-
# typed expression, is wanted). Found live, the same way the
|
|
324
|
-
# nested-`.modulo` bug was: `Resolver.parse` has no knowledge of
|
|
325
|
-
# `==`/`<`/`&&`/`||`/leading `!`/`.include?` at all — those are
|
|
326
|
-
# `Evaluator`'s own, entirely separate parsing layer, stripped off
|
|
327
|
-
# before anything reaches `Resolver.parse` at all (confirmed
|
|
328
|
-
# directly: `Resolver.parse("str_b < str_a")` — no `.` anywhere in
|
|
329
|
-
# that text for any suffix regex to anchor on — falls through
|
|
330
|
-
# every leaf regex to the `Lookup` catch-all, exactly like the
|
|
331
|
-
# nested-modulo bug did). `bounded(:boolean, depth)`'s full set
|
|
332
|
-
# includes `Compare`/`Include`/`Or`/`And`/`Not` — genuinely boolean-
|
|
333
|
-
# typed at `interpret` time, but `Evaluator`-level syntax, not
|
|
334
|
-
# something `Resolver.parse` can ever recognize as a receiver no
|
|
335
|
-
# matter how it's parenthesized (confirmed directly too: `Resolver
|
|
336
|
-
# .parse` never strips parens at all — `"(3)"` alone already fails
|
|
337
|
-
# to resolve). This is a real, permanent boundary of the actual
|
|
338
|
-
# grammar (this whole sublanguage's own two-layer split, not a
|
|
339
|
-
# limitation to work around) — a `given`/`invariant` author simply
|
|
340
|
-
# cannot write `(a < b).to_s` in this language, ever, no matter how
|
|
341
|
-
# they punctuate it. So this generator doesn't either: only
|
|
342
|
-
# resolver-level boolean-producing constructs (bare literals/
|
|
343
|
-
# lookups, `SignTest`, `Empty`, `Presence`, `MatchesRegex`,
|
|
344
|
-
# `StartsWith`/`EndsWith`, `BlockPredicate` — every one of them
|
|
345
|
-
# parsed via a suffix regex inside `Resolver.parse` itself, per
|
|
346
|
-
# this generator's own design report) are eligible here.
|
|
347
|
-
#
|
|
348
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
349
|
-
# @return [Array<String>] boolean-typed expressions safe to embed as a
|
|
350
|
-
# Resolver-level suffix receiver: literals/lookups, sign tests, string
|
|
351
|
-
# predicates, `.empty?`, and sampled block predicates
|
|
352
|
-
def resolver_boolean_leaves(depth)
|
|
353
|
-
leaves(:boolean) +
|
|
354
|
-
resolver_numeric_leaves(depth).flat_map { |n| ["#{n}.positive?", "#{n}.negative?", "#{n}.zero?"] } +
|
|
355
|
-
bounded(:string, depth).flat_map do |s|
|
|
356
|
-
["#{s}.present?", "#{s}.blank?", "#{s}.match?(/a/)", "#{s}.start_with?(\"a\")", "#{s}.end_with?(\"a\")"]
|
|
357
|
-
end +
|
|
358
|
-
(sample(numeric_array_productions(depth)) + sample(string_array_productions(depth)) + bounded(:array, depth)).map do |x|
|
|
359
|
-
"#{x}.empty?"
|
|
360
|
-
end +
|
|
361
|
-
sample(block_predicate_productions(depth))
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
# A "numeric" in resolver's own sense — `Addition`'s twin of
|
|
365
|
-
# `resolver_boolean_leaves`'s own restriction, found the identical
|
|
366
|
-
# way: `"0 + 0.to_s"` (meant as `(0 + 0).to_s`) actually parses as
|
|
367
|
-
# `0 + (0.to_s)`, because `Resolver.parse` tries `split_addition`
|
|
368
|
-
# before `.to_s`'s own suffix regex in its dispatch order — the `+`
|
|
369
|
-
# "wins" the split before the suffix ever gets a chance to anchor
|
|
370
|
-
# on its own receiver boundary. Confirmed to have zero real-corpus
|
|
371
|
-
# precedent either (`grep`, no `bluebook` file anywhere chains a
|
|
372
|
-
# method or sign-test onto a parenthesized addition) — the same
|
|
373
|
-
# verdict as `resolver_boolean_leaves`'s own comparisons/`&&`/`||`:
|
|
374
|
-
# a real permanent grammar boundary (`Resolver.parse` never strips
|
|
375
|
-
# parens, confirmed directly, so no amount of punctuation rescues
|
|
376
|
-
# `(a + b).to_s`), not a bug to fix in the resolver for a shape
|
|
377
|
-
# nothing has ever needed. `Modulo`/`Size`/`First`/`Last` stay in
|
|
378
|
-
# (each is its own trailing `.method(...)`/`.method` call, so
|
|
379
|
-
# `Resolver.parse`'s greedy `(.+)\.suffix\z` regexes correctly
|
|
380
|
-
# isolate them as a receiver regardless of what precedes them —
|
|
381
|
-
# only bare top-level `+` has this problem).
|
|
382
|
-
#
|
|
383
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
384
|
-
# @return [Array<String>] numeric-typed expressions safe to embed as a
|
|
385
|
-
# Resolver-level suffix receiver (never a bare top-level `Addition`)
|
|
386
|
-
def resolver_numeric_leaves(depth)
|
|
387
|
-
return leaves(:numeric) if depth <= 0
|
|
388
|
-
|
|
389
|
-
# Self-referential on purpose, one depth down — `Modulo`'s own
|
|
390
|
-
# receiver needs the same restriction `resolver_numeric_leaves`
|
|
391
|
-
# exists to express in the first place (see its own header
|
|
392
|
-
# comment); the argument stays the full, unrestricted set, same
|
|
393
|
-
# as `numeric_productions`' identical split right above.
|
|
394
|
-
leaves(:numeric) +
|
|
395
|
-
cross(resolver_numeric_leaves(depth - 1), bounded(:numeric, depth - 1)).map { |a, b| "#{a}.modulo(#{b})" } +
|
|
396
|
-
bounded(:string, depth).map { |s| "#{s}.size" } +
|
|
397
|
-
bounded(:array, depth).map { |a| "#{a}.size" } +
|
|
398
|
-
sample(numeric_array_productions(depth)).flat_map { |a| ["#{a}.first", "#{a}.last"] }
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
# STRING ← ToS(numeric | boolean | nil | string) | First/Last(string
|
|
402
|
-
# array). `Split` produces an array, never a String (resolver.rb's
|
|
403
|
-
# own `apply_split`) — deliberately not listed as a string producer
|
|
404
|
-
# here; that would be exactly the "vocabulary-legal but grammar-
|
|
405
|
-
# can't-actually-produce-it" mistake this sublanguage's own
|
|
406
|
-
# `ArrayLiteral` bug (§1 of this generator's own design report) was
|
|
407
|
-
# found from, inverted.
|
|
408
|
-
#
|
|
409
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
410
|
-
# @return [Array<String>] string expressions one construct deep: `ToS` of a
|
|
411
|
-
# numeric, boolean, nil, or string operand, or `First`/`Last` of a string array
|
|
412
|
-
def string_productions(depth)
|
|
413
|
-
sample(resolver_numeric_leaves(depth - 1)).map { |n| "#{n}.to_s" } +
|
|
414
|
-
sample(resolver_boolean_leaves(depth - 1)).map { |b| "#{b}.to_s" } +
|
|
415
|
-
bounded(:nil_type, depth - 1).map { |n| "#{n}.to_s" } +
|
|
416
|
-
bounded(:string, depth - 1).map { |s| "#{s}.to_s" } +
|
|
417
|
-
sample(string_array_productions(depth - 1)).flat_map { |a| ["#{a}.first", "#{a}.last"] }
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
# ARRAY ← Split(string, separator) | ArrayLiteral[same-type
|
|
421
|
-
# elements] | the synthetic array attributes (already in `leaves`).
|
|
422
|
-
# Kept deliberately small relative to numeric/string/boolean —
|
|
423
|
-
# `array` is overwhelmingly a receiver type in this grammar (`.size`
|
|
424
|
-
# /`.any?`/`.include?`/…), rarely a produced value; the two real
|
|
425
|
-
# producers are enough to exercise every array-typed consumer
|
|
426
|
-
# elsewhere in this file at least once via a non-leaf path.
|
|
427
|
-
#
|
|
428
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
429
|
-
# @return [Array<String>] array expressions one construct deep: a string
|
|
430
|
-
# `Split`, a numeric array literal, and a string array literal; `[]` at depth 0
|
|
431
|
-
def array_productions(depth)
|
|
432
|
-
return [] if depth <= 0
|
|
433
|
-
|
|
434
|
-
bounded(:string, depth - 1).map { |s| "#{s}.split(\",\")" } +
|
|
435
|
-
[numeric_array_literal(depth - 1), string_array_literal(depth - 1)]
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
# Builds a two-element numeric array literal at `depth`.
|
|
439
|
-
#
|
|
440
|
-
# @param depth [Integer] maximum recursive steps remaining for its own elements
|
|
441
|
-
# @return [String] a `"[a, b]"` numeric array literal source text
|
|
442
|
-
def numeric_array_literal(depth) = "[#{bounded(:numeric, depth).first(2).join(', ')}]"
|
|
443
|
-
|
|
444
|
-
# Builds a two-element string array literal at `depth`.
|
|
445
|
-
#
|
|
446
|
-
# @param depth [Integer] maximum recursive steps remaining for its own elements
|
|
447
|
-
# @return [String] a `"[a, b]"` string array literal source text
|
|
448
|
-
def string_array_literal(depth) = "[#{bounded(:string, depth).first(2).join(', ')}]"
|
|
449
|
-
|
|
450
|
-
# Arrays known (by construction, not merely by type) to hold numeric
|
|
451
|
-
# elements — the two synthetic array attributes (`ARRAY_ELEMENT_TYPE`
|
|
452
|
-
# tags `arr_num`) plus any numeric-array literal this same depth
|
|
453
|
-
# budget can build. `.first`/`.last`/block predicates need to know
|
|
454
|
-
# the element type, which plain `array_productions` doesn't carry —
|
|
455
|
-
# this (and `string_array_productions`, its twin) is how that extra
|
|
456
|
-
# bit of type information flows without inventing a second, richer
|
|
457
|
-
# AST just to carry it.
|
|
458
|
-
#
|
|
459
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
460
|
-
# @return [Array<String>] array expressions known to hold numeric elements:
|
|
461
|
-
# `"arr_num"`, plus a numeric array literal at `depth`
|
|
462
|
-
def numeric_array_productions(depth)
|
|
463
|
-
["arr_num", numeric_array_literal(depth)]
|
|
464
|
-
# a Split of a string never yields numeric elements — no third
|
|
465
|
-
# entry here, deliberately, not omitted by oversight (see
|
|
466
|
-
# `string_array_productions`, its non-empty twin, right below).
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
# Arrays known (by construction) to hold string elements — `arr_str`'s
|
|
470
|
-
# own twin of `numeric_array_productions`, above.
|
|
471
|
-
#
|
|
472
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
473
|
-
# @return [Array<String>] `"arr_str"`, a string array literal at `depth`, and
|
|
474
|
-
# every string `Split` expression at `depth`
|
|
475
|
-
def string_array_productions(depth)
|
|
476
|
-
["arr_str", string_array_literal(depth)] + bounded(:string, depth).map { |s| "#{s}.split(\",\")" }
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
# BOOLEAN ← every comparison/predicate construct in the grammar.
|
|
480
|
-
# This is where almost all of the sublanguage's own real surface
|
|
481
|
-
# lives — a `given`/`invariant`/`ensures` body is always boolean-
|
|
482
|
-
# typed at its own top level (`Evaluator.truthy?`), so this is also
|
|
483
|
-
# the set `all_predicates` (below) draws its top-level cases from
|
|
484
|
-
# directly.
|
|
485
|
-
# One declarative table of boolean productions, not several
|
|
486
|
-
# unrelated things bolted together — each `+`-joined term is a
|
|
487
|
-
# distinct grammar rule, and several carry their own comment
|
|
488
|
-
# explaining a specific inclusion/exclusion choice (the
|
|
489
|
-
# `resolver_numeric_leaves`-not-`num` note above, `arr_num`'s non-
|
|
490
|
-
# empty twin, etc). Extracting terms into separate methods would
|
|
491
|
-
# need `sub`/`num`/`str` threaded into each as parameters and
|
|
492
|
-
# would separate every rule from the comment justifying it.
|
|
493
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
494
|
-
#
|
|
495
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
496
|
-
# @return [Array<String>] every boolean-typed construct in the grammar, one
|
|
497
|
-
# level deep: comparisons, sign tests, `.empty?`/`.match?`/`.present?`/
|
|
498
|
-
# `.blank?`/`.start_with?`/`.end_with?`, `&&`/`||`/`!`, block predicates,
|
|
499
|
-
# and `include?`
|
|
500
|
-
def boolean_productions(depth)
|
|
501
|
-
sub = bounded(:boolean, depth - 1)
|
|
502
|
-
num = bounded(:numeric, depth - 1)
|
|
503
|
-
str = bounded(:string, depth - 1)
|
|
504
|
-
|
|
505
|
-
pairs(num).map { |a, b| "#{a} == #{b}" } +
|
|
506
|
-
pairs(num).map { |a, b| "#{a} > #{b}" } +
|
|
507
|
-
pairs(str).map { |a, b| "#{a} == #{b}" } +
|
|
508
|
-
pairs(str).map { |a, b| "#{a} < #{b}" } +
|
|
509
|
-
# `resolver_numeric_leaves`, not the raw `num` `==`/`>` above
|
|
510
|
-
# safely use — SignTest's own suffix match
|
|
511
|
-
# (`match_suffix`/`.positive?` et al.) is a plain trailing-
|
|
512
|
-
# string strip, not `Evaluator`'s comparison-scanning, so an
|
|
513
|
-
# `Addition` in `num` would hit the exact "`0 + 0.to_s`" mis-
|
|
514
|
-
# split this generator's own `resolver_numeric_leaves` comment
|
|
515
|
-
# documents (`"0 + 0.positive?"` would parse as `0 + (0
|
|
516
|
-
# .positive?)`, not `(0 + 0).positive?`).
|
|
517
|
-
resolver_numeric_leaves(depth - 1).flat_map { |n| ["#{n}.positive?", "#{n}.negative?", "#{n}.zero?"] } +
|
|
518
|
-
(str + sample(numeric_array_productions(depth - 1)) + sample(string_array_productions(depth - 1)) + bounded(:array,
|
|
519
|
-
depth - 1))
|
|
520
|
-
.map do |x|
|
|
521
|
-
"#{x}.empty?"
|
|
522
|
-
end +
|
|
523
|
-
str.map { |s| "#{s}.match?(/a/)" } +
|
|
524
|
-
str.flat_map { |s| ["#{s}.present?", "#{s}.blank?"] } +
|
|
525
|
-
str.flat_map { |s| ["#{s}.start_with?(\"a\")", "#{s}.end_with?(\"a\")"] } +
|
|
526
|
-
pairs(sub).flat_map { |a, b| ["#{a} && #{b}", "#{a} || #{b}"] } +
|
|
527
|
-
sub.map { |b| "!#{b}" } +
|
|
528
|
-
block_predicate_productions(depth - 1) +
|
|
529
|
-
include_productions(depth - 1)
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
# `.all?`/`.any?`/`.none?` over each known-element-typed array
|
|
533
|
-
# source, with a predicate body drawn from `boolean_productions` at
|
|
534
|
-
# one less depth, evaluated against `BLOCK_PARAM` bound to the
|
|
535
|
-
# array's own element type — this is the only place `Resolver` and
|
|
536
|
-
# `Evaluator` are truly mutually recursive (a `BlockPredicate`'s own
|
|
537
|
-
# `predicate` field is a full `Evaluator` AST, not a `Resolver`
|
|
538
|
-
# leaf — confirmed directly), and the only construct in this whole
|
|
539
|
-
# generator that can nest into another block predicate (the real
|
|
540
|
-
# corpus already does this two levels deep —
|
|
541
|
-
# `examples/roster/bluebook/roster.bluebook`'s own `seats.any? { |s|
|
|
542
|
-
# assignments.none? { |a| … } }`).
|
|
543
|
-
#
|
|
544
|
-
# @param depth [Integer] maximum recursive steps remaining for the predicate body
|
|
545
|
-
# @return [Array<String>] `.all?`/`.any?`/`.none?` calls over each
|
|
546
|
-
# known-element-typed array source, one per predicate body `predicate_bodies`
|
|
547
|
-
# produces; `[]` if `depth` is negative
|
|
548
|
-
def block_predicate_productions(depth)
|
|
549
|
-
return [] if depth.negative?
|
|
550
|
-
|
|
551
|
-
[
|
|
552
|
-
["arr_num", :numeric], [numeric_array_literal(depth), :numeric],
|
|
553
|
-
["arr_str", :string], [string_array_literal(depth), :string]
|
|
554
|
-
].flat_map do |array_text, element_type|
|
|
555
|
-
predicate_bodies(element_type, depth).flat_map do |body|
|
|
556
|
-
%w[all? any? none?].map { |mode| "#{array_text}.#{mode} { |#{BLOCK_PARAM}| #{body} }" }
|
|
557
|
-
end
|
|
558
|
-
end
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
# The predicate body a block gets — `boolean_productions`, but with
|
|
562
|
-
# `BLOCK_PARAM` (bound to `element_type`) also admitted as a leaf,
|
|
563
|
-
# since inside the block it is exactly as usable as any other
|
|
564
|
-
# `Lookup` name (resolver.rb's own `interpret_with_element`: the
|
|
565
|
-
# bound element joins `attrs` for the span of one evaluation, no
|
|
566
|
-
# different from a top-level attribute — confirmed directly).
|
|
567
|
-
# `sample`d for the same reason every other internal list is — this
|
|
568
|
-
# feeds three more constructs per body (`all?`/`any?`/`none?`) times
|
|
569
|
-
# four array sources, so an unbounded body list here is exactly the
|
|
570
|
-
# kind of multiplier this file's own `bounded` comment warns about.
|
|
571
|
-
#
|
|
572
|
-
# @param element_type [Symbol] the type `BLOCK_PARAM` is bound to inside the block
|
|
573
|
-
# @param depth [Integer] maximum recursive steps remaining for the body
|
|
574
|
-
# @return [Array<String>] a sampled set of boolean expressions, with
|
|
575
|
-
# `BLOCK_PARAM` admitted as a leaf of `element_type`
|
|
576
|
-
def predicate_bodies(element_type, depth)
|
|
577
|
-
sample(with_element_leaf(element_type) { boolean_productions(depth) })
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
# `haystack.include?(needle)` — String haystack needs a String
|
|
581
|
-
# needle (raises otherwise, evaluator.rb's own `includes?`); Array
|
|
582
|
-
# haystack admits any needle type (compared via `equal?`, itself
|
|
583
|
-
# numeric-coerced-first). Both sides generated here, matching
|
|
584
|
-
# `Vocabulary::IncludeHaystack` exactly.
|
|
585
|
-
#
|
|
586
|
-
# @param depth [Integer] maximum recursive steps remaining
|
|
587
|
-
# @return [Array<String>] `haystack.include?(needle)` expressions: a String
|
|
588
|
-
# haystack with a String needle, or a numeric/string array haystack with a
|
|
589
|
-
# matching-type needle
|
|
590
|
-
def include_productions(depth)
|
|
591
|
-
str = bounded(:string, depth)
|
|
592
|
-
pairs(str).map { |haystack, needle| "#{haystack}.include?(#{needle})" } +
|
|
593
|
-
sample(numeric_array_productions(depth)).product(bounded(:numeric, depth)).map do |arr, needle|
|
|
594
|
-
"#{arr}.include?(#{needle})"
|
|
595
|
-
end +
|
|
596
|
-
sample(string_array_productions(depth)).product(bounded(:string, depth)).map do |arr, needle|
|
|
597
|
-
"#{arr}.include?(#{needle})"
|
|
598
|
-
end
|
|
599
|
-
end
|
|
600
|
-
|
|
601
|
-
# **Sampled, not a full cross product** — a full `list.product(list)`
|
|
602
|
-
# is what actually explodes this generator (numeric productions
|
|
603
|
-
# alone hit 8000+ by depth 2; squaring that for `&&`/`==` pairs is
|
|
604
|
-
# where "tens of thousands" becomes tens of millions). The
|
|
605
|
-
# combinatorics genuinely don't buy proof coverage: proving `Or`/
|
|
606
|
-
# `And`/`Not` themselves never crash needs one representative pair
|
|
607
|
-
# per depth (`evaluator.rb`'s own `interpret` does zero type-
|
|
608
|
-
# dependent work for those three — `interpret(left) || interpret
|
|
609
|
-
# (right)`, plain Ruby, no coercion, no receiver-type check at
|
|
610
|
-
# all — so a crash there could only come from left or right
|
|
611
|
-
# themselves, already covered by testing every operand on its own
|
|
612
|
-
# elsewhere in this same predicate set). `Compare`/`Include`/
|
|
613
|
-
# `Addition`/`Modulo` genuinely do real per-pair type coercion
|
|
614
|
-
# (`Evaluator.apply`/`less_than`/`Resolver.add`/`apply_modulo`), so
|
|
615
|
-
# those stay covered across every construct shape at every depth —
|
|
616
|
-
# just sampled evenly across each side's own operand set (leaves
|
|
617
|
-
# and deep productions alike, not just whichever the list happens
|
|
618
|
-
# to enumerate first) rather than every possible pairing of them.
|
|
619
|
-
SAMPLE_CAP = 14
|
|
620
|
-
|
|
621
|
-
# Evenly samples `list` down to at most `SAMPLE_CAP` entries, so a list
|
|
622
|
-
# size never compounds unbounded across recursive passes.
|
|
623
|
-
#
|
|
624
|
-
# @param list [Array<String>] the list to sample
|
|
625
|
-
# @return [Array<String>] `list` unchanged if it is at most `SAMPLE_CAP` entries;
|
|
626
|
-
# otherwise every `list.size.fdiv(SAMPLE_CAP).ceil`-th entry, evenly spaced
|
|
627
|
-
def sample(list) = list.size <= SAMPLE_CAP ? list : list.each_slice(list.size.fdiv(SAMPLE_CAP).ceil).map(&:first)
|
|
628
|
-
|
|
629
|
-
# Builds every pair of one sampled list with itself.
|
|
630
|
-
#
|
|
631
|
-
# @param list [Array<String>] the list to sample and pair with itself
|
|
632
|
-
# @return [Array<Array(String, String)>] the sampled list's own self cross product
|
|
633
|
-
def pairs(list)
|
|
634
|
-
sampled = sample(list)
|
|
635
|
-
sampled.product(sampled)
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
# `pairs`' own two-different-lists twin — used wherever the left
|
|
639
|
-
# and right of a construct have genuinely different safety
|
|
640
|
-
# requirements (`Modulo`'s own receiver vs. argument, below) and
|
|
641
|
-
# squaring the same sampled list wouldn't be correct.
|
|
642
|
-
#
|
|
643
|
-
# @param left [Array<String>] the left-hand list to sample
|
|
644
|
-
# @param right [Array<String>] the right-hand list to sample
|
|
645
|
-
# @return [Array<Array(String, String)>] the two sampled lists' own cross product
|
|
646
|
-
def cross(left, right) = sample(left).product(sample(right))
|
|
647
|
-
|
|
648
|
-
# The full set — every boolean-typed expression up to `MAX_DEPTH`,
|
|
649
|
-
# deduplicated (many shorter expressions are also produced, re-
|
|
650
|
-
# wrapped, at every deeper level — `.uniq` inside `productions`
|
|
651
|
-
# already collapses most of that; this is the final pass over the
|
|
652
|
-
# complete depth-`MAX_DEPTH` set specifically).
|
|
653
|
-
#
|
|
654
|
-
# @param max_depth [Integer] maximum recursive depth to generate up to
|
|
655
|
-
# @return [Array<String>] every boolean-typed expression up to `max_depth`,
|
|
656
|
-
# deduplicated
|
|
657
|
-
def all_predicates(max_depth = MAX_DEPTH)
|
|
658
|
-
productions(:boolean, max_depth).uniq
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
# Interprets one predicate against the shared synthetic
|
|
662
|
-
# state/attrs, returning `{ok: true, result: ...}` on any outcome
|
|
663
|
-
# `Evaluator.call` itself can express (a real true/false answer, or
|
|
664
|
-
# a clean `EvaluationError` — both are the sublanguage working
|
|
665
|
-
# correctly, never a finding) and `{ok: false, error: ...}` only
|
|
666
|
-
# for anything else escaping — the one shape this whole file exists
|
|
667
|
-
# to prove never happens for well-typed input.
|
|
668
|
-
#
|
|
669
|
-
# @param expr [String] a generated (or hand-written) expression source string
|
|
670
|
-
# @return [Hash{Symbol => Object}] `{ok: true, result: Object}` for a real
|
|
671
|
-
# answer, `{ok: true, result: :refused, message: String}` for a clean
|
|
672
|
-
# `EvaluationError`, or `{ok: false, error: StandardError}` for anything else
|
|
673
|
-
# escaping
|
|
674
|
-
def check(expr)
|
|
675
|
-
result = Hecks::Bluebook::Expression::Evaluator.call(expr, synthetic_state, synthetic_attrs)
|
|
676
|
-
{ ok: true, result: result }
|
|
677
|
-
rescue Hecks::Bluebook::Expression::EvaluationError => e
|
|
678
|
-
{ ok: true, result: :refused, message: e.message }
|
|
679
|
-
rescue StandardError => e
|
|
680
|
-
{ ok: false, error: e }
|
|
681
|
-
end
|
|
682
|
-
end
|
|
683
|
-
end
|
|
684
|
-
end
|