hecks 2.9.0 → 3.0.1
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 +51 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +16 -3
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +13 -4
- data/lib/hecks/adapters/driven/prism.rb +41 -8
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
- data/lib/hecks/behaviors/expectations.rb +1 -1
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
- data/lib/hecks/bluebook/chapter.rb +6 -2
- data/lib/hecks/bluebook/domain_port.rb +36 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
- data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
- data/lib/hecks/bluebook/expression/resolver.rb +3 -2
- data/lib/hecks/bluebook/hexagon.rb +13 -5
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator.rb +9 -4
- data/lib/hecks/bluebook/model_check.rb +23 -0
- data/lib/hecks/bluebook/pattern_subset.rb +38 -0
- data/lib/hecks/bluebook/query.rb +20 -5
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +2 -2
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +1 -1
- data/lib/hecks/cli/mcp.rb +3 -3
- data/lib/hecks/cli/model_check.rb +15 -8
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +224 -32
- data/lib/hecks/cli/project_diagrams.rb +4 -3
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +3 -3
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +5 -3
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -5
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +201 -52
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +1 -1
- data/lib/hecks/{facade → doors}/handle.rb +2 -2
- data/lib/hecks/{facade → doors}/json_door.rb +4 -4
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
- data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
- data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +1 -1
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +24 -37
- data/lib/hecks/forms/app.rb +6 -6
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/field_shape.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +1 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
- data/lib/hecks/language/bluebook/query.bluebook +7 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
- data/lib/hecks/mcp_stdio_guard.rb +2 -2
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/state_codec.rb +24 -5
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +5 -5
- data/lib/hecks/projections/deploy/lambda.rb +43 -32
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/template.rb +1 -1
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/shared.rb +1 -1
- data/lib/hecks/projections/diagrams.rb +3 -3
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/parser_table.rb +4 -4
- data/lib/hecks/projections/reference.rb +2 -3
- data/lib/hecks/projections/rust_vocabulary.rb +1 -1
- data/lib/hecks/projector/cli_projector.rb +106 -37
- data/lib/hecks/projector.rb +1 -1
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/release/runner/ci_publisher.rb +2 -2
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +14 -7
- data/lib/hecks/release/runner/npm_publisher.rb +6 -36
- data/lib/hecks/release/runner/preflight.rb +26 -2
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +30 -15
- data/lib/hecks/release/runner.rb +57 -6
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
- data/lib/hecks/runtime/dependency_planning.rb +20 -2
- data/lib/hecks/runtime/dispatcher.rb +53 -3
- data/lib/hecks/runtime/interpreting.rb +3 -0
- data/lib/hecks/runtime/loader.rb +65 -12
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/policy_interpreter.rb +5 -2
- data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
- data/lib/hecks/runtime/query_interpreter.rb +77 -0
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/refusal_wording.rb +23 -1
- data/lib/hecks/runtime/registry/verification.rb +163 -4
- data/lib/hecks/runtime/registry.rb +57 -3
- data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
- data/lib/hecks/runtime/value/coercion.rb +23 -135
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +4 -1
- data/lib/hecks/runtime.rb +20 -6
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +16 -16
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +32 -15
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +477 -12
- data/lib/hecks/facade/cli_door.rb +0 -96
- data/lib/hecks/facade/cli_runner.rb +0 -127
- data/lib/hecks/mcp_door_scope.rb +0 -95
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
//! Interprets the `Expr` AST — a structural port of Ruby's
|
|
2
|
+
//! `Evaluator`/`Resolver` (docs/implemented/guides/running-a-runtime.md).
|
|
3
|
+
|
|
4
|
+
use super::Refusal;
|
|
5
|
+
use crate::kernel::attribute_shapes::composite;
|
|
6
|
+
use crate::kernel::expression_operators::{self, comparison, OperatorCategory};
|
|
7
|
+
|
|
8
|
+
/// The dynamic runtime value an expression evaluates to — mirrors
|
|
9
|
+
/// Ruby's `Resolver#interpret`, plus `List(usize)`: a list field's length.
|
|
10
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
11
|
+
pub enum Value {
|
|
12
|
+
Int(i64),
|
|
13
|
+
Float(f64),
|
|
14
|
+
Str(String),
|
|
15
|
+
Bool(bool),
|
|
16
|
+
List(usize),
|
|
17
|
+
Nil,
|
|
18
|
+
/// A materialised list — distinct from the length-only `List(usize)`
|
|
19
|
+
/// above. Produced by an array literal or `.split`; never used for
|
|
20
|
+
/// `.include?`'s haystack (`expr_emitter.rb`'s `emit_include` rewrites
|
|
21
|
+
/// that shape at codegen time instead).
|
|
22
|
+
Array(Vec<Value>),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl Value {
|
|
26
|
+
/// Ruby's `Evaluator.truthy?`: false only for `Nil`/`Bool(false)`.
|
|
27
|
+
pub fn truthy(&self) -> bool {
|
|
28
|
+
!matches!(self, Value::Nil | Value::Bool(false))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub enum Field<'a> {
|
|
33
|
+
Value(Value),
|
|
34
|
+
Nested(&'a dyn Fielded),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// The lookup surface every generated struct implements. A dotted
|
|
38
|
+
/// `Lookup` resolves its head segment here, then walks `Field::Nested`.
|
|
39
|
+
pub trait Fielded {
|
|
40
|
+
fn field(&self, name: &str) -> Option<Field<'_>>;
|
|
41
|
+
|
|
42
|
+
/// The bare (undotted) `Lookup` reading of this object — Ruby's
|
|
43
|
+
/// `unwrap_scalar` fallthrough. `DerefNode` overrides to answer its id.
|
|
44
|
+
fn as_scalar(&self) -> Option<Value> {
|
|
45
|
+
None
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// The elements of a list-typed field, for the enumeration operators
|
|
49
|
+
/// (`.any?`/`.all?`/`.none?`/`.find`). Default `None`, like `as_scalar`.
|
|
50
|
+
fn items(&self, _name: &str) -> Option<Vec<Field<'_>>> {
|
|
51
|
+
None
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// A block's own parameter, checked before `rest` (real args/instance) —
|
|
56
|
+
/// Ruby's `interpret_with_element` merge order. Nested blocks chain a
|
|
57
|
+
/// `Bound` inside a `Bound`, matching Ruby's nested `merge`.
|
|
58
|
+
pub struct Bound<'a> {
|
|
59
|
+
pub name: &'a str,
|
|
60
|
+
pub value: Field<'a>,
|
|
61
|
+
pub rest: &'a dyn Fielded,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl<'a> Fielded for Bound<'a> {
|
|
65
|
+
fn field(&self, name: &str) -> Option<Field<'_>> {
|
|
66
|
+
if name == self.name {
|
|
67
|
+
return Some(match &self.value {
|
|
68
|
+
Field::Value(v) => Field::Value(v.clone()),
|
|
69
|
+
Field::Nested(obj) => Field::Nested(*obj),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
self.rest.field(name)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn items(&self, name: &str) -> Option<Vec<Field<'_>>> {
|
|
76
|
+
if name == self.name {
|
|
77
|
+
// A bound element is one member, never itself a list.
|
|
78
|
+
return None;
|
|
79
|
+
}
|
|
80
|
+
self.rest.items(name)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// A `Fielded` with nothing in it — Ruby's own `attrs` is `{}` too, e.g.
|
|
85
|
+
/// a value object checking its own invariants against only its fields.
|
|
86
|
+
pub struct NoFields;
|
|
87
|
+
impl Fielded for NoFields {
|
|
88
|
+
fn field(&self, _name: &str) -> Option<Field<'_>> {
|
|
89
|
+
None
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// `ensures`'s own `attrs`, plus `old` (pre-mutation instance) merged in
|
|
94
|
+
/// and checked first — Ruby's `Hash#merge` precedence, `enforce_ensures`.
|
|
95
|
+
pub struct WithOld<'a> {
|
|
96
|
+
pub args: &'a dyn Fielded,
|
|
97
|
+
pub old: &'a dyn Fielded,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl<'a> Fielded for WithOld<'a> {
|
|
101
|
+
fn field(&self, name: &str) -> Option<Field<'_>> {
|
|
102
|
+
if name == "old" {
|
|
103
|
+
return Some(Field::Nested(self.old));
|
|
104
|
+
}
|
|
105
|
+
self.args.field(name)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn items(&self, name: &str) -> Option<Vec<Field<'_>>> {
|
|
109
|
+
if name == "old" {
|
|
110
|
+
return None;
|
|
111
|
+
}
|
|
112
|
+
self.args.items(name)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// C2.3 (docs/semantics/bluebook-semantics.md): inside `ensures`, settled
|
|
117
|
+
/// state wins over a same-named argument (unlike `given`'s C2.2) — the
|
|
118
|
+
/// same drop `enforce_ensures` does, answered lazily per lookup.
|
|
119
|
+
pub struct StateFirst<'a> {
|
|
120
|
+
pub args: &'a dyn Fielded,
|
|
121
|
+
pub settled: &'a dyn Fielded,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
impl<'a> Fielded for StateFirst<'a> {
|
|
125
|
+
fn field(&self, name: &str) -> Option<Field<'_>> {
|
|
126
|
+
if self.settled.field(name).is_some() {
|
|
127
|
+
return None;
|
|
128
|
+
}
|
|
129
|
+
self.args.field(name)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn items(&self, name: &str) -> Option<Vec<Field<'_>>> {
|
|
133
|
+
if self.settled.field(name).is_some() {
|
|
134
|
+
return None;
|
|
135
|
+
}
|
|
136
|
+
self.args.items(name)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// An entity command's owning record, exposed as `parent` on the args
|
|
141
|
+
/// side (`enforce_givens`/`enforce_ensures`), merged after `old` — Ruby's
|
|
142
|
+
/// own `merge` order gives `old` precedence.
|
|
143
|
+
pub struct WithParent<'a> {
|
|
144
|
+
pub args: &'a dyn Fielded,
|
|
145
|
+
pub parent: &'a dyn Fielded,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
impl<'a> Fielded for WithParent<'a> {
|
|
149
|
+
fn field(&self, name: &str) -> Option<Field<'_>> {
|
|
150
|
+
if name == "parent" {
|
|
151
|
+
return Some(Field::Nested(self.parent));
|
|
152
|
+
}
|
|
153
|
+
self.args.field(name)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn items(&self, name: &str) -> Option<Vec<Field<'_>>> {
|
|
157
|
+
if name == "parent" {
|
|
158
|
+
return None;
|
|
159
|
+
}
|
|
160
|
+
self.args.items(name)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Re-exported here so generated call sites (`expr_emitter.rb` emits
|
|
165
|
+
// `crate::kernel::Comparison { .. }`) keep working unchanged.
|
|
166
|
+
pub use comparison::Comparison;
|
|
167
|
+
|
|
168
|
+
/// The three spellings of one Array-aggregation family, kept as one
|
|
169
|
+
/// node with a mode — matching Ruby's `BlockPredicate#mode`.
|
|
170
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
171
|
+
pub enum BlockMode {
|
|
172
|
+
All,
|
|
173
|
+
Any,
|
|
174
|
+
None,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
impl BlockMode {
|
|
178
|
+
/// The operator's own spelling, for the "expects a list" wording
|
|
179
|
+
/// Ruby renders from `"#{node.mode}?"`.
|
|
180
|
+
pub fn ruby_name(&self) -> &'static str {
|
|
181
|
+
match self {
|
|
182
|
+
BlockMode::All => "all?",
|
|
183
|
+
BlockMode::Any => "any?",
|
|
184
|
+
BlockMode::None => "none?",
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// The full Evaluator + Resolver AST, one recursive enum — each variant
|
|
190
|
+
/// is exactly one Ruby node type. An unconstructed variant for one
|
|
191
|
+
/// domain is expected, not dead code to prune.
|
|
192
|
+
#[derive(Debug, Clone)]
|
|
193
|
+
#[allow(dead_code)]
|
|
194
|
+
pub enum Expr {
|
|
195
|
+
Or(Box<Expr>, Box<Expr>),
|
|
196
|
+
And(Box<Expr>, Box<Expr>),
|
|
197
|
+
Not(Box<Expr>),
|
|
198
|
+
Compare { op: Comparison, left: Box<Expr>, right: Box<Expr> },
|
|
199
|
+
Include { haystack: Box<Expr>, needle: Box<Expr> },
|
|
200
|
+
Int(i64),
|
|
201
|
+
Float(f64),
|
|
202
|
+
Str(String),
|
|
203
|
+
Bool(bool),
|
|
204
|
+
Nil,
|
|
205
|
+
Add(Box<Expr>, Box<Expr>),
|
|
206
|
+
SignTest { op: Comparison, receiver: Box<Expr> },
|
|
207
|
+
Empty(Box<Expr>),
|
|
208
|
+
ToS(Box<Expr>),
|
|
209
|
+
Modulo { receiver: Box<Expr>, divisor: Box<Expr> },
|
|
210
|
+
Size(Box<Expr>),
|
|
211
|
+
Lookup(&'static str),
|
|
212
|
+
/// `receiver.any?`/`.all?`/`.none? { |param| predicate }` —
|
|
213
|
+
/// evaluated once per element with `param` bound (see `Bound`).
|
|
214
|
+
BlockPredicate { mode: BlockMode, receiver: Box<Expr>, param: &'static str, predicate: Box<Expr> },
|
|
215
|
+
/// `receiver.find { |param| predicate }.a.b` — first accepted
|
|
216
|
+
/// element, projected through `path`, or nil when nothing matches.
|
|
217
|
+
Find { receiver: Box<Expr>, param: &'static str, predicate: Box<Expr>, path: &'static [&'static str] },
|
|
218
|
+
/// `[a, b, c]` — a leaf like `Int`/`Str`/`Nil` above, not an
|
|
219
|
+
/// operator, so `interpret` handles it directly, not via
|
|
220
|
+
/// `dispatch_operator`.
|
|
221
|
+
Array(Vec<Expr>),
|
|
222
|
+
/// `receiver.match?(/pattern/flags)` — `pattern` is a real
|
|
223
|
+
/// Ruby-Regexp source, taken as-is between the slashes.
|
|
224
|
+
MatchesRegex { receiver: Box<Expr>, pattern: String, flags: String },
|
|
225
|
+
/// `receiver.present?` / `.blank?` — one node for the pair,
|
|
226
|
+
/// `negated` distinguishing them (`.blank?` is `.present?` negated).
|
|
227
|
+
Presence { receiver: Box<Expr>, negated: bool },
|
|
228
|
+
/// `receiver.set?` / `.unset?` — narrower than `Presence`: only
|
|
229
|
+
/// `!receiver.is_nil()`. An assigned-but-empty value is `.set?`,
|
|
230
|
+
/// unlike `Presence`'s Rails-standard emptiness reading.
|
|
231
|
+
Assignment { receiver: Box<Expr>, negated: bool },
|
|
232
|
+
/// `receiver.split("SEP")` — `Resolver::Split`. Produces a real
|
|
233
|
+
/// `Value::Array`, not `Value::List` — see that variant's own header.
|
|
234
|
+
Split { receiver: Box<Expr>, separator: String },
|
|
235
|
+
/// `receiver.start_with?("prefix")` — `Resolver::StartsWith`.
|
|
236
|
+
StartsWith { receiver: Box<Expr>, substring: String },
|
|
237
|
+
/// `receiver.end_with?("suffix")` — `Resolver::EndsWith`.
|
|
238
|
+
EndsWith { receiver: Box<Expr>, substring: String },
|
|
239
|
+
/// `receiver.first` — `Resolver::First`.
|
|
240
|
+
First(Box<Expr>),
|
|
241
|
+
/// `receiver.last` — `Resolver::Last`.
|
|
242
|
+
Last(Box<Expr>),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// `args`/`instance` from `Evaluator.call` — `args` is checked first for
|
|
246
|
+
/// every `Lookup`, matching `Resolver#fetch`. Pass `&NoFields` for `{}`.
|
|
247
|
+
pub struct EvalContext<'a> {
|
|
248
|
+
pub args: &'a dyn Fielded,
|
|
249
|
+
pub instance: &'a dyn Fielded,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Leaves (literals, `Lookup`) have no `OperatorCategory`, so they're
|
|
253
|
+
/// handled directly; everything else routes through `dispatch_operator`.
|
|
254
|
+
pub fn interpret(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
|
|
255
|
+
use Expr::*;
|
|
256
|
+
match expr {
|
|
257
|
+
Int(v) => Ok(Value::Int(*v)),
|
|
258
|
+
Float(v) => Ok(Value::Float(*v)),
|
|
259
|
+
Str(v) => Ok(Value::Str(v.clone())),
|
|
260
|
+
Bool(v) => Ok(Value::Bool(*v)),
|
|
261
|
+
Nil => Ok(Value::Nil),
|
|
262
|
+
Lookup(path) => lookup(path, ctx),
|
|
263
|
+
Array(elements) => Ok(Value::Array(elements.iter().map(|e| interpret(e, ctx)).collect::<Result<Vec<_>, _>>()?)),
|
|
264
|
+
_ => dispatch_operator(category_of(expr), expr, ctx),
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Which `OperatorCategory` a non-leaf `Expr` variant belongs to — a
|
|
269
|
+
/// hand-written association `interpret` never calls for a leaf, hence
|
|
270
|
+
/// the `unreachable!` there rather than a wrong category.
|
|
271
|
+
fn category_of(expr: &Expr) -> OperatorCategory {
|
|
272
|
+
use Expr::*;
|
|
273
|
+
match expr {
|
|
274
|
+
Or(..) | And(..) | Not(..) => OperatorCategory::Logical,
|
|
275
|
+
Include { .. } => OperatorCategory::Membership,
|
|
276
|
+
Compare { .. } => OperatorCategory::Comparison,
|
|
277
|
+
Add(..) | Modulo { .. } => OperatorCategory::Arithmetic,
|
|
278
|
+
SignTest { .. } => OperatorCategory::SignTest,
|
|
279
|
+
Empty(..) | Size(..) => OperatorCategory::Sized,
|
|
280
|
+
ToS(..) => OperatorCategory::ToString,
|
|
281
|
+
BlockPredicate { .. } | Find { .. } => OperatorCategory::Enumeration,
|
|
282
|
+
MatchesRegex { .. } => OperatorCategory::PatternMatch,
|
|
283
|
+
Presence { .. } | Assignment { .. } => OperatorCategory::Presence,
|
|
284
|
+
Split { .. } | StartsWith { .. } | EndsWith { .. } => OperatorCategory::Text,
|
|
285
|
+
First(..) | Last(..) => OperatorCategory::Positional,
|
|
286
|
+
Int(..) | Float(..) | Str(..) | Bool(..) | Nil | Lookup(..) | Array(..) => {
|
|
287
|
+
unreachable!("interpret's own leaf arms handle these before category_of is ever called")
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// Exhaustive over `OperatorCategory`, no wildcard `_ =>` arm: adding or
|
|
293
|
+
/// removing a variant (hecks project_kernel_capabilities) stops this
|
|
294
|
+
/// compiling until a matching arm exists. Never add one back — it would
|
|
295
|
+
/// silently route a new category to the wrong operator file.
|
|
296
|
+
fn dispatch_operator(category: OperatorCategory, expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
|
|
297
|
+
use expression_operators::*;
|
|
298
|
+
match category {
|
|
299
|
+
OperatorCategory::Logical => logical::interpret(expr, ctx),
|
|
300
|
+
OperatorCategory::Membership => membership::interpret(expr, ctx),
|
|
301
|
+
OperatorCategory::Comparison => comparison::interpret(expr, ctx),
|
|
302
|
+
OperatorCategory::Arithmetic => match expr {
|
|
303
|
+
Expr::Add(..) => arithmetic::add(expr, ctx),
|
|
304
|
+
Expr::Modulo { .. } => arithmetic::modulo(expr, ctx),
|
|
305
|
+
_ => Err(Refusal::TypeMismatch(format!("dispatch_operator(Arithmetic, ..) called with {expr:?} — a router bug"))),
|
|
306
|
+
},
|
|
307
|
+
OperatorCategory::SignTest => sign_test::interpret(expr, ctx),
|
|
308
|
+
OperatorCategory::Sized => match expr {
|
|
309
|
+
Expr::Empty(..) => sized::empty(expr, ctx),
|
|
310
|
+
Expr::Size(..) => sized::size(expr, ctx),
|
|
311
|
+
_ => Err(Refusal::TypeMismatch(format!("dispatch_operator(Sized, ..) called with {expr:?} — a router bug"))),
|
|
312
|
+
},
|
|
313
|
+
OperatorCategory::ToString => to_string::interpret(expr, ctx),
|
|
314
|
+
OperatorCategory::Enumeration => enumeration::interpret(expr, ctx),
|
|
315
|
+
OperatorCategory::PatternMatch => pattern_match::interpret(expr, ctx),
|
|
316
|
+
OperatorCategory::Presence => match expr {
|
|
317
|
+
Expr::Presence { .. } => presence::interpret(expr, ctx),
|
|
318
|
+
Expr::Assignment { .. } => presence::interpret_assignment(expr, ctx),
|
|
319
|
+
_ => Err(Refusal::TypeMismatch(format!("dispatch_operator(Presence, ..) called with {expr:?} — a router bug"))),
|
|
320
|
+
},
|
|
321
|
+
OperatorCategory::Text => text::interpret(expr, ctx),
|
|
322
|
+
OperatorCategory::Positional => positional::interpret(expr, ctx),
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// `Resolver#fetch`: the head segment checks `args` then `instance`;
|
|
327
|
+
/// later segments walk `composite::step`. Only the head segment can
|
|
328
|
+
/// refuse here — canonical text is validated at boot, so any refusal
|
|
329
|
+
/// past this point is a codegen bug, not a real business refusal.
|
|
330
|
+
fn lookup(path: &str, ctx: &EvalContext) -> Result<Value, Refusal> {
|
|
331
|
+
let mut segments = path.split('.');
|
|
332
|
+
let head = segments.next().unwrap();
|
|
333
|
+
let mut current = ctx
|
|
334
|
+
.args
|
|
335
|
+
.field(head)
|
|
336
|
+
.or_else(|| ctx.instance.field(head))
|
|
337
|
+
.ok_or_else(|| eval_error(format!("cannot resolve {head:?} — no such attribute or argument")))?;
|
|
338
|
+
|
|
339
|
+
for seg in segments {
|
|
340
|
+
current = composite::step(current, seg, head)?;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
composite::finish(current, path)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// The list-elements reading of a `Lookup` path — same head resolution
|
|
347
|
+
/// as `lookup`, then `Fielded::items` on the last segment (a list is
|
|
348
|
+
/// always the end of a path). `op` names the caller for error wording.
|
|
349
|
+
pub(crate) fn lookup_items<'a>(path: &str, ctx: &EvalContext<'a>, op: &str) -> Result<Vec<Field<'a>>, Refusal> {
|
|
350
|
+
let segments: Vec<&str> = path.split('.').collect();
|
|
351
|
+
let (head, rest) = segments.split_first().unwrap();
|
|
352
|
+
|
|
353
|
+
if rest.is_empty() {
|
|
354
|
+
// args first when the key is present there, state second — a
|
|
355
|
+
// non-list argument refuses rather than falling through to a
|
|
356
|
+
// same-named instance list.
|
|
357
|
+
let side: &'a dyn Fielded = if ctx.args.field(head).is_some() { ctx.args } else { ctx.instance };
|
|
358
|
+
return side
|
|
359
|
+
.items(head)
|
|
360
|
+
.ok_or_else(|| eval_error(format!("{op} expects a list, got {}", describe_field(side.field(head)))));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let (last, middle) = rest.split_last().unwrap();
|
|
364
|
+
let mut current = ctx
|
|
365
|
+
.args
|
|
366
|
+
.field(head)
|
|
367
|
+
.or_else(|| ctx.instance.field(head))
|
|
368
|
+
.ok_or_else(|| eval_error(format!("cannot resolve {head:?} — no such attribute or argument")))?;
|
|
369
|
+
for seg in middle {
|
|
370
|
+
current = composite::step(current, seg, head)?;
|
|
371
|
+
}
|
|
372
|
+
match current {
|
|
373
|
+
Field::Nested(obj) => obj
|
|
374
|
+
.items(last)
|
|
375
|
+
.ok_or_else(|| eval_error(format!("{op} expects a list, got {}", describe_field(obj.field(last))))),
|
|
376
|
+
Field::Value(v) => Err(eval_error(format!("{path} — cannot look up {last:?} on scalar {v:?}"))),
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
fn describe_field(field: Option<Field<'_>>) -> String {
|
|
381
|
+
match field {
|
|
382
|
+
None => "nothing".to_string(),
|
|
383
|
+
Some(Field::Value(v)) => format!("{v:?}"),
|
|
384
|
+
Some(Field::Nested(_)) => "an object".to_string(),
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
pub(crate) fn eval_error(message: String) -> Refusal {
|
|
389
|
+
Refusal::Fault(message)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#[cfg(test)]
|
|
393
|
+
mod lookup_tests {
|
|
394
|
+
use super::*;
|
|
395
|
+
|
|
396
|
+
// Shaped like a generated value object: one required boolean field,
|
|
397
|
+
// one optional — the exact shape `!previous_sessions.nil?` names
|
|
398
|
+
// (see `composite::step`'s own header).
|
|
399
|
+
struct Attendee {
|
|
400
|
+
previous_sessions: bool,
|
|
401
|
+
first_time: Option<bool>,
|
|
402
|
+
}
|
|
403
|
+
impl Fielded for Attendee {
|
|
404
|
+
fn field(&self, name: &str) -> Option<Field<'_>> {
|
|
405
|
+
match name {
|
|
406
|
+
"previous_sessions" => Some(Field::Value(Value::Bool(self.previous_sessions))),
|
|
407
|
+
"first_time" => Some(match self.first_time {
|
|
408
|
+
Some(b) => Field::Value(Value::Bool(b)),
|
|
409
|
+
None => Field::Value(Value::Nil),
|
|
410
|
+
}),
|
|
411
|
+
_ => None,
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
fn not_nil(field: &'static str, instance: &dyn Fielded) -> Value {
|
|
417
|
+
let expr = Expr::Not(Box::new(Expr::Lookup(field)));
|
|
418
|
+
let ctx = EvalContext { args: &NoFields, instance };
|
|
419
|
+
interpret(&expr, &ctx).expect("evaluates without refusing")
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#[test]
|
|
423
|
+
fn a_present_true_boolean_answers_not_nil() {
|
|
424
|
+
// `!previous_sessions.nil?` — the live crash, `previous_sessions:
|
|
425
|
+
// true`: `.nil?` is not a dedicated node in this grammar, so this
|
|
426
|
+
// is really `Lookup("previous_sessions.nil?")` negated.
|
|
427
|
+
let attendee = Attendee { previous_sessions: true, first_time: None };
|
|
428
|
+
assert_eq!(not_nil("previous_sessions.nil?", &attendee), Value::Bool(true));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#[test]
|
|
432
|
+
fn a_present_false_boolean_also_answers_not_nil() {
|
|
433
|
+
// The other half of the live crash: `false` is exactly as
|
|
434
|
+
// present as `true` — `walk_path` never inspects the value,
|
|
435
|
+
// only whether the head attribute resolved at all.
|
|
436
|
+
let attendee = Attendee { previous_sessions: false, first_time: None };
|
|
437
|
+
assert_eq!(not_nil("previous_sessions.nil?", &attendee), Value::Bool(true));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
#[test]
|
|
441
|
+
fn a_genuinely_unset_optional_field_still_answers_not_nil() {
|
|
442
|
+
// Matches Ruby parity: `walk_path` breaks to `nil` off any
|
|
443
|
+
// scalar before inspecting `.nil?`'s segment name, so `!x.nil?`
|
|
444
|
+
// reads `true` for any head attribute that resolved, set or not.
|
|
445
|
+
let attendee = Attendee { previous_sessions: true, first_time: None };
|
|
446
|
+
assert_eq!(not_nil("first_time.nil?", &attendee), Value::Bool(true));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
fn a_genuinely_missing_head_attribute_still_refuses() {
|
|
451
|
+
// An undeclared head attribute is still a codegen bug, not a
|
|
452
|
+
// legitimate absent value — see `lookup`'s own doc comment.
|
|
453
|
+
let attendee = Attendee { previous_sessions: true, first_time: None };
|
|
454
|
+
let expr = Expr::Not(Box::new(Expr::Lookup("not_a_real_attribute.nil?")));
|
|
455
|
+
let ctx = EvalContext { args: &NoFields, instance: &attendee };
|
|
456
|
+
assert!(interpret(&expr, &ctx).is_err());
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//! `+` and `.modulo` (`Resolver::Addition`/`Modulo`); both require numeric operands.
|
|
2
|
+
|
|
3
|
+
use crate::kernel::attribute_shapes::scalar;
|
|
4
|
+
use crate::kernel::expr::{eval_error, interpret as eval, EvalContext, Expr, Value};
|
|
5
|
+
use crate::kernel::Refusal;
|
|
6
|
+
|
|
7
|
+
pub fn add(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
|
|
8
|
+
let Expr::Add(l, r) = expr else {
|
|
9
|
+
return Err(Refusal::TypeMismatch(format!("arithmetic::add called with a non-addition node {expr:?} — a router bug")));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
sum(&eval(l, ctx)?, &eval(r, ctx)?)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub fn modulo(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
|
|
16
|
+
let Expr::Modulo { receiver, divisor } = expr else {
|
|
17
|
+
return Err(Refusal::TypeMismatch(format!("arithmetic::modulo called with a non-modulo node {expr:?} — a router bug")));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let r = require_number(&eval(receiver, ctx)?, "modulo")?.trunc() as i64;
|
|
21
|
+
let d = require_number(&eval(divisor, ctx)?, "modulo")?.trunc() as i64;
|
|
22
|
+
if d == 0 {
|
|
23
|
+
return Err(eval_error("divided by 0".to_string()));
|
|
24
|
+
}
|
|
25
|
+
Ok(Value::Int(floored_mod(r, d)))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Floored like Ruby's `Integer#%` (the result takes the divisor's sign); Rust's `%` truncates.
|
|
29
|
+
fn floored_mod(r: i64, d: i64) -> i64 {
|
|
30
|
+
// `i64::MIN % -1` overflows in Rust's `%`; every integer model answers 0 (C3.3).
|
|
31
|
+
if d == -1 {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
let raw = r % d;
|
|
35
|
+
if raw != 0 && (raw < 0) != (d < 0) { raw + d } else { raw }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[cfg(test)]
|
|
39
|
+
mod tests {
|
|
40
|
+
use super::floored_mod;
|
|
41
|
+
|
|
42
|
+
// Expected values are real `ruby -e 'puts X % Y'` results.
|
|
43
|
+
#[test]
|
|
44
|
+
fn matches_ruby_integer_modulo_across_every_sign_combination() {
|
|
45
|
+
assert_eq!(floored_mod(7, 3), 1);
|
|
46
|
+
assert_eq!(floored_mod(-7, 3), 2);
|
|
47
|
+
assert_eq!(floored_mod(7, -3), -2);
|
|
48
|
+
assert_eq!(floored_mod(-7, -3), -1);
|
|
49
|
+
assert_eq!(floored_mod(0, 3), 0);
|
|
50
|
+
assert_eq!(floored_mod(6, 3), 0);
|
|
51
|
+
assert_eq!(floored_mod(-6, 3), 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// C3.3 — `i64::MIN % -1` answers 0, never panics.
|
|
55
|
+
#[test]
|
|
56
|
+
fn i64_min_modulo_minus_one_is_zero() {
|
|
57
|
+
assert_eq!(floored_mod(i64::MIN, -1), 0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[cfg(test)]
|
|
62
|
+
mod float_sum_tests {
|
|
63
|
+
use super::sum;
|
|
64
|
+
use crate::kernel::expr::Value;
|
|
65
|
+
|
|
66
|
+
// C3.4 — a Float sum that overflows to infinity faults rather than answering inf.
|
|
67
|
+
#[test]
|
|
68
|
+
fn refuses_a_float_sum_that_is_not_finite() {
|
|
69
|
+
let result = sum(&Value::Float(1.0e308), &Value::Float(1.0e308));
|
|
70
|
+
assert!(result.is_err(), "1e308 + 1e308 must fault, not answer inf");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[cfg(test)]
|
|
75
|
+
mod sum_tests {
|
|
76
|
+
use super::sum;
|
|
77
|
+
use crate::kernel::expr::Value;
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn adds_ordinary_ints() {
|
|
81
|
+
assert_eq!(sum(&Value::Int(2), &Value::Int(3)).unwrap(), Value::Int(5));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[test]
|
|
85
|
+
fn mixed_int_float_promotes_to_float_like_ruby_coercion() {
|
|
86
|
+
assert_eq!(sum(&Value::Int(2), &Value::Float(3.5)).unwrap(), Value::Float(5.5));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Ruby's `Integer#+` promotes to Bignum and never overflows; this kernel has no bignum, so
|
|
90
|
+
// overflow refuses instead of panicking or wrapping.
|
|
91
|
+
#[test]
|
|
92
|
+
fn refuses_cleanly_on_overflow_instead_of_panicking_or_wrapping() {
|
|
93
|
+
let result = sum(&Value::Int(i64::MAX), &Value::Int(1));
|
|
94
|
+
assert!(result.is_err(), "i64::MAX + 1 must refuse, not silently wrap to i64::MIN");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn refuses_cleanly_on_negative_overflow() {
|
|
99
|
+
let result = sum(&Value::Int(i64::MIN), &Value::Int(-1));
|
|
100
|
+
assert!(result.is_err(), "i64::MIN + -1 must refuse, not silently wrap to i64::MAX");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn does_not_refuse_right_at_the_boundary() {
|
|
105
|
+
assert_eq!(sum(&Value::Int(i64::MAX - 1), &Value::Int(1)).unwrap(), Value::Int(i64::MAX));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Ruby's `Integer#+` promotes to Bignum; an `i64` cannot, so `checked_add` refuses on overflow
|
|
110
|
+
// instead of panicking (debug) or wrapping (release).
|
|
111
|
+
fn sum(lhs: &Value, rhs: &Value) -> Result<Value, Refusal> {
|
|
112
|
+
if let (Value::Int(l), Value::Int(r)) = (lhs, rhs) {
|
|
113
|
+
return l.checked_add(*r).map(Value::Int).ok_or_else(|| eval_error(format!("addition overflowed: {l} + {r} does not fit in a 64-bit integer")));
|
|
114
|
+
}
|
|
115
|
+
let l = require_number(lhs, "addition")?;
|
|
116
|
+
let r = require_number(rhs, "addition")?;
|
|
117
|
+
let sum = l + r;
|
|
118
|
+
// C3.4 — a non-finite Float sum faults like an Integer overflow.
|
|
119
|
+
if !sum.is_finite() {
|
|
120
|
+
return Err(eval_error(format!("addition overflowed: {l} + {r} is not a finite number")));
|
|
121
|
+
}
|
|
122
|
+
Ok(Value::Float(sum))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Shared with `sign_test.rs` — see this file's own header.
|
|
126
|
+
pub fn require_number(v: &Value, operation: &str) -> Result<f64, Refusal> {
|
|
127
|
+
scalar::numeric(v).ok_or_else(|| eval_error(format!("{operation} expects a number, got {v:?}")))
|
|
128
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//! `>=`, `<=`, `<`, `>`, `==`, `!=` (`Evaluator.apply`), reduced to two primitives plus negation.
|
|
2
|
+
|
|
3
|
+
use crate::kernel::attribute_shapes::scalar;
|
|
4
|
+
use crate::kernel::expr::{eval_error, interpret as eval, EvalContext, Expr, Value};
|
|
5
|
+
use crate::kernel::Refusal;
|
|
6
|
+
|
|
7
|
+
/// The comparison algebra: `less_than` and `equal` OR'd together, negated when `negated`.
|
|
8
|
+
#[derive(Debug, Clone, Copy)]
|
|
9
|
+
pub struct Comparison {
|
|
10
|
+
pub less_than: bool,
|
|
11
|
+
pub equal: bool,
|
|
12
|
+
pub negated: bool,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub fn interpret(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
|
|
16
|
+
let Expr::Compare { op, left, right } = expr else {
|
|
17
|
+
return Err(Refusal::TypeMismatch(format!("comparison::interpret called with a non-comparison node {expr:?} — a router bug")));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let l = eval(left, ctx)?;
|
|
21
|
+
let r = eval(right, ctx)?;
|
|
22
|
+
Ok(Value::Bool(apply(op, &l, &r)?))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Applies `op` to resolved values; `sign_test.rs` reuses it against the literal 0.
|
|
26
|
+
pub fn apply(op: &Comparison, lhs: &Value, rhs: &Value) -> Result<bool, Refusal> {
|
|
27
|
+
let lt = op.less_than && less_than(lhs, rhs)?;
|
|
28
|
+
let eq = op.equal && values_equal(lhs, rhs);
|
|
29
|
+
Ok(if op.negated { !(lt || eq) } else { lt || eq })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn less_than(lhs: &Value, rhs: &Value) -> Result<bool, Refusal> {
|
|
33
|
+
match (scalar::numeric(lhs), scalar::numeric(rhs)) {
|
|
34
|
+
(Some(l), Some(r)) => Ok(l < r),
|
|
35
|
+
_ => match (lhs, rhs) {
|
|
36
|
+
(Value::Str(l), Value::Str(r)) => Ok(l < r),
|
|
37
|
+
_ => Err(eval_error(format!("comparison of {lhs:?} with {rhs:?} failed"))),
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// `pub(crate)` so `membership.rs` reuses the numeric-coerced equality for array haystacks.
|
|
43
|
+
pub(crate) fn values_equal(lhs: &Value, rhs: &Value) -> bool {
|
|
44
|
+
match (scalar::numeric(lhs), scalar::numeric(rhs)) {
|
|
45
|
+
(Some(l), Some(r)) => l == r,
|
|
46
|
+
_ => lhs == rhs,
|
|
47
|
+
}
|
|
48
|
+
}
|